Compare commits

..

No commits in common. 'b2d43c01894d11c37c401e8993200cdb5293ec5d' and '7a64eeff094eb9dee5cda49f7d1fe57aa7c78ef9' have entirely different histories.

  1. 2
      .gitignore
  2. 20
      Makefile
  3. 8
      README.markdown
  4. 4
      docs/Makefile
  5. 139
      docs/generate_command_signatures.py
  6. 5
      docs/source/_data/cloc.csv
  7. 1294
      docs/source/_includes/overlays.rst
  8. 6
      docs/source/_includes/project-dependencies.rst
  9. 12
      docs/source/_includes/project-tests.rst
  10. BIN
      docs/source/_static/images/architecture-diagram.png
  11. 0
      docs/source/_static/images/slack-1.jpg
  12. 0
      docs/source/_static/images/slack-2.jpg
  13. 0
      docs/source/_static/images/slack-3.jpg
  14. 0
      docs/source/_static/images/twist-1.png
  15. 0
      docs/source/_static/images/twist-2.png
  16. 86
      docs/source/commands.rst
  17. 4
      docs/source/contact.rst
  18. 77
      docs/source/contributing.rst
  19. 32
      docs/source/getting-started.rst
  20. 236
      docs/source/how-to.rst
  21. 22
      docs/source/index.rst
  22. 125
      docs/source/introduction.rst
  23. 140
      docs/source/project.rst
  24. 107
      docs/source/reference.rst
  25. 29
      docs/source/releases.rst
  26. 115
      docs/source/topics-configuration.rst
  27. 24
      docs/source/topics-overlays.rst
  28. 11
      docs/source/topics.rst
  29. 118
      help/docs/commands/django.md
  30. 19
      help/docs/commands/messages.md
  31. 54
      help/docs/commands/mysql.md
  32. 57
      help/docs/commands/pgsql.md
  33. 6
      help/docs/commands/php.md
  34. 148
      help/docs/commands/posix.md
  35. 20
      help/docs/commands/python.md
  36. 69
      help/docs/config/command-file.md
  37. 17
      help/docs/config/variables.md
  38. 58
      help/docs/index.md
  39. 31
      help/docs/profiles/centos.md
  40. 39
      help/docs/profiles/ubuntu.md
  41. 171
      help/en/docs/cli.md
  42. 21
      help/en/docs/getting-started.md
  43. 14
      help/en/docs/how-to/create-executable-script.md
  44. 51
      help/en/docs/how-to/define-custom-command.md
  45. 41
      help/en/docs/how-to/post-message-slack.md
  46. 35
      help/en/docs/how-to/post-message-twist.md
  47. 56
      help/en/docs/how-to/use-with-commonkit.md
  48. 66
      help/en/docs/index.md
  49. 14
      help/en/docs/topics/itemized-commands.md
  50. 43
      help/en/docs/topics/templates.md
  51. 132
      help/en/docs/usage/django.md
  52. 119
      help/en/docs/usage/pgsql.md
  53. 264
      help/en/docs/usage/posix.md
  54. 49
      help/en/docs/usage/python.md
  55. 52
      help/en/mkdocs.yml
  56. 26
      help/mkdocs.yml
  57. 8
      meta.ini
  58. 382
      sandbox/cli.py
  59. 7
      sandbox/tease.py
  60. 516
      scripttease/cli/__init__.py
  61. 454
      scripttease/cli/initialize.py
  62. 208
      scripttease/cli/subcommands.py
  63. 6
      scripttease/data/inventory/nextcloud/meta.ini
  64. 6
      scripttease/data/inventory/pgsql/meta.ini
  65. 8
      scripttease/data/inventory/radicale/steps.ini
  66. 6
      scripttease/data/inventory/ubuntu/meta.ini
  67. 87
      scripttease/lib/commands/base.py
  68. 52
      scripttease/lib/commands/centos.py
  69. 96
      scripttease/lib/commands/django.py
  70. 73
      scripttease/lib/commands/messages.py
  71. 94
      scripttease/lib/commands/mysql.py
  72. 138
      scripttease/lib/commands/pgsql.py
  73. 6
      scripttease/lib/commands/php.py
  74. 301
      scripttease/lib/commands/posix.py
  75. 22
      scripttease/lib/commands/python.py
  76. 64
      scripttease/lib/commands/ubuntu.py
  77. 2
      scripttease/lib/factories.py
  78. 3
      scripttease/lib/loaders/base.py
  79. 1
      scripttease/lib/loaders/ini.py
  80. 8
      setup.py
  81. 9
      tests/examples/users.ini
  82. 9
      tests/test_lib_commands.py
  83. 52
      tests/test_lib_commands_base.py
  84. 27
      tests/test_lib_commands_django.py
  85. 17
      tests/test_lib_commands_pgsql.py
  86. 2
      tests/test_lib_commands_posix.py

2
.gitignore vendored

@ -12,7 +12,7 @@ _scraps
build
dist
docs/build
help/*/site
help/site
htmlcov
tmp.*
tmp

@ -9,13 +9,6 @@ PROJECT_NAME := python-scripttease
# The directory where test coverage is generated.
COVERAGE_PATH := docs/build/html/coverage
# $(file) may not work depending upon version of make.
#RELEASE := $(strip $(file < RELEASE.txt))
RELEASE := $(strip $(shell cat RELEASE.txt))
VERSION := $(strip $(shell cat VERSION.txt))
DOCS_PATH := $(PROJECT_HOME)/docs_diff6_com
DOCS_WWW := $(DOCS_PATH)/www
# Attempt to load a local makefile which may override any of the values above.
-include local.makefile
@ -74,19 +67,6 @@ publish:
secure:
bandit -r $(PACKAGE_NAME);
#> support - Build support (help) and reference documentation.
support:
cd docs && make dirhtml;
cd help/en/ && mkdocs build;
test -d $(DOCS_WWW)/en/$(PROJECT_NAME)/$(RELEASE) || mkdir -p $(DOCS_WWW)/en/$(PROJECT_NAME)/$(RELEASE);
test -d $(DOCS_WWW)/en/$(PROJECT_NAME)/$(RELEASE)/reference || mkdir -p $(DOCS_WWW)/en/$(PROJECT_NAME)/$(RELEASE)/reference;
cp -R docs/build/dirhtml/* $(DOCS_WWW)/en/$(PROJECT_NAME)/$(RELEASE)/reference/;
cp -R help/en/site/* $(DOCS_WWW)/en/$(PROJECT_NAME)/$(RELEASE)/;
cp meta.ini $(DOCS_PATH)/config/$(PROJECT_NAME).ini;
cd $(DOCS_WWW)/en/$(PROJECT_NAME) && ln -fs $(RELEASE) latest;
cd $(DOCS_WWW)/en/$(PROJECT_NAME) && ln -fs $(RELEASE) stable; # if product is stable
cd $(DOCS_PATH) && make build;
#> tests - Run unit tests and generate coverage report.
tests:
coverage run --source=. -m pytest;

@ -1,13 +1,7 @@
# Python Script Tease
![](https://img.shields.io/badge/status-active-green.svg)
![](https://img.shields.io/badge/stage-stable-green.svg)
![](https://img.shields.io/badge/stage-development-blue.svg)
![](https://img.shields.io/badge/coverage-100%25-green.svg)
A collection of classes and commands for automated command line scripting using Python.
## Install
```bash
pip install python-scripttease;
```

@ -54,8 +54,8 @@ clean:
.PHONY: html
html:
#resrc pkg docs -I ../packages.ini -O rst --no-heading > source/_includes/project-dependencies.rst;
#./generate_command_signatures.py > source/_includes/overlays.rst
resrc pkg docs -I ../packages.ini -O rst --no-heading > source/_includes/project-dependencies.rst;
./generate_command_signatures.py > source/_includes/overlays.rst
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
@echo
@echo "Build finished. The HTML pages are in $(BUILDDIR)/html."

@ -0,0 +1,139 @@
#! /usr/bin/env python
# Imports
from collections import OrderedDict
import inspect
import sys
# Set path before importing overlays.
sys.path.append("../")
# Import overlays
from scripttease.library.overlays.common import COMMON_MAPPINGS
from scripttease.library.overlays.centos import MAPPINGS as CENTOS_MAPPINGS
from scripttease.library.overlays.django import DJANGO_MAPPINGS
from scripttease.library.overlays.mysql import MYSQL_MAPPINGS
from scripttease.library.overlays.pgsql import PGSQL_MAPPINGS
from scripttease.library.overlays.posix import POSIX_MAPPINGS
from scripttease.library.overlays.ubuntu import MAPPINGS as UBUNTU_MAPPINGS
# Functions
# https://stackoverflow.com/a/52003056/241720
def get_signature(fn):
params = inspect.signature(fn).parameters
args = []
kwargs = OrderedDict()
for p in params.values():
if p.default is p.empty:
args.append(p.name)
else:
kwargs[p.name] = p.default
return args, kwargs
def print_description(text):
print(text)
print("")
def print_heading(title):
print(title)
print("=" * len(title))
print("")
def print_mapping(commands, excludes=None):
keys = list(commands.keys())
keys.sort()
_excludes = excludes or dict()
for key in keys:
if key in _excludes:
continue
func = commands[key]
docstring = func.__doc__
if not docstring:
continue
print(key)
print("-" * len(key))
print("")
for i in docstring.split("\n"):
print(i.strip())
# print("")
print(".. code-block:: ini")
print("")
print(" [run %s command]" % key)
args, kwargs = get_signature(func)
line = list()
for a in args:
if a != "kwargs":
line.append(a)
print(" %s: %s" % (key, " ".join(line)))
for option, value in kwargs.items():
if value is True:
_value = "yes"
elif value is False:
_value = "no"
else:
_value = value
print(" %s: %s" % (option, value))
print("")
# Overlay output.
print(".. generated by generate_command_signatures.py")
print("")
print_heading("Common")
print_description("Common commands are available to all overlays.")
print_mapping(COMMON_MAPPINGS)
print_heading("Django")
print_description("Django commands are available to all overlays.")
print_mapping(DJANGO_MAPPINGS)
print_heading("MySQL")
print_description("MySQL commands.")
print_mapping(MYSQL_MAPPINGS)
print_heading("Postgres")
print_description("Postgres commands.")
print_mapping(PGSQL_MAPPINGS)
print_heading("POSIX")
print_description("Posix commands form the basis of overlays for *nix platforms.")
print_mapping(POSIX_MAPPINGS, excludes=["func"])
exclude_from_centos = COMMON_MAPPINGS.copy()
exclude_from_centos.update(DJANGO_MAPPINGS)
exclude_from_centos.update(MYSQL_MAPPINGS)
exclude_from_centos.update(PGSQL_MAPPINGS)
exclude_from_centos.update(POSIX_MAPPINGS)
print_heading("Cent OS")
print_description("The Cent OS overlay incorporates commands specific to that platform as well as commands from "
"common, Django, MySQL, Postgres, and POSIX.")
print_mapping(CENTOS_MAPPINGS, excludes=exclude_from_centos)
exclude_from_ubuntu = COMMON_MAPPINGS.copy()
exclude_from_ubuntu.update(DJANGO_MAPPINGS)
exclude_from_ubuntu.update(MYSQL_MAPPINGS)
exclude_from_ubuntu.update(PGSQL_MAPPINGS)
exclude_from_ubuntu.update(POSIX_MAPPINGS)
print_heading("Ubuntu")
print_description("The Ubuntu overlay incorporates commands specific to that platform as well as commands from "
"common, Django, MySQL, Postgres, and POSIX.")
print_mapping(UBUNTU_MAPPINGS, excludes=exclude_from_ubuntu)

@ -1,4 +1,3 @@
files,language,blank,comment,code
23,Python,1230,1178,2094
12,INI,54,83,174
35,SUM,1284,1261,2268
22,Python,1124,981,1847
22,SUM,1124,981,1847

1 files language blank comment code
2 23 22 Python 1230 1124 1178 981 2094 1847
3 12 22 INI SUM 54 1124 83 981 174 1847
35 SUM 1284 1261 2268

File diff suppressed because it is too large Load Diff

@ -1,6 +1,10 @@
Requirements
------------
jinja2
------
**Install**
.. code-block:: bash
@ -10,6 +14,7 @@ jinja2
pygments
--------
**Install**
.. code-block:: bash
@ -19,6 +24,7 @@ pygments
python-commonkit
----------------
**URLs**
- `Source Code <https://github.com/develmaycare/python-commonkit>`_

@ -1,7 +1,7 @@
Coverage Requirements
.....................
---------------------
We always strive for 100% coverage in the ``master`` branch. The CLI is currently ignored.
100% coverage is required for the ``master`` branch.
See `current coverage report <coverage/index.html>`_.
@ -9,7 +9,7 @@ See `current coverage report <coverage/index.html>`_.
:file: ../_data/cloc.csv
Set Up for Testing
..................
------------------
Install requirements:
@ -35,16 +35,16 @@ Run a specific test:
.. code-block:: bash
python -m pytest tests/path/to/test.py
python -m pytest tests/units/path/to/test.py
To allow output from print statements within a test method, add the ``-s`` switch:
.. code-block:: bash
python -m pytest -s tests/path/to/test.py
python -m pytest -s tests/units/path/to/test.py
Reference
.........
---------
- `coverage <https://coverage.readthedocs.io/en/v4.5.x/>`_
- `pytest <https://pytest.org>`_

Binary file not shown.

Before

Width:  |  Height:  |  Size: 33 KiB

Before

Width:  |  Height:  |  Size: 301 KiB

After

Width:  |  Height:  |  Size: 301 KiB

Before

Width:  |  Height:  |  Size: 393 KiB

After

Width:  |  Height:  |  Size: 393 KiB

Before

Width:  |  Height:  |  Size: 33 KiB

After

Width:  |  Height:  |  Size: 33 KiB

Before

Width:  |  Height:  |  Size: 111 KiB

After

Width:  |  Height:  |  Size: 111 KiB

Before

Width:  |  Height:  |  Size: 181 KiB

After

Width:  |  Height:  |  Size: 181 KiB

@ -0,0 +1,86 @@
.. _commands:
********
Commands
********
.. code-block:: text
usage: tease [-h] [-c] [-C= VARIABLES] [-d] [-D] [-f= FILTERS] [-O= OPTIONS] [-s] [-T= TEMPLATE_LOCATIONS] [-w= OUTPUT_FILE] [-V= VARIABLES_FILE]
[-v] [--version]
[path]
positional arguments:
path The path to the configuration file.
optional arguments:
-h, --help show this help message and exit
-c, --color Enable code highlighting for terminal output.
-C= VARIABLES, --context= VARIABLES
Context variables for use in pre-parsing the config and templates. In the form of: name:value
-d, --docs Output documentation instead of code.
-D, --debug Enable debug output.
-f= FILTERS, --filter= FILTERS
Filter the commands in the form of: attribute:value
-O= OPTIONS, --option= OPTIONS
Common command options in the form of: name:value
-s, --script Output commands as a script.
-T= TEMPLATE_LOCATIONS, --template-path= TEMPLATE_LOCATIONS
The location of template files that may be used with the template command.
-w= OUTPUT_FILE, --write= OUTPUT_FILE
Write the output to disk.
-V= VARIABLES_FILE, --variables-file= VARIABLES_FILE
Load variables from a file.
-v Show version number and exit.
--version Show verbose version information and exit.
NOTES
This command is used to parse configuration files and output the commands.
Using the Tease Command
=======================
The ``tease`` command may be used to parse a configuration file, providing additional utilities for working with commands.
The ``path`` argument defaults to ``commands.ini``.
Context Variables May be Provided on the Command Line
-----------------------------------------------------
To supply context variables on the command line:
.. code-block:: bash
tease -C domain_name:example.com -C domain_tld:example_com
Loading Context Variables from a File
-------------------------------------
Context variables may be loaded from a file:
.. code-block:: ini
[domain]
name = example.com
tld = example_com
The variables above are available as ``section_key``. For example, ``domain_name`` is ``example.com``.
.. code-block:: bash
tease -V variables.ini
Setting Common Options for All Commands
---------------------------------------
Rather than include a common parameter in the configuration file, it is possible to specify a common option on the command line.
.. code-block:: bash
tease -O sudo:yes
The Difference Between Variables and Options
--------------------------------------------
Variables are used to pre-process configuration files as templates, while common options are passed to *all* command instances.

@ -10,14 +10,14 @@ General Contact
Please use the `contact form at develmaycare.com`_ to inquire about training, commercial development or support, to
report security issues, and other questions or communications.
.. _contact form at develmaycare.com: https://develmaycare.com/contact/?product=python-scripttease
.. _contact form at develmaycare.com: https://develmaycare.com/contact/?product=ScriptTease
Issue Tracker
=============
Bugs and feature requests are logged with the `issue tracker`_. Do *not* log security issues here.
.. _issue tracker: https://gittraction.com/diff6/python-scripttease/issues
.. _issue tracker: https://github.com/develmaycare/python-scripttease/issues
Support
=======

@ -1,77 +0,0 @@
************
Contributing
************
We welcome contributions to this and any of `our open source projects`_. There are a number of ways to participate and contribute. See :ref:`contact`.
.. _our open source projects: https://docs.diff6.com
Issue Management
================
Reporting Issues
----------------
If you have found a bug or error in the documentation, please submit a request. See :ref:`contact`.
.. important::
Do **not** report security issues using the issue tracker. Instead, send an email to security@develmaycare.com with details on the issue you've discovered.
Submitting Feature Requests
---------------------------
Although we reserve the right to decline new features, we welcome all feature requests. See :ref:`contact`.
Testing and Quality Control
---------------------------
Testing involves using Script Tease in real life or in development. Feel free to report any issues you find, or to improve the unit tests.
Pull Requests
-------------
Pull requests are welcome. Such requests should be associated with an issue. We may ignore pull requests that do not have a corresponding issue, so create an issue if one does not already exist.
Promotion
=========
You may help spread awareness of Script Tease by writing blog posts. We are happy to link out to reviews and tutorials from our web site. `Let us know if you've created a blog post`_ that we can share. Be sure to include a link to the post.
You may also provide us with a guest post to be included on our blog.
.. _Let us know if you've created a blog post: https://develmaycare.com/contact/?product=Script%20Tease
.. note::
We reserve the right to proof and approve or decline all content posted on our web site.
Development
===========
Setting Up For Development
--------------------------
1. Clone the repo at https://github.com/develmaycare/python-scripttease
2. Create a virtual environment and install the requirements from ``requirements.pip``
Style Guide
-----------
Script Tease follows `PEP8`_ and (where appropriate) the `Django style guide`_ and `JavaScript Standard Style`_.
.. _Django style guide: https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/
.. _JavaScript Standard Style: https://standardjs.com
.. _PEP8: https://www.python.org/dev/peps/pep-0008/
We *do* make a few exceptions and provide additional guidance which is documented in our `developer docs`_.
.. _developer docs: https://docs.develmaycare.com/en/developer/
Dependencies
------------
.. include:: _includes/dependencies.rst
Testing
-------
.. include:: _includes/tests.rst

@ -0,0 +1,32 @@
.. _getting-started:
***************
Getting Started
***************
System Requirements
===================
Python 3.6 or greater is required.
Install
=======
To install:
.. code-block:: bash
pip install python-scripttease;
Configuration
=============
See :ref:`topics-configuration` for creating a command configuration file.
FAQs
====
Have a question? `Just ask`_!
.. _Just ask: https://develmaycare.com/contact/?product=Script%20Tease

@ -0,0 +1,236 @@
.. _how-to:
******
How To
******
Create a New Command Overlay
============================
:ref:`topics-overlays` are used to define the commands supported by a given application, service, or operating system. Commands are defined as a function.
1) Define a Module
------------------
The first step is to create a new module in which functions will be defined.
.. code-block:: python
# module_name.py
from ..commands import Command
For overlays that represent an operating system, the ``command_exists()`` function is required:
.. code-block:: python
def command_exists(name):
return name in MAPPINGS
2) Define Command Function
--------------------------
The purpose of each function is to provide an interface for instantiating a :py:class:`scripttease.library.commands.base.Command` instance. The example below is taken from the ``posix`` module.
.. code-block:: python
# module_name.py
# ...
def mkdir(path, mode=None, recursive=True, **kwargs):
"""Create a directory.
- path (str): The path to be created.
- mode (int | str): The access permissions of the new directory.
- recursive (bool): Create all directories along the path.
"""
kwargs.setdefault("comment", "create directory %s" % path)
statement = ["mkdir"]
if mode is not None:
statement.append("-m %s" % mode)
if recursive:
statement.append("-p")
statement.append(path)
return Command(" ".join(statement), **kwargs)
The arguments and any specific keyword arguments are automatically used by the parser, but also serve as a simple interface for programmatic use.
Each function *must* also accept ``**kwargs`` and should set a default for ``comment`` as above.
.. important::
Rather than the usual Spinx-based documentation, define the docstring as shown above. This is used to automatically create the documentation for the command.
3) Add Functions to the Mapping
-------------------------------
The final step adds the function to the mapping. This makes it available to the command factory.
.. code-block:: python
# module_name.py
# ...
MAPPINGS = {
'mkdir': mkdir,
}
For overlays that represent an operating system, ``MAPPINGS`` is required -- in addition to ``command_exists()`` above. For commands that are specific to service or application, the name of the dictionary may be anything that is appropriate. For example, ``DJANGO_MAPPINGS``.
Additionally, for an operating system overlay, you may wish to import other mappings and incorporate them into ``MAPPINGS``.
.. code-block:: python
# module_name.py
from ..commands import Command
from .common import COMMON_MAPPINGS
from .django import DJANGO_MAPPINGS
from .pgsql import PGSQL_MAPPINGS
MAPPINGS = {
# ...
}
MAPPINGS.update(COMMON_MAPPINGS)
MAPPINGS.update(DJANGO_MAPPINGS)
MAPPINGS.update(PGSQL_MAPPINGS)
4) Update Documentation
-----------------------
Add the command mappings to the ``docs/generate_command_signatures.py`` file. See the script for more details.
Export Commands as a Script
===========================
You can export commands as a read-to-use script. For example:
.. code-block:: python
config = Config("commands.ini")
if not config.load():
print("Bummer!")
exit()
script = config.as_script()
print(script)
Post a Message to Slack
=======================
The slack function may be used to send a message to a Slack channel. This uses the Incoming Webhooks feature, which requires some additional setup.
.. note::
The following steps were accurate as of September 2020.
**1.** Log in to Slack and go to `Your Apps`_.
.. _Your Apps: https://api.slack.com/apps
**2.** Create a new Slack app.
**3.** On the next page, select Incoming Webhooks and then toggle activation.
.. image:: /_static/images/slack-1.jpg
**4.** Next click Add new Webhook to Workspace and select the channel to which the message will be posted.
.. image:: /_static/images/slack-2.jpg
.. image:: /_static/images/slack-3.jpg
**5.** Copy the URL for the new webhook to use as the ``url`` parameter for the Slack command.
.. code-block:: ini
[send a message to slack]
slack: "This is a test message."
url: the URL you created goes here
Post a Message to Twist
=======================
The twist function may be used to send a message to Twist, which requires some additional setup.
.. note::
The following steps were accurate as of September 2020.
**1.** Log in to Twist and from the profile menu go to Add Integrations. Then click on Build and "Add a new integration".
**2.** Provide the requested info.
.. image:: _static/images/twist-1.png
**3.** After submitting this info, go to Installation. Select a channel and who to notify. Then click "Install integration".
.. image:: _static/images/twist-2.png
**4.** Copy the "Post content manually" URL for use in your configuration file.
.. code-block:: ini
[post a message to twist]
twist: "This is a test message."
url: the URL you created goes here
Use Script Tease With Common Kit
================================
Since the focus of Script Tease is to convert plain text instructions into valid command line statements, it does *not* provide support for executing those statements either locally or remotely. However, The shell component of `python-commonkit`_ *does* provide support for executing commands in local POSIX environments.
.. _python-commonkit: https://docs.develmaycare.com/en/python-commonkit/stable/components/#module-commonkit.shell
Here is an example of how to use these packages together:
.. code-block:: python
from commonkit.shell import Command
from scripttease.parsers.utils import load_commands
def execute(step):
command = Command(
step.statement,
comment=step.comment,
path=step.cd,
prefix=step.prefix,
shell=step.shell
)
# Sudo is a different class, but identical in behavior.
command.sudo = step.sudo
if command.run():
print("[success] %s" % step.comment)
else:
print("[failure] %s" % step.comment)
if step.stop:
print("I can't go on: %s" % command.error)
exit(command.code)
# Load SCRIPT TEASE commands from an INI file. These are instances of either Command or ItemizedCommand found in
# scripttease.library.commands
steps = load_commands("path/to/steps.ini")
# A failure to load results in None.
if steps is None:
print("Failed to load steps.")
exit(1)
# Iterate through each step to create a COMMON KIT command.
for step in steps:
# To preview ...
# print(step.get_statement(cd=True))
if step.is_itemized:
for substep in step.get_commands():
execute(substep)
else:
execute(step)
Common Kit is already a dependency of Script Tease so it is installed by default. The ``execute()`` function is a shortcut that helps deal with itemized commands. The path (``step.cd``) is automatically handled by Common Kit's Command class.

@ -1,20 +1,22 @@
Script Tease Developer Documentation
====================================
Script Tease
============
Resources for turning plain text instructions (INI, YAML) into Bash scripting statements.
.. raw:: html
.. note::
This documentation is for developers interested in working with the Script Tease library to create a custom implementation, or to contribute to the product. For usage information, please refer to the `user help page`_.
.. _user help page: https://docs.diff6.com/en/python-scripttease
<p>
<a href="https://develmaycare.com/products/python/script-tease/">Project Home Page</a>
</p>
.. toctree::
:maxdepth: 2
Introduction <introduction>
Code Reference <reference>
Releases <releases>
Contributing <contributing>
Getting Started <getting-started>
How To <how-to>
Commands <commands>
Topics <topics>
Reference <reference>
Project <project>
Contact <contact>
Indices and tables

@ -7,96 +7,30 @@ Introduction
Overview
========
The Script Tease package takes plain text instructions in INI or YAML (currently untested) format and converts them into valid command line statements for a given platform (currently CentOS and Ubuntu are supported). It does *not* provide support for executing these statements.
Script Tease is a library and command line tool for generating commands programmatically or (especially) using configuration files.
Architecture
============
The provided command line interface (the ``tease`` command) loads configuration files containing instructions and may output these as a Bash script or as documentation.
However, it is possible to create your own implementation to provide input and gather output from the Script Tease library.
.. image:: _static/images/architecture-diagram.png
The primary focus (and limit) is to convert plain text instructions into valid command line statements for a given platform (see `Overlays`_). It does *not* provide support for executing those statements.
Concepts
========
Loaders
-------
A Loader instance is responsible for reading configuration files and parsing the contents into a) a command name, b) arguments, c) keyword arguments.
Command instances are *not* created by the loader. This is the job of the :py:func:`scriptease.lib.factories.command_factory`.
Separating Command generation from loading configuration files makes the loader classes simpler, and keeps the overall system more flexible.
Command Functions
-----------------
Generating Commands
-------------------
Commands are represented by Python functions. All functions return either a :py:class:`scripttease.lib.commands.base.Command` instance, or in some cases an instance of :py:class:`scripttease.lib.commands.base.MultipleCommands`. (The Template command may also be returned; it is handled specially.)
Script Tease may be used in two (2) ways:
.. code-block:: python
1. Using the library to programmatically define commands and export them as command line statements. See :ref:`developer-reference`.
2. Using the ``tease`` command to generate commands from a configuration file. See :ref:`topics-configuration`.
def copy(from_path, to_path, overwrite=False, recursive=False, **kwargs):
# ...
You could of course use this function directly:
.. code-block:: python
command = copy("/etc", "/tmp/", recursive=True)
print(command.get_statement())
But the point of Script Tease is to take plain text instructions and turn them into command line statements.
Mappings
Overlays
--------
So the `copy` function is mapped to a name that may be used in a configuration file:
.. code-block:: python
MAPPINGS = {
# ...
'copy': copy,
# ...
}
An *overlay* is a set of command meta functions that define the capabilities of a specific operating system.
Such mappings are used by the :py:func:`scriptease.lib.factories.command_factory` to match a command name in a configuration file to the Python function that instantiates the Command.
.. note::
At present, the only fully defined operating system overlays are for Cent OS and Ubuntu.
Configuration File
------------------
The configuration that invokes this command would like like:
.. code-block:: ini
[create a copy of the etc directory]
copy: /etc /tmp/
recursive: yes
Note that the configuration is self-documenting, and in fact, it is possible to output commands as documentation rather than a script.
Variables File
--------------
Prior to a Loader processing commands, configuration files may be parsed as Jinja2 templates. This allows variables to be loaded from a file and passed to the loader as a Context instance.
.. code-block:: ini
# variables.ini
[db_name]
comment: The name of the database is the same as the domain name.
value: example_app
In the configuration file:
.. code-block:: ini
[create the database]
pgsql.create: {{ db_name }}
Variables are collected in a :py:class:`scripttease.lib.contexts.Context` instance and are passed to the Loader.
See :ref:`topics-overlays`.
Terms and Definitions
=====================
@ -104,43 +38,10 @@ Terms and Definitions
command
When used in Script Tease documentation, this is a command instance which contains the properties and parameters for a command line statement.
profile
A profile represents the commands available to a specific operating system.
statement
A specific statement (string) to be executed. A *statement* is generated by a *command*.
A specific statement (string) to be executed. A *statement* is contained within a *command*.
License
=======
Script Tease is released under the BSD 3 clause license.
.. code-block:: text
Copyright (c) Pleasant Tents, LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Pleasant Tents, LLC nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -0,0 +1,140 @@
*******
Project
*******
Contributing
============
We welcome contributions to this and any of `our open source projects`_. There are a number of ways to participate and contribute. See :ref:`contact`.
.. _our open source projects: https://develmaycare.com/products/
Reporting Issues
----------------
Perhaps the easiest way to contribute is to submit an issue. If you have found a bug or error in the documentation, please submit a request. See :ref:`contact`.
.. important::
Do **not** report security issues using the issue tracker. Instead, send an email to security@develmaycare.com with details on the issue you've discovered.
Submitting Feature Requests
---------------------------
Although we reserve the right to decline new features, we welcome all feature requests. See :ref:`contact`.
Testing and Quality Control
---------------------------
Testing involves using Script Tease in real life or in development. Feel free to report any issues you find, or to improve the unit tests.
Pull Requests
-------------
Pull requests are welcome. Such requests should be associated with an issue. We may ignore pull requests that do not have a corresponding issue, so create an issue if one does not already exist.
Blogging
--------
You may help spread awareness of Script Tease by writing blog posts. We are happy to link out to reviews and tutorials from our web site. `Let us know if you've created a blog post`_ that we can share. Be sure to include a link to the post.
You may also provide us with a guest post to be included on our blog.
.. _Let us know if you've created a blog post: https://develmaycare.com/contact/?product=Script%20Tease
.. note::
We reserve the right to proof and approve or decline all content posted on our web site.
Development
===========
Setting Up For Development
--------------------------
1. Clone the repo at https://github.com/develmaycare/python-scripttease
2. Create a virtual environment and install the requirements from ``requirements.pip``
3. See :ref:`how-to`.
Style Guide
-----------
Script Tease follows `PEP8`_ and (where appropriate) the `Django style guide`_ and `JavaScript Standard Style`_.
.. _Django style guide: https://docs.djangoproject.com/en/stable/internals/contributing/writing-code/coding-style/
.. _JavaScript Standard Style: https://standardjs.com
.. _PEP8: https://www.python.org/dev/peps/pep-0008/
We *do* make a few exceptions and provide additional guidance which is documented in our `developer docs`_.
.. _developer docs: https://docs.develmaycare.com/en/developer/
Dependencies
============
.. include:: _includes/project-dependencies.rst
Tests
=====
.. include:: _includes/project-tests.rst
Releasing
=========
Versioning
----------
Script Tease follows a loose form of `semantic versioning`_. The use of semantic versioning makes it clear when deprecation occurs and backward compatibility is removed. Documented incompatibilities may still exist where deprecation is not feasible (technically or financially).
.. _semantic versioning: https://semver.org/
Cadence
-------
New features (and especially new overlays) are planned for release every 3 months. Patch-level changes (to fix bugs or security issues) are always released as needed.
Long-Term Support
-----------------
Some releases may be designated as long-term support (LTS) releases. Such releases will have security and critical bug fixes applied for 6 months.
Deprecation Policy
------------------
Minor releases may deprecate features from a previous minor release. For example, if a feature is deprecated in release 1.1, it will continue to work in the 1.2 release, though warnings may be raised. However, the deprecated feature may be removed in release 1.3 and may not function as previously expected or will raise errors.
Major releases may *always* remove deprecated features.
Patch-level releases *never* remove deprecated features.
Legal
=====
.. code-block:: text
Copyright (c) Pleasant Tents, LLC
All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of Pleasant Tents, LLC nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

@ -1,8 +1,8 @@
.. _code-reference:
.. _developer-reference:
**************
Code Reference
**************
*******************
Developer Reference
*******************
Constants
=========
@ -12,136 +12,99 @@ Constants
:show-inheritance:
:special-members: __init__
Exceptions
==========
.. automodule:: scripttease.exceptions
:members:
:show-inheritance:
:special-members: __init__
Library
=======
Commands
========
Base
----
.. automodule:: scripttease.lib.commands.base
:members:
:show-inheritance:
:special-members: __init__
Centos
------
.. automodule:: scripttease.lib.commands.centos
:members:
:show-inheritance:
:special-members: __init__
Django
------
--------
.. automodule:: scripttease.lib.commands.django
.. automodule:: scripttease.library.commands.base
:members:
:show-inheritance:
:special-members: __init__
Messages
Overlays
--------
.. automodule:: scripttease.lib.commands.messages
:members:
:show-inheritance:
:special-members: __init__
Common
......
MySQL
-----
.. automodule:: scripttease.lib.commands.mysql
.. automodule:: scripttease.library.overlays.common
:members:
:show-inheritance:
:special-members: __init__
PHP
---
Django
......
.. automodule:: scripttease.lib.commands.php
.. automodule:: scripttease.library.overlays.django
:members:
:show-inheritance:
:special-members: __init__
Postgres
--------
........
.. automodule:: scripttease.lib.commands.pgsql
.. automodule:: scripttease.library.overlays.pgsql
:members:
:show-inheritance:
:special-members: __init__
Posix
-----
.....
.. automodule:: scripttease.lib.commands.posix
:members:
:show-inheritance:
:special-members: __init__
Python
------
.. automodule:: scripttease.lib.commands.python
.. automodule:: scripttease.library.overlays.posix
:members:
:show-inheritance:
:special-members: __init__
Ubuntu
------
......
.. automodule:: scripttease.lib.commands.ubuntu
.. automodule:: scripttease.library.overlays.ubuntu
:members:
:show-inheritance:
:special-members: __init__
Contexts
========
Scripts
-------
.. automodule:: scripttease.lib.contexts
.. automodule:: scripttease.library.scripts
:members:
:show-inheritance:
:special-members: __init__
Factories
=========
Factory
=======
.. automodule:: scripttease.lib.factories
.. automodule:: scripttease.factory
:members:
:show-inheritance:
:special-members: __init__
Loaders
Parsers
=======
Base
----
.. automodule:: scripttease.lib.loaders.base
.. automodule:: scripttease.parsers.base
:members:
:show-inheritance:
:special-members: __init__
INI
---
Config (INI)
------------
.. automodule:: scripttease.lib.loaders.ini
.. automodule:: scripttease.parsers.ini
:members:
:show-inheritance:
:special-members: __init__
YAML
----
Utils
-----
.. automodule:: scripttease.lib.loaders.yaml
.. automodule:: scripttease.parsers.utils
:members:
:show-inheritance:
:special-members: __init__

@ -1,29 +0,0 @@
********
Releases
********
Versioning
==========
Script Tease follows a loose form of `semantic versioning`_. The use of semantic versioning makes it clear when deprecation occurs and backward compatibility is removed. Documented incompatibilities may still exist where deprecation is not feasible (technically or financially).
.. _semantic versioning: https://semver.org/
Cadence
=======
New features (and especially new overlays) are planned for release every 3 months. Patch-level changes (to fix bugs or security issues) are always released as needed.
Long-Term Support
=================
Some releases may be designated as long-term support (LTS) releases. Such releases will have security and critical bug fixes applied for 6 months.
Deprecation Policy
==================
Minor releases may deprecate features from a previous minor release. For example, if a feature is deprecated in release 1.1, it will continue to work in the 1.2 release, though warnings may be raised. However, the deprecated feature may be removed in release 1.3 and may not function as previously expected or will raise errors.
Major releases may *always* remove deprecated features.
Patch-level releases *never* remove deprecated features.

@ -0,0 +1,115 @@
.. _topics-configuration:
*************
Configuration
*************
Generating Commands From a File
===============================
The :py:class:`scripttease.parsers.ini.Config` class may instantiate commands by loading a configuration file.
.. note::
Additional formats such as JSON or YAML may be supported in the future.
An example file:
.. code-block:: ini
[install apache]
install: apache2
[create the web site directory]
mkdir: /var/www/domains/example_com/www
recursive: yes
[set permissions on the website directory]
perms: /var/www/domains/example_com/www
group: www-data
mode: 775
owner: www-data
Notes regarding this format:
- This is the standard format for Python's ConfigParser. If you prefer, you may use ``=`` instead of ``:``.
- The first part of each command is the INI section and is used as the default comment.
- The command name *must* be the *first* option in the section.
- The arguments for the command appear as the value of the first option in the section. Arguments are separated by a
space.
- Arguments that should be treated as a single value should be enclosed in double quotes.
- ``yes`` and ``no`` are interpreted as boolean values.
- List values, where required, are separated by commas.
.. _topics-configuration-common-parameters:
Common Parameters
-----------------
All commands support the following common parameters:
- ``comment``: A comment regarding the command.
- ``condition``: A condition for execution. For example, ``! -f /path/to/some/file.txt``
- ``cd``: The path from which a command should be executed.
- ``environments``: A string or list of comma-separated strings indicating the operational environments in which the command runs. This is *not* used by default, but may be used to programmatically filter commands for a specific environment. For example, development versus live.
- ``prefix``: A statement to be added prior to executing the command.
- ``register``: A variable name to which the the success or failure (exit code) of the statement is captured.
- ``shell``: The shell used to run the commands. For example, ``/bin/bash``. This is generally not important, but can be a problem when attempting to execute some commands (such as Django management commands).
- ``stop``: ``True`` indicates no other commands should be executed if the given command fails.
- ``sudo``: ``True`` indicates the command should be automatically prefixed with ``sudo``. If provided as a string, the command is also prefixed with a specific user name.
- ``tags``: A list of tags used to classify the command.
Defining an "Itemized" Command
------------------------------
Certain command definitions may be repeated by defining a list of items.
Example of an "itemized" command:
.. code-block:: ini
[create multiple directories]
mkdir: /var/www/domains/example_com/$item
items: www, www/assets, www/content
recursive: yes
[touch a bunch of files]
touch: /var/www/domains/example_com/www/$item
items: index.html, assets/index.html, content/index.html
.. note::
Command itemization may vary with the command type.
Pre-Parsing Command Files as Templates
======================================
Configuration file may be pre-processed as a Jinja2 template by providing a context dictionary:
.. code-block:: ini
[install apache]
install: apache
[create the website directory]
mkdir: /var/www/domains/{{ domain_tld }}/www
recursive: yes
[set permissions on the website directory]
perms: /var/www/domains/{{ domain_tld }}/www
group: www-data
mode: 775
owner: www-data
Then with a config instance:
.. code-block:: python
context = {
'domain_tld': "example_com",
}
config = Config("commands.ini", context=context)
config.load()
for command in config.get_commands():
print(command.get_statement(cd=True))
print("")

@ -0,0 +1,24 @@
.. _topics-overlays:
********
Overlays
********
An overlay is a collection of functions that provide an interface to command creation. An overlay allows configuration files to specify commands in a generic way. When the file is loaded, an overlay may be specified which Script Tease uses to generate commands that are specific to a given operating system.
There are currently four (5) general and re-usable overlays:
- common
- django
- mysql
- pgsql
- posix
And two (2) overlays that are specific to operating systems:
- centos
- ubuntu
The examples that follow instantiate command instances from an INI file. Each example is shown with the defaults. All commands support a number of :ref:`topics-configuration-common-parameters`.
.. include:: _includes/overlays.rst

@ -0,0 +1,11 @@
.. _topics:
******
Topics
******
.. toctree::
:maxdepth: 2
Configuration <topics-configuration>
Overlays <topics-overlays>

@ -0,0 +1,118 @@
# Django
Summary: Work with Django management commands.
## Common Options for Django Commands
You will generally want to include `cd` to change to the project directory and `prefix` to load the virtual environment.
```yaml
- collect static files:
django: static
cd: /path/to/project/source
prefix: source ../python/bin/activate
```
## Automatic Conversion of Django Command Switches
Options provided in the command configuration file are automatically converted to command line switches.
```yaml
- run database migrations:
django: migrate
settings: tenants.example_com.settings
- dump some data:
django: dumpdata
indent: 4
natural_foreign: yes
natural_primary: yes
```
## Available Commands
### check
```ini
[run django checks]
django: check
```
```yaml
- run django checks:
django: check
```
### dumpdata
Dump fixture data.
- app: Required. The name of the app.
- model: Optional. A model name within the app.
- path: The path to the JSON file. When a model is provided, this defaults to `fixtures/app/model.json`. Otherwise, it is `fixtures/app/initial.json`.
```ini
[dump project data]
django: dumpdata
app: projects
[dump project categories]
django: dumpdata
app: projects
model: Category
path: local/projects/fixtures/default-categories.json
```
### loaddata
Load fixture data.
- path: The path to the JSON file. When a model is provided, this defaults to `fixtures/app/model.json`. Otherwise, it is `fixtures/app/initial.json`.
### migrate
Run database migrations.
```ini
[run database migrations]
django: migrate
```
```yaml
- run database migrations:
django: migrate
```
### static
Collect static files.
```ini
[collect static files]
django: static
```
```yaml
- collect static files:
django: static
```
## Custom or Ad Hoc Commands
It is possible to work with any Django management command provided the parameters may be specified as a switch.
```ini
[run any django command]
django: command_name
first_option_name: asdf
second_option_name: 1234
third_option_name: yes
```
```yaml
- run any django command:
django: command_name
first_option_name: asdf
second_option_name: 1234
third_option_name: yes
```

@ -8,9 +8,9 @@ Summary: Send feedback to users.
Use the dialog CLI to display a message.
- `height` (int): The height of the dialog box. Default: `15`
- `title` (str): An optional title to display as part of the dialog box. Default: `Message`.
- `width` (int): The width of the dialog box. Default: `100`
- `height`: The height of the dialog box. Default: `15`
- `title`: An optional title to display as part of the dialog box. Default: `Message`.
- `width`: The width of the dialog box. Default: `100`
```ini
[send some feedback]
@ -56,7 +56,7 @@ echo: "This is a message."
Send a message via Slack.
- `url` (str): Required. The URL to which the message should be sent.
- `url`: Required. The URL to which the message should be sent.
```ini
[send some feedback]
@ -77,11 +77,6 @@ url: https://subdomain.slack.com/path/to/your/integration
Like `explain` above, a screenshot adds detail to comments or documentation, but does not produce a command statement.
- `caption` (str): An optional caption for the image.
- `css` (str): CSS class(es) to be apply to the image.
- `height` (int | str): The height of the image in pixels or as a percentage.
- `width` (int | str): The width of the image in pixels or as a percentage.
```ini
[login screenshot after successful install]
screenshot: images/login.png
@ -90,14 +85,14 @@ height: 50%
width: 50%
```
The value of `screenshot` may be relative to the command file or a full URL to the image. If `caption` is omitted the comment is used.
The value of `screenshot` may be relative to the command file or a full URL to the image. If `caption` is omitted the section (comment) is used.
### twist
Send a message via [Twist](https://twist.com).
- `title` (str): The title of the message. Default: `Notice`
- `url` (str): Required. The URL to which the message should be sent.
- `title`: The title of the message. Default: `Notice`
- `url`: Required. The URL to which the message should be sent.
```ini
[send some feedback]

@ -30,63 +30,35 @@ Drop a database. Argument is the database name.
Dump the database schema. Argument is the database name.
- `path`: The path to the dump file. Default: `database_name.sql`
- `path`: The path to the dump file. Default: `dump.sql`
```ini
[create a soft backup of the database]
mysql.dump: example_app
path: /tmp/example_app.sql
### mysql.exec
```
Execute an SQL statement. Argument is the SQL statement.
- `database`: The name of the database where the statement will be executed. Default: `default`
### mysql.exists
Determine if a database exists. Argument is the database name.
```ini
[determine if the database exists]
mysql.exists: example_app
```
### mysql.grant
Grant privileges to a user.
Grant privileges to a user. Argument is the privileges to be granted.
- `database`: The database name where privileges are granted.
- `privileges`: The privileges to be granted. Default: `ALL`
- `user`: The user name for which the privileges are provided.
```ini
[grant select privileges to bob]
mysql.grant: bob
privileges: select
### mysql.user.create
```
### mysql.user
Create a user. Argument is the username.
Create a user. Argument is the user name.
- `password`: The user's password.
```ini
[create a database user]
mysql.user: username
```
### mysql.user.drop
Remove a user.
Remove a user. Argument is the user name.
```ini
[create a database user]
mysql.user: username
op: remove
```
Determine if a user exists.
### mysql.user.exists
```ini
[create a database user]
mysql.user: username
op: exists
```
Determine if a user exists. Argument is the user name.

@ -0,0 +1,57 @@
# PostgreSQL
Summary: Work with Postgres databases.
## Common Options
- `admin_pass`: The password off the admin-authorized user.
- `admin_user`: The user name of the admin-authorized user. Default: `postgres`
- `host`: The host name. Default: `localhost`
- `port`: The TCP port. Default: `5432`
## Available Commands
### pgsql.create
Create a database. Argument is the database name.
- `owner`: The user name that owns the database.
```ini
[create the database]
pgsql.create: database_name
```
### pgsql.drop
Drop a database. Argument is the database name.
### pgsql.dump
Dump the database schema. Argument is the database name.
- `path`: The path to the dump file. Default: `dump.sql`
### pgsql.exec
Execute an SQL statement. Argument is the SQL statement.
- `database`: The name of the database where the statement will be executed. Default: `default`
### pgsql.exists
Determine if a database exists. Argument is the database name.
### pgsql.user.create
Create a user. Argument is the user name.
- `password`: The user's password.
### pgsql.user.drop
Remove a user. Argument is the user name.
### pgsql.user.exists
Determine if a user exists. Argument is the user name.

@ -7,9 +7,3 @@ Summary: Work with PHP.
### module
Enable a PHP module. Argument is the module name.
```ini
[enable postgres for PHP]
php.module: pdo_pgsql
```

@ -0,0 +1,148 @@
# POSIX
Summary: Work with common POSIX-compliant commands..
## Available Commands
### append
Append content to a file. Argument is the file name.
- `content`: The content to be appended.
### archive
Create an archive (tarball). Argument is the target file or directory.
- `absolute`: Don't strip leading slashes from file names.
- `view`: View the progress.
- `exclude`: Exclude file name patterns.
- `strip`: Strip component paths to the given depth (integer).
- `to`: The path to where the archive will be created.
### copy
Copy a file or directory. First argument is the target file/directory. Second argument is the destination.
- `overwrite`: Overwrite an existing target.
- `recursive`: Copy directories recursively.
### dir
Create a directory. Argument is the path.
- `group`: Set the group to the given group name.
- `mode`: Set the mode on the path.
- `owner`: Set the owner to the given owner name.
- `recursive`: Create the full path even if intermediate directories do not exist.
### extract
Extract an archive (tarball). Argument is the path to the archive file.
- `absolute`: Strip leading slashes from file names.
- `view`: View the progress.
- `exclude`: Exclude file name patterns.
- `strip`: Strip component paths to the given depth (integer).
- `to`: The path to where the archive will be extracted. Defaults to the current working directory.
### file
Create a file. Argument is the path.
- `content`: The content of the file. Otherwise, an empty file is created.
- `group`: Set the group to the given group name.
- `mode`: Set the mode on the path.
- `owner`: Set the owner to the given owner name.
### link
Create a symlink. First argument is the target. Second argument is the destination.
- `force`: Force creation of the link.
### move
Move a file or directory. First argument is the target. Second argument is the desitnation.
### perms
Set permissions on a file or directory. Argument is the path.
- `group`: Set the group to the given group name.
- `mode`: Set the mode on the path.
- `owner`: Set the owner to the given owner name.
- `recursive`: Apply permission recursively (directories only).
### push
Push (rsync) a path to a remote server. First argument is the local path. Second argument is the remote path.
- `delete`: Delete existing files/directories.
- `host`: The host name. Required.
- `key_file`: Use the given SSL (private) key. Required.
- `links`: Copy symlinks.
- `exclude`: Exclude patterns from the given (local) file.
- `port`: The TCP port on the host. Default: `22`
- `recursive`: Operate recursively on directories.
- `user`: The user name. Required.
### remove
Remove a file or directory. Argument is the path.
- `force`: Force the removal.
- `recursive`: Remove (directories) rescurisvely.
### rename
Rename a file or directory. First argument is the target. Second argument is the destination.
### replace
Replace something in a file. First argument is the path.
- `backup`: Create a backup.
- `delimiiter`: The sed delimiter. Default: `/`
- `find`: The text to be found. Required.
- `sub`: The text to be replaced. Required.
### scopy
Copy a file to a remote server. First argument is the local file name. Second argument is the remote destination.
- `key_file`: The private key file to use for the connection.
- `host`: The host name. Required.
- `port`: The TCP port. Default: `22`
- `user`: The user name. Required.
### ssl
Use Let's Encrypt (certbot) to acquire an SSL certificate. Argument is the domain name.
- `email`: The email address for "agree tos". Default: `webmaster@domain_name`
- `webroot`: The webroot to use. Default: `/var/www/maint/www`
### sync
Sync (rsync) local files and directories. First argument is the target. Second argument is the destination.
- `delete`: Delete existing files/directories.
- `links`: Copy symlinks.
- `exclude`: Exclude patterns from the given (local) file.
- `recursive`: Operate recursively on directories.
### touch
Touch a file, whether it exists or not. Argument is the path.
### wait
Wait for n number of seconds before continuing. Argument is the number of seconds.
### write
Write to a file. Argument is the path.
- `content`: The content to write to the file. Replaces existing content.

@ -0,0 +1,20 @@
# Python
Summary: Work with Python.
## Available Commands
### pip
Use the pip command. Argument is the package name.
- `op`: The operation; `install` (the default), `remove`, or `updgrade`.
- `venv`: The name of the virtual environment to use.
### pip3
Use Python3 pip. See pip above.
### virtualenv
Create a python virtual environment. Argument is the environment name.

@ -1,6 +1,6 @@
# Steps File
# Command File
A steps file contains the metadata about the commands to be generated. INI and YAML formats are supported.
A command file contains the metadata about the commands to be generated. INI and YAML formats are supported.
In an INI file, each section is a command. With YAML, each top-level list item is a command.
@ -39,8 +39,9 @@ restart: postfix
With both INI and YAML files, the formatting rules are:
- The first part of each command is the INI section or YAML item and is used as the comment.
- The command name *must* be the *first* option in an INI section.
- The arguments for the command appear as the value of the first option in the section. Arguments are separated by a space.
- The command name *must* be the *first* option in the section.
- The arguments for the command appear as the value of the first option in the section. Arguments are separated by a
space.
- Arguments that should be treated as a single value should be enclosed in double quotes.
- `yes` and `no` are interpreted as boolean values. `maybe` is interpreted as `None`.
- List values, where required, are separated by commas when appearing in INI files, but are a `[standard, list, of, values]` in a YAML file.
@ -69,7 +70,7 @@ cd: /path/to/project
### comment
The comment comes from the section name (INI) or list name (YAML). It is included by default when the statement is generated, but may be suppressed using `include_comment=False`.
The comment comes from the section name (INI) or list name (YAML). It is included by default when the statement is generated, by may be suppressed using `include_comment=False`.
```ini
[this becomes the comment]
@ -93,22 +94,20 @@ The `env` option indicates the target environment (or environments) in which the
This option may be given as `environments`, `environs`, `envs`, or simply `env`. It may be a list or CSV string.
The attribute name is always normalized to `environments`.
### prefix
The `prefix` option is used to define a statement to be executed before the main statement is executed.
```ini
[migrate the database]
django.migrate:
django: migrate
cd: /path/to/project/source
prefix: source ../python/bin/activate
```
### register
`register` defines the name of a variable to which the result of the statement should be saved. For some commands, it may be included by default when the statement is generated, but may be suppressed using `include_register=False`.
`register` defines the name of a variable to which the result of the statement should be saved. It is included by default when the statement is generated, but may be suppressed using `include_register=False`.
```yaml
- check apache configuration:
@ -118,11 +117,11 @@ prefix: source ../python/bin/activate
### shell
The `shell` defines the shell to be used for command execution. It is not used for statement generation, but may be used programmatically -- for example, with Python's subprocess module. Some commands (such as Django management commands) may need a shell to be explicitly defined.
The `shell` defines the shell to be used for command execution. It is not used for statement generation, but may be used programmatically -- for example, with Python's subprocess module. Some commands (such as Django management commands) need a shell to be explicitly defined.
```ini
[run django checks]
django.check:
django: check
cd: /path/to/project/source
prefix: source ../python/bin/activate
shell: /bin/bash
@ -134,7 +133,7 @@ shell: /bin/bash
### stop
A `yes` indicates processing should stop if the statement fails to execute with success. If provided, it is included by default when the statement is generated, but may be suppressed. Additionally, when [register](#register) is defined, this option will use the result of the command to determine success. This option is also useful for programmatic execution.
A `yes` indicates processing should stop if the statement fails to execute with success. It is included by default when the statement is generated, but may be suppressed. Additionally, when [register](#register) is defined, this option will use the result of the command to determine success. This option is also useful for programmatic execution.
```yaml
- check apache configuration:
@ -145,7 +144,7 @@ A `yes` indicates processing should stop if the statement fails to execute with
!!! warning
Some commands do not provide a zero or non-zero exit code on success or failure. You should verify that the `stop` will actually be used.
Some commands do not provide an zero or non-zero exit code on success or failure. You should verify that the `stop` will actually be used.
### sudo
@ -187,17 +186,7 @@ sudo: yes
tags: [django, python]
```
With YAML, tags may also be specified like so:
```yaml
- install apache:
install: apache2
tags:
- apache
- web
```
## Ad Hoc Parameters
## Ad Hoc Options
Options that are not recognized as common or as part of those specific to a command are still processed by the loader. This makes it possible to define your own options based on the needs of a given implementation.
@ -216,34 +205,4 @@ remote: yes
; and so on ...
```
This will be of no use as a generated script since the generator does not know about `local` and `remote`, but these could (for example) be used programmatically to control whether Python subprocess or an SSH client is invoked.
## Template Processing
Commands often need to be specific to an environment or installation. To facilitate this, the configuration file may be parsed as a Jinja2 template prior to processing and generating the statements. This happens automatically when variables are provided.
Template context [may be provided on the command line](../cli.md#context-variables-may-be-provided-on-the-command-line), using a [variables file](variables.md), or both.
In the configuration file:
```ini
; commands.ini
[create the document root of the website]
dir: /var/www/{{ domain_tld }}/www
owner: {{ deploy_user }}
group: {{ webserver_user }}
recursive: yes
```
In the variables file:
```ini
[deploy_user]
value: deploy
[domain_tld]
value: example_com
[webserver_user]
value: www-data
```
This will be of no use as a generated script since the generator does not know about `local` and `remote`, but these could be used programmatically to control whether Python subprocess or an SSH client is invoked.

@ -1,8 +1,8 @@
# Variables File
A variables file contains variable definitions that may be used as the context for parsing a [steps file](steps-file.md) *before* the actual commands are generated.
A variables file contains variable definitions that may be used as the context for parsing a [command file](command-file.md) *before* the actual commands are generated.
Unlike a steps file, the INI format is currently the only supported format for a variables file.
Unlike a command file, the INI format is the only supported format for a variables file.
## The Variable Name
@ -35,6 +35,7 @@ You may define an environment for any given variable that may be used for filter
```ini
[database_host:development]
comment: Local host used in development.
value: localhost
[database_host:live]
@ -47,16 +48,6 @@ In this way, variables of the same name may be supported across different deploy
As demonstrated in the example above, you may comment on a variable by adding a `comment:` attribute to the section.
```ini
[database_host:testing]
comment: Local host used when testing.
value: localhost
[database_host:live]
comment: Separate server used when live.
value: db1.example.com
```
## Defining Tags
Tags may be defined for any variable as a comma separated list. This is useful for filtering.
@ -77,7 +68,7 @@ tags: application
## Other Attributes
Any other variable defined in the section is dynamically available. These are not used by Script Tease, but implementers may find this feature useful.
Any other variable defined in the section is dynamically available.
```ini
[domain_name]

@ -0,0 +1,58 @@
# Python Script Tease
## Overview
Script Tease is a library and command line tool for generating Bash commands programmatically and (especially) using configuration files.
The primary focus (and limit) is to convert plain text instructions (in INI or YAML format) into valid command line statements for a given platform. It does *not* provide support for executing those statements.
## Concepts
### Command Generation
Script Tease may be used in two (2) ways:
1. Using the library to programmatically define commands and export them as command line statements.
2. Using the `tease` command to generate commands from a configuration file. See [command file configuration](config/command-file.md).
This documentation focuses on the second method, but the developer docs may be used in your own implementation.
### Self-Documenting
The format of INI and YAML files is self-documenting. The command comment is this section (INI) or start of a list item (YAML). This ensures that all commands have a basic description of their purpose or intent.
### Snippets
An *snippet* is simply a tokenized command that may be customized based on the instructions found in a command file. Related snippets are collected into groups and then merged into a larger set that define the capabilities of a specific operating system.
!!! note
At present, the only fully defined operating systems are for Cent OS and Ubuntu.
Snippets are defined in Python dictionaries. These include a "canonical" command name as the key and either a string or list which define the command. In both cases, the contents are parsed as Jinja templates. There are various approaches to evaluating a snippet.
First: The snippet is a simple mapping of command name and command snippet. This is easy. Find the command name in the dictionary, and we have the snippet to be used. For example the `append` command in the `posix` dictionary.
Second: The snippet is a mapping of command name and a list of snippets to be combined. Find the command name in the dictionary, and iterate through the snippets. For example, many of the commands in the `posix` dictionary takes this form. Command identification is the same as the first condition.
Third: The command is a mapping to informal sub-commands. Examples include `apache` and `system` in the `ubuntu` dictionary. There are a couple of ways to handle this in the config file:
- Use the outer command as the command with the inner command as the first (and perhaps only) argument. For example `apache: reload` or `system: upgrade`.
- Use a "dotted path" to find the command. For example: `apache.reload: (implicity True)` or `system.upgrade: (implicitly True)`. Or `apache.enable_site: example.com`.
The first approach complicates things when detecting actual sub-commands (below). Script Tease supports both of these approaches.
Fourth: The command also expects a sub-command. In some cases, the sub-command may be implicit, like `pip install`. In other cases, a number of sub-commands may be pre-defined, but ad hoc sub-commands should also be supported as with Django commands.
Fifth: Builds upon the third and fourth conditions where the commands have lots of options, some of which may be defined at runtime. Postgres and MySQL may use be presented as informal sub-commands, but there are lots of options and challenges in building the final command. Django management commands have a number of standard options, specific options, and must also support ad hoc commands.
## Terms and Definitions
command
: When used in Script Tease documentation, this is a command instance which contains the properties and parameters for a command line statement.
statement
: A specific statement (string) to be executed. A *statement* is contained within a *command*.
## License
Python Script Tease is released under the BSD 3 clause license.

@ -54,6 +54,8 @@ Run any shell command.
run: "ls -ls"
```
Note that commands with arguments will need to be in quotes.
### start
Start a service:
@ -74,23 +76,11 @@ stop: postgresql
### system
Work with the system.
- `reboot`
- `update`
- `upgrade`
```ini
[update package info]
update:
With with the system.
[upgrade the system]
upgrade:
[reboot the system]
reboot:
```
- `system.reboot`
- `system.update`
- `system.upgrade`
### uninstall
@ -121,17 +111,14 @@ Create a user:
```ini
[create the deploy user]
user: deploy
groups: sudo, www-data
user.add: deploy
groups: www-data
home: /var/www
sudo: yes
```
Remove a user:
```ini
[remove bob]
user: bob
op: remove
sudo: yes
user.remove: bob
```

@ -54,6 +54,8 @@ Run any shell command.
run: "ls -ls"
```
Note that commands with arguments will need to be in quotes.
### start
Start a service:
@ -74,23 +76,11 @@ stop: postgresql
### system
Work with the system.
- `reboot`
- `update`
- `upgrade`
```ini
[update package info]
update:
With with the system.
[upgrade the system]
upgrade:
[reboot the system]
reboot:
```
- `system.reboot`
- `system.update`
- `system.upgrade`
### uninstall
@ -114,24 +104,21 @@ upgrade: libxyz-dev
Create a user:
- `groups` (str): A comma separated list of groups to which the user should be added.
- `home` (str): The user's home directory.
- `login` (str): The shell to assign.
- `system` (bool): Create as a system user.
- `groups`: A comma separated list of groups to which the user should be added.
- `home`: The user's home directory.
- `login`: The shell to use.
- `system`: Create as a system user.
```ini
[create the deploy user]
user: deploy
groups: sudo, www-data
user.add: deploy
groups: www-data
home: /var/www
sudo: yes
```
Remove a user:
```ini
[remove bob]
user: bob
op: remove
sudo: yes
user.remove: bob
```

@ -1,171 +0,0 @@
---
title: CLI
---
The `tease` command may be used to parse a steps file, providing additional utilities for working with commands.
## Getting Help
Use `tease -h` to get started. There are three (3) sub-commands: [docs](#the-docs-sub-command), [inventory](#the-inventory-sub-command), and [script](#the-script-sub-command).
```text
usage: tease [-h] [-v] [--version] docs, inventory, script ...
positional arguments:
docs, inventory, script
Commands
docs Output documentation instead of code.
inventory (inv) Copy an inventory item to a local directory.
script Output the commands.
optional arguments:
-h, --help show this help message and exit
-v Show version number and exit.
--version Show verbose version information and exit.
NOTES
This command is used to parse configuration files and output the commands.
```
## The Docs Sub-Command
A nice benefit of using configuration for commands is that the information may be used to output documentation. This allows the automatic creation of an install guide or tutorial using exactly the same commands that would be used for the actual work.
Additionally, Script Tease provides the [explain](commands/messages.md#explain) and [screenshot](commands/messages.md#screenshot) commands that help provide extra content for documentary output.
```text
usage: tease docs [-h] [-o= {html,md,plain,rst}] [-C= VARIABLES] [-i= STEPS_FILE] [-O= OPTIONS] [-P= {centos,ubuntu}]
[-T= TEMPLATE_LOCATIONS] [-w= OUTPUT_FILE] [-V= VARIABLES_FILE] [-D] [-p]
optional arguments:
-h, --help show this help message and exit
-o= {html,md,plain,rst}, --output-format= {html,md,plain,rst}
The output format; HTML, Markdown, plain text, or ReStructuredText.
-C= VARIABLES, --context= VARIABLES
Context variables for use in pre-parsing the config and templates. In the form of: name:value
-i= STEPS_FILE, --input-file= STEPS_FILE
The path to the configuration file.
-O= OPTIONS, --option= OPTIONS
Common command options in the form of: name:value
-P= {centos,ubuntu}, --profile= {centos,ubuntu}
The OS profile to use.
-T= TEMPLATE_LOCATIONS, --template-path= TEMPLATE_LOCATIONS
The location of template files that may be used with the template command.
-w= OUTPUT_FILE, --write= OUTPUT_FILE
Write the output to disk.
-V= VARIABLES_FILE, --variables-file= VARIABLES_FILE
Load variables from a file.
-D, --debug Enable debug mode. Produces extra output.
-p Preview mode.
```
## The Inventory Sub-Command
Script Tease ships with few pre-defined configurations that may be copied to a local directory.
Use `tease inv ?` to list available inventory items.
```text
usage: tease inventory [-h] [-P= TO_PATH] [-D] [-p] name
positional arguments:
name The name of the inventory item. Use ? to list available items.
optional arguments:
-h, --help show this help message and exit
-P= TO_PATH, --path= TO_PATH
The path to where the item should be copied. Defaults to the current working directory.
-D, --debug Enable debug mode. Produces extra output.
-p Preview mode.
```
!!! note "Road Map"
A future release will include support for multiple inventory locations that may be defined by the user.
## The Script Sub-Command
The `script` sub-command exports command configuration to actual Bash statements. Minimum usage is
```bash
tease -i steps.ini
```
This will output the statements represented in the specified configuration file. There are quite a few other parameters.
```text
usage: tease script [-h] [-c] [-s] [-C= VARIABLES] [-i= STEPS_FILE] [-O= OPTIONS] [-P= {centos,ubuntu}] [-T= TEMPLATE_LOCATIONS]
[-w= OUTPUT_FILE] [-V= VARIABLES_FILE] [-D] [-p]
optional arguments:
-h, --help show this help message and exit
-c, --color Enable code highlighting for terminal output.
-s, --shebang Add the shebang to the beginning of the output.
-C= VARIABLES, --context= VARIABLES
Context variables for use in pre-parsing the config and templates. In the form of: name:value
-i= STEPS_FILE, --input-file= STEPS_FILE
The path to the configuration file.
-O= OPTIONS, --option= OPTIONS
Common command options in the form of: name:value
-P= {centos,ubuntu}, --profile= {centos,ubuntu}
The OS profile to use.
-T= TEMPLATE_LOCATIONS, --template-path= TEMPLATE_LOCATIONS
The location of template files that may be used with the template command.
-w= OUTPUT_FILE, --write= OUTPUT_FILE
Write the output to disk.
-V= VARIABLES_FILE, --variables-file= VARIABLES_FILE
Load variables from a file.
-D, --debug Enable debug mode. Produces extra output.
-p Preview mode.
```
### Context Variables May be Provided on the Command Line
To supply context variables on the command line:
```bash
tease -C domain_name:example.com -C domain_tld:example_com
```
!!! important
Variables provided on the command will always override those provided in a file.
### Loading Context Variables from a File
Context variables may be loaded from a file:
```ini
[domain_name]
value = example.com
[domain_tld]
value = example_com
```
The variables above are available as template variables in a steps file.
For example, ``{{ domain_name }}`` becomes ``example.com``.
To load the variables file, use the `-V` switch:
```bash
tease -i steps.ini -V variables.ini
```
### Setting Common Options for All Commands
Rather than include a common parameter in the configuration file, it is possible to specify a common option on the command line.
```bash
tease -O sudo:yes
```
### The Difference Between Variables and Options
Variables are used to pre-process configuration files as templates, while common options are passed to *all* command instances.

@ -1,21 +0,0 @@
# Getting Started
## System Requirements
Python 3.6 or greater is required.
## Install
To install:
```bash
pip install python-scripttease;
```
## Configuration
See [Steps File](topics/steps-file.md) for creating a configuration file.
## FAQs
**Does Script Tease execute commands?** No. Script Tease generates statements that may be [saved to a file for examination and execution](how-to/create-executable-script.md). Or a [custom implementation](how-to/use-with-commonkit.md) may be created to execute the generated statements.

@ -1,14 +0,0 @@
# Create an Executable Script
To create a script that is ready to execute:
```bash
tease -i commands.ini -s -w script.sh
chmod +x script.sh
```
The `-s` switch causes the [shebang](https://linuxhandbook.com/shebang/) to be included at the top of the output. This defaults to:
`#! /usr/bin/env bash`
The script is now ready to run: `./script.sh`

@ -1,51 +0,0 @@
# Define A Custom Command
!!! note
It is not currently possible to define a custom command which may be used with the `tease` command.
## 1) Create A Function
Create a function that does what you want:
```python
# mycommands.py
from scripttease.lib.commands.base import Command
def do_something_impressive(arg1, **kwargs):
return Command("ls -ls %s" % arg1, **kwargs)
```
!!! important
kwargs are *always* required.
## 2) Create A Mapping
```python
# mycommands.py
MAPPINGS = {
'impressive': do_something_impressive,
}
```
`impressive` is now mapped to the function which creates the command.
## 3) Supply The Mapping to the Command Factory
```python
from scripttease.lib.factories import command_factory
from scripttease.lib.loaders import INILoader
from .mycommands import MAPPINGS
ini = INILoader("path/to/commands.ini")
ini.load()
commands = command_factory(ini, mappings=MAPPINGS)
```
## 4) Include the Custom Command
```ini
[this is my custom command]
impressive: testing
```

@ -1,41 +0,0 @@
# Post a Message to Slack
The `slack` command may be used to send a message to a Slack channel. This uses the Incoming Webhooks feature, which requires some additional setup.
!!! note
The following steps were accurate as of September 2020.
## 1) Log in to Slack
Log in to Slack and go to [Your Apps](https://api.slack.com/apps).
## 2) Create New Slack App
Create a new Slack app.
## 3) Select Incoming Webhooks
On the next page, select Incoming Webhooks and then toggle activation.
![incoming webhooks](images/slack-1.jpg)
## 4) Select Channel
Next, click "Add new Webhook to Workspace" and select the channel to which the message will be posted.
![slack 2](images/slack-2.jpg)
![slack 3](images/slack-3.jpg)
## 5) Copy the URL
Copy the URL for the new webhook to use as the ``url`` parameter for the Slack command.
```ini
[send a message to slack]
slack: "This is a test message."
url: the URL you created goes here
```
!!! tip
Define this URL in a `variables.ini` file if you need to send multiple messages to the same channel.

@ -1,35 +0,0 @@
# Post a Message to Twist
The `twist` command may be used to send a message to Twist, which requires some additional setup.
!!! note
The following steps were accurate as of September 2020.
## 1) Log In To Twist
Log in to Twist and from the profile menu go to Add Integrations. Then click on Build and "Add a new integration".
## 2) Provide Requested Information
Provide the requested info.
![twist 1](images/twist-1.png)
## 3) Install Integration
After submitting this info, go to Installation. Select a channel and who to notify. Then click "Install integration".
![twist 2](images/twist-2.png)
## 4) Copy the URL
Copy the "Post content manually" URL for use in your configuration file.
```ini
[post a message to twist]
twist: "This is a test message."
url: the URL you created goes here
```
!!! tip
Define this URL in a `variables.ini` file if you need to send multiple messages to the same channel.

@ -1,56 +0,0 @@
# Use Script Tease With Common Kit
Since the focus of Script Tease is to convert plain text instructions into valid command line statements, it does *not* provide support for executing those statements either locally or remotely. However, the shell component of [python-commonkit](https://docs.develmaycare.com/en/python-commonkit/stable/components/#module-commonkit.shell) *does* provide support for executing commands in local POSIX environments.
Here is an example of how to use these packages together:
```python
from commonkit.shell import Command
from scripttease.exceptions import InvalidInput
from scripttease.lib.factories import command_factory
from scripttease.lib.loaders import INILoader
def execute(step):
command = Command(
step.statement,
comment=step.comment,
path=step.cd,
prefix=step.prefix,
shell=step.shell
)
# Sudo is a different class, but identical in behavior.
command.sudo = step.sudo
if command.run():
print("[success] %s" % step.comment)
else:
print("[failure] %s" % step.comment)
if step.stop:
print("I can't go on: %s" % command.error)
exit(command.code)
ini = INILoader("path/to/steps.ini")
ini.load()
try:
steps = command_factory(ini)
except InvalidInput as e:
print("%s: I can't go on." % e)
exit(1)
# A failure to load results in None.
if steps is None:
print("Failed to load steps. Bummer.")
exit(1)
# Iterate through each step to create a Common Kit command instance.
for step in steps:
# To preview ...
# print(step.get_statement(cd=True))
execute(step)
```
Common Kit is already a dependency of Script Tease, so it is installed by default. The ``execute()`` function provides the interface between Script Tease command instances and Common Kit command instances.

@ -1,66 +0,0 @@
# Python Script Tease
## Overview
Script Tease is a library and command line tool for generating Bash commands programmatically and (especially) using configuration files.
The primary focus (and limit) is to convert plain text instructions (in INI or YAML format) into valid command line statements for a given platform. It does *not* provide support for executing those statements.
!!! warning
YAML support is currently untested.
## Concepts
### Command Generation
Script Tease may be used in three (3) ways:
1. Using the library to programmatically define commands and export them as command line statements.
2. Using the `tease` command to generate commands from a configuration file. See [steps file](topics/steps-file.md).
3. Using the command file format and library to create a custom implementation.
This documentation focuses on the second method, but the [developer docs](./reference) may be used to guide custom implementations for 1 and 3 above.
### Self-Documenting
The format of INI and YAML files is self-documenting. The command comment is this section (INI) or start of a list item (YAML). This ensures that all commands have a basic description of their purpose or intent.
```ini
[install apache]
install: apache2
```
```yaml
- install apache
install: apache2
```
### Representing Commands
All commands are represented by simple, Python functions. These functions are responsible for accepting the arguments provided (usually by a command loader) and converting them into a common `Command` instance. This instance is then capable of generating a finished statement that may be used on the command line.
#### Profiles
Profiles contain command functions that are specific to an operating system. Not all operating systems support the same commands.
!!! note
At present, the only defined operating system profiles are for Cent OS and Ubuntu.
Profiles import and appropriate all other available commands.
## Terms and Definitions
command
: When used in Script Tease documentation, this is a command instance which contains the properties and parameters for assembling a command line statement.
profile
: A collection of commands that work for a specific operating system profile.
statement
: A specific statement (string) to be executed. A *statement* is generated from a *command*.
## License
Python Script Tease is released under the BSD 3 clause license.

@ -1,14 +0,0 @@
# Itemized Commands
It is sometimes useful to create a single command entry that does the same thing with different input. Script Tease facilitates this with the "itemized" command.
```ini
[install common utilities]
install: $item
items: curl, git, lftp, wget
```
In the example above, statements will be generated to install each of the packages named in `items`.
!!! note
Content commands (`explain` and `screenshot`) and Template commands cannot be itemized.

@ -1,43 +0,0 @@
# Templates
Script Tease supports processing of template files using Jinja2.
## Location of Templates
By default, the location of template files are reckoned as relative to the steps file.
```text
package_name/
|-- steps.ini
|-- templates
| `-- httpd.conf
```
Upon loading the `steps.ini` file, any reference to a template file is assumed to be in the `templates/` directory in the same location.
```ini
[create the apache config]
template: httpd.conf /etc/apache2/sites-available/example.app.conf
```
!!! tip
The `-T` switch of the `tease` command may be used to add template locations.
## Template Context
The context variables available to parse the template are:
- Variables received on the command line are included in the context.
- Variables loaded from a file.
- Options passed to the loader from the command line are also included.
Additionally, any unrecognized parameters specified in the command are included in the context. For example:
```ini
[create the httpd.conf file]
template: httpd.conf /etc/apache/sites-available/example.com.conf
ssl_enabled: yes
```
`ssl_enabled` is not a normal parameter for the template command, so it is included in the context.

@ -1,132 +0,0 @@
# Django
Summary: Work with Django management commands.
## Common Options for Django Commands
You will want to include `cd` to change to the project directory (where `manage.py` lives) and supply `venv` to load the virtual environment.
```ini
[collect static files]
django.static:
cd: /path/to/project/source
venv: ../python
```
## Automatic Conversion of Django Command Switches
Options provided in the steps file are automatically converted to command line switches.
```ini
[run database migrations]
django.migrate:
settings: tenants.example_com.settings
[dump some data]
django.dump: projects.Category
indent: 4
natural_foreign: yes
natural_primary: yes
```
`settings` becomes "--settings=tenants.example_com.settings". `indent` becomes "--indent=4". `natural_foreign` and `natural_primary` become "--natural-foreign" and "--natural-primary" respectively.
## Available Commands
### check
```ini
[run django checks]
django.check:
stop: yes
```
### collectstatic
Alias: static
Collect static files.
```ini
[collect static files]
django.static:
```
### createsuperuser
Create a superuser account.
```ini
[create the root user account]
django.createsuperuser: root
email: root@example.com
```
### dumpdata
Alias: dump
Dump fixture data.
- `target` (str): Required. The name of the app or `app.Model`.
- `format` (str): `json` (default) or `xml`.
- `path` (str): The path to the output file. When a model is provided, this defaults to `fixtures/app/model.json`. Otherwise, it is `fixtures/app/initial.json`.
```ini
[dump project data]
django.dump: projects
[dump project categories]
django.dump: projects.Category
path: local/projects/fixtures/default-categories.json
```
### loaddata
Alias: load
Load fixture data.
- `target` (str): Required. The name of the app or `app.Model`.
- `format` (str): `json` (default) or `xml`
- `path` (str): The path to the JSON file. When a model is provided, this defaults to `fixtures/app/model.json`. Otherwise, it is `fixtures/app/initial.json`.
```ini
[load project categories]
django.load: projects
path: local/projects/fixtures/default-categories.json
```
### migrate
Run database migrations.
```ini
[run database migrations]
django.migrate:
stop: yes
```
## Custom or Ad Hoc Commands
It is possible to work with any Django management command provided the parameters may be specified as a switch.
```ini
[run any django command]
django: command_name
option_one: asdf
option_two: 1234
option_three: yes
```
This will generate a statement like:
```bash
./manage.py command_name --option-one="asdf" --option-two=1234 --option-three
```

@ -1,119 +0,0 @@
# PostgreSQL
Summary: Work with Postgres databases.
## Common Options
- `host` (str): The host name. Default: `localhost`
- `password` (str): The password of the user executing the command.
- `port` (int): The TCP port. Default: `5432`
- `user` (str): The username of the user executing the command. Default: `postgres`
## Automatic Conversion of Postgres Command Switches
Options provided in the steps file are automatically converted to command line switches. For example:
```ini
[create a soft backup of the database schema]
pgsql.dump: example_app
schema_only: yes
path: /tmp/example_app.sql
```
`schema_only` becomes "--schema-only".
## Available Commands
### pgsql.create
Create a database. Argument is the database name.
- `owner` (str): The username that owns the database.
```ini
[create the database]
pgsql.create: database_name
```
### pgsql.drop
Drop a database. Argument is the database name.
```ini
[drop the testing database]
pgsql.drop: testing_example_app
```
### pgsql.dump
Dump the database schema. Argument is the database name.
- `path` (str): The path to the dump file. Default: `database_name.sql`
```ini
[create a soft backup of the database]
pgsql.dump: example_app
column_inserts: yes
path: /tmp/example_app.sql
```
### pgsql.exists
Determine if a database exists. Argument is the database name.
```ini
[determine if the database exists]
pgsql.exists: example_app
```
### pgsql.grant
Grant privileges to a user. Argument is the username. Database option is required.
- `database` (str): The name of the database where the target object exists.
- `privileges` (str): The privileges to be granted. Default `ALL` (see [Postgres docs](https://www.postgresql.org/docs/current/sql-grant.html))
- `schema` (str): The schema name to which the privileges apply.
- `table` (str): The table name to which privileges apply.
!!! note
A schema name or table name is required.
```ini
[grant select access to bob]
pgsql.grant: bob
database: example_app
privileges: select
schema: public
```
### pgsql.user
Create a user. Argument is the user name.
- `password` (str): The user's password.
```ini
[create a database user]
pgsql.user: username
```
Remove a user.
```ini
[remove a database user]
pgsql.user: username
op: remove
```
Determine if a user exists.
```ini
[determine if database user exists]
pgsql.user: username
op: exists
```

@ -1,264 +0,0 @@
# POSIX
Summary: Work with common POSIX-compliant commands..
## Available Commands
### append
Append content to a file. Argument is the file name.
- `content` (str): The content to be appended.
```ini
[add to the log file]
append: /path/to/file.log
content: This is a test.
```
### archive
Create an archive (tarball). Argument is the target file or directory.
- `absolute` (bool): Don't strip leading slashes from file names. Default `False`
- `exclude` (str): Exclude file name patterns.
- `file_name` (str): The name of the archive file. Default `archive.tgz`
- `strip` (int): Strip component paths to the given depth.
- `to_path` (str): The path to where the archive will be created. Default `.`
- `view` (bool): View the progress. Default `False`
```ini
[create an archive of the site]
archive: /path/to/file_or_directory
file_name: testing.tgz
to: /tmp
```
### certbot
Alias: ssl
Use Let's Encrypt (certbot) to acquire an SSL certificate. Argument is the domain name.
- `email`: The email address for "agree tos". Default: `webmaster@domain_name`
- `webroot`: The webroot to use. Default: `/var/www/maint/www`
```ini
[get an SSL cert]
ssl: example.app
email: webmaster@example.app
```
### copy
Copy a file or directory. First argument is the target file/directory. Second argument is the destination.
- `overwrite` (bool): Overwrite an existing target.
- `recursive` (bool): Copy directories recursively.
```ini
[copy a directory]
copy: /path/to/directory /path/to/new_directory
overwrite: yes
recursive: yes
```
### dir
Create a directory. Argument is the path.
- `group` (str): Set the group to the given group name.
- `mode` (str): Set the mode on the path.
- `owner` (str): Set the owner to the given owner name.
- `recursive` (str): Create the full path even if intermediate directories do not exist.
```ini
[create a directory]
dir: /path/to/directory
group: www-data
mode: 755
owner: deploy
recursive: yes
```
### extract
Extract an archive (tarball). Argument is the path to the archive file.
- `absolute` (bool): Don't strip leading slashes from file names. Default `False`
- `exclude` (str): Exclude file name patterns.
- `strip` (int): Strip component paths to the given depth.
- `to_path` (str): The path to where the archive will be created. Default `./`
- `view` (bool): View the progress. Default `False`
```ini
[extract an archive]
extract: /path/to/archive.tgz
```
### link
Create a symlink. First argument is the source.
- `force` (bool): Force creation of the link.
- `target` (str): The location of the link. Defaults to the current directory and the base name of the source.
```ini
[create a symlink]
link: /path/to/project/releases/1.0
cd: /path/to/project
force: yes
target: current
```
### move
Move a file or directory. First argument is the target. Second argument is the desitnation.
```ini
[move a file]
move: /path/to/file.txt /new/path/to/file.txt
```
### perms
Set permissions on a file or directory. Argument is the path.
- `group` (str): Set the group to the given group name.
- `mode` (str): Set the mode on the path.
- `owner` (str): Set the owner to the given owner name.
- `recursive` (bool): Apply permission recursively (directories only).
```ini
[set permissions on the shared directory]
perms: /path/to/project/shared
group: www-data
mode: 775
owner: deploy
recursive: yes
```
### push
Alias: rsync
Push (rsync) a path to a remote server. First argument is the local path. Second argument is the remote path.
- `delete` (bool): Delete existing files/directories. Default `False`
- `host` (str): The host name. Required.
- `key_file` (str): Use the given SSL (private) key.
- `links` (bool): Copy symlinks. Default `True
- `exclude` (str): Exclude patterns from the given (local) file.
- `port` (int): The TCP port on the host. Default: `22`
- `recursive` (bool): Operate recursively on directories.
- `user` (str): The username.
```ini
[push the project to the server]
push: /path/to/project /path/on/server
key_file: ~/.ssh/example_app
host: example.app
user: deploy
```
### remove
Remove a file or directory. Argument is the path.
- `force` (bool): Force the removal. Default `False`
- `recursive` (bool): Remove all directories in the path. Default `False`
```ini
[remove a directory]
remove: /path/to/directory
force: yes
recusrive: yes
```
### replace
Replace something in a file. First argument is the path.
- `backup`: Backup file extension. Default `.b`
- `delimiiter`: The sed delimiter. Default: `/`
- `find`: The text to be found. Required.
- `sub`: The text to be replaced. Required.
```ini
[replace text in a file]
replace: /path/to/file.txt
find: testing
sub: 123
```
### scopy
Copy a file to a remote server. First argument is the local file name. Second argument is the remote destination.
- `key_file` (str): The private key file to use for the connection.
- `host` (str): The host name. Required.
- `port` (int): The TCP port. Default: `22`
- `user` (str): The username. Required.
```ini
[copy a file to the server]
scopy: /path/to/local.txt path/to/remove.txt
host: example.app
user: deploy
```
### sync
Sync (rsync) local files and directories. First argument is the target. Second argument is the destination.
- `delete` (bool): Delete existing files/directories.
- `links` (bool): Copy symlinks.
- `exclude` (str): Exclude patterns from the given (local) file.
- `recursive` (bool): Operate recursively on directories.
```ini
[syncrhonize files on the local machine]
sync: /path/to/project /path/to/sync/directory
```
### touch
Touch a file, whether it exists or not. Argument is the path.
```ini
[touch a file]
touch: /path/to/file.txt
```
### wait
Wait for n number of seconds before continuing. Argument is the number of seconds.
```ini
[wait just a minute]
wait: 60
```
### write
Write to a file. Argument is the path.
- `content` (str): The content to write to the file. Replaces existing content.
```ini
[replace an existing file]
write: /path/to/file.txt
content: This whole file has been replaced.
```

@ -1,49 +0,0 @@
# Python
Summary: Work with Python.
## Available Commands
### pip
Use the pip command. Argument is the package name.
- `op` (str): The operation; `install` (the default) or `remove`.
- `upgrade` (bool): Upgrade the package.
- `venv` (str): The name of the virtual environment to use.
- `version` (int): The pip version to use. Default `3`
```ini
[install django]
pip: django
cd: /path/to/project
venv: python
```
### pip_file
Alias: pipf
Install packages from a pip file.
- `venv` (str): The name of the virtual environment to use.
- `version` (int): The pip version to use. Default `3`
```ini
[install dependencies]
pip_file: deploy/packages/testing.pip
cd: path/to/project
venv: python
```
### virtualenv
Create a python virtual environment. Argument is the environment name.
```ini
[create the virtual environment]
virtualenv: python
cd: /path/to/project
```

@ -1,52 +0,0 @@
site_name: Script Tease
copyright: Copyright &copy; Pleasant Tents, LLC. All rights reserved.
markdown_extensions:
- toc:
permalink: True
- admonition
- attr_list
- def_list
- pymdownx.superfences
nav:
- Introduction: index.md
- Getting Started: getting-started.md
- Topics:
- Steps File: topics/steps-file.md
- Variables File: topics/variables.md
- Itemized Commands: topics/itemized-commands.md
- Templates: topics/templates.md
- How-To:
- Create an Executable Script: how-to/create-executable-script.md
- Define a Custom Command: how-to/define-custom-command.md
- Post a Message to Slack: how-to/post-message-slack.md
- Post a Message to Twist: how-to/post-message-twist.md
- Use Script Tease with Common Kit: how-to/use-with-commonkit.md
- Usage:
- CentOS: usage/centos.md
- Django: usage/django.md
- Messages: usage/messages.md
- MySQL: usage/mysql.md
- PHP: usage/php.md
- Postgres: usage/pgsql.md
- POSIX: usage/posix.md
- Python: usage/python.md
- Ubuntu: usage/ubuntu.md
- CLI: cli.md
repo_name: Git Traction
repo_url: https://gittraction.com/diff6/python-scripttease
theme:
name: material
palette:
# Palette toggle for light mode
- scheme: default
toggle:
icon: material/brightness-7
name: Switch to dark mode
# Palette toggle for dark mode
- scheme: slate
toggle:
icon: material/brightness-4
name: Switch to light mode

@ -0,0 +1,26 @@
site_name: Script Tease
copyright: Copyright &copy; Pleasant Tents, LLC. All rights reserved.
markdown_extensions:
- toc:
permalink: True
- admonition
- attr_list
- def_list
nav:
- Home: index.md
- Configuration:
- Command File: config/command-file.md
- Variables: config/variables.md
- Commands:
- Django: commands/django.md
- Messages: commands/messages.md
- MySQL: commands/mysql.md
- PHP: commands/php.md
- Postgres: commands/pgsql.md
- POSIX: commands/posix.md
- Python: commands/python.md
- Profiles:
- Ubuntu: profiles/ubuntu.md
# - Developer Reference: /developers/
repo_name: GitLab
repo_url: https://git.sixgunsoftware.com/python-scripttease

@ -1,15 +1,9 @@
[project]
category = developer
description = A collection of classes and commands for automated command line scripting using Python.
icon = fas fa-scroll
description = A collection of classes and commands for automated command line scripting using Pythonn.
title = Python Script Tease
type = cli
[business]
code = PTL
name = Pleasant Tents, LLC
[license]
code = bsd3
name = 3-Clause BSD
url = https://opensource.org/licenses/BSD-3-Clause

@ -0,0 +1,382 @@
#! /usr/bin/env python
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from commonkit import highlight_code, indent, smart_cast, write_file
from commonkit.logging import LoggingHelper
from commonkit.shell import EXIT
from markdown import markdown
import os
import sys
sys.path.insert(0, "../")
from scripttease.constants import LOGGER_NAME
from scripttease.lib.contexts import Context
from scripttease.lib.loaders import load_variables, INILoader, YMLLoader
from scripttease.variables import PATH_TO_SCRIPT_TEASE
from scripttease.version import DATE as VERSION_DATE, VERSION
DEBUG = 10
logging = LoggingHelper(colorize=True, name=LOGGER_NAME)
log = logging.setup()
def execute():
"""Process script configurations."""
__author__ = "Shawn Davis <shawn@develmaycare.com>"
__date__ = VERSION_DATE
__help__ = """NOTES
This command is used to parse configuration files and output the commands.
"""
__version__ = VERSION + "+new"
# Main argument parser from which sub-commands are created.
parser = ArgumentParser(description=__doc__, epilog=__help__, formatter_class=RawDescriptionHelpFormatter)
parser.add_argument(
"path",
default="steps.ini",
nargs="?",
help="The path to the configuration file."
)
parser.add_argument(
"-c",
"--color",
action="store_true",
dest="color_enabled",
help="Enable code highlighting for terminal output."
)
parser.add_argument(
"-C=",
"--context=",
action="append",
dest="variables",
help="Context variables for use in pre-parsing the config and templates. In the form of: name:value"
)
parser.add_argument(
"-d=",
"--docs=",
choices=["html", "markdown", "plain", "rst"],
# default="markdown",
dest="docs",
help="Output documentation instead of code."
)
parser.add_argument(
"-D",
"--debug",
action="store_true",
dest="debug_enabled",
help="Enable debug output."
)
parser.add_argument(
"-f=",
"--filter=",
action="append",
dest="filters",
help="Filter the commands in the form of: attribute:value"
)
parser.add_argument(
"-i=",
"--inventory=",
dest="inventory",
help="Copy an inventory item to a local directory."
)
parser.add_argument(
"-o=",
"--option=",
action="append",
dest="options",
help="Common command options in the form of: name:value"
)
parser.add_argument(
"-P=",
"--profile=",
choices=["centos", "ubuntu"],
default="ubuntu",
dest="profile",
help="The OS profile to use."
)
parser.add_argument(
"-T=",
"--template-path=",
action="append",
dest="template_locations",
help="The location of template files that may be used with the template command."
)
parser.add_argument(
"-w=",
"--write=",
dest="output_file",
help="Write the output to disk."
)
parser.add_argument(
"-V=",
"--variables-file=",
dest="variables_file",
help="Load variables from a file."
)
# Access to the version number requires special consideration, especially
# when using sub parsers. The Python 3.3 behavior is different. See this
# answer: http://stackoverflow.com/questions/8521612/argparse-optional-subparser-for-version
parser.add_argument(
"-v",
action="version",
help="Show version number and exit.",
version=__version__
)
parser.add_argument(
"--version",
action="version",
help="Show verbose version information and exit.",
version="%(prog)s" + " %s %s by %s" % (__version__, __date__, __author__)
)
# Parse arguments.
args = parser.parse_args()
if args.debug_enabled:
log.setLevel(DEBUG)
log.debug("Namespace: %s" % args)
# Create the global context.
context = Context()
if args.variables_file:
variables = load_variables(args.variables_file)
for v in variables:
context.variables[v.name] = v
if args.variables:
for token in args.variables:
try:
key, value = token.split(":")
context.add(key, smart_cast(value))
except ValueError:
context.add(token, True)
# Capture filters.
if args.filters:
filters = dict()
for token in args.filters:
key, value = token.split(":")
if key not in filters:
filters[key] = list()
filters[key].append(value)
# Handle global command options.
options = dict()
if args.options:
for token in args.options:
try:
key, value = token.split(":")
options[key] = smart_cast(value)
except ValueError:
options[token] = True
# The path may have been given as a file name (steps.ini), path, or an inventory name.
input_locations = [
args.path,
os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", args.path, "steps.ini"),
os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", args.path, "steps.yml"),
]
path = None
for location in input_locations:
if os.path.exists(location):
path = location
break
if path is None:
log.warning("Path does not exist: %s" % args.path)
exit(EXIT.INPUT)
# Load the commands.
if path.endswith(".ini"):
loader = INILoader(
path,
context=context,
locations=args.template_locations,
profile=args.profile,
**options
)
elif path.endswith(".yml"):
loader = YMLLoader(
path,
context=context,
locations=args.template_locations,
profile=args.profile,
**options
)
else:
log.error("Unsupported file format: %s" % path)
exit(EXIT.ERROR)
# noinspection PyUnboundLocalVariable
if not loader.load():
log.error("Failed to load the input file: %s" % path)
exit(EXIT.ERROR)
# Validate snippets before continuing.
valid = list()
for snippet in loader.get_snippets():
if snippet.is_valid:
valid.append(True)
else:
log.error("Invalid snippet: %s" % snippet.name)
valid.append(False)
if not all(valid):
exit(EXIT.ERROR)
# Generate output.
if args.docs:
output = list()
for snippet in loader.get_snippets():
# Will this every happen?
# if snippet is None:
# continue
if snippet.name == "explain":
if snippet.header:
if args.docs == "plain":
output.append("***** %s *****" % snippet.name.title())
elif args.docs == "rst":
output.append(snippet.name.title())
output.append("=" * len(snippet.name))
else:
output.append("## %s" % snippet.name.title())
output.append("")
output.append(snippet.content)
output.append("")
elif snippet.name == "screenshot":
if args.docs == "html":
b = list()
b.append('<img src="%s"' % snippet.args[0])
b.append('alt="%s"' % snippet.caption or snippet.comment)
if snippet.classes:
b.append('class="%s"' % snippet.classes)
if snippet.height:
b.append('height="%s"' % snippet.height)
if snippet.width:
b.append('width="%s"' % snippet)
output.append(" ".join(b) + ">")
output.append("")
elif args.docs == "plain":
output.append(snippet.args[0])
output.append("")
elif args.docs == "rst":
output.append(".. figure:: %s" % snippet.args[0])
if snippet.caption:
output.append(indent(":alt: %s" % snippet.caption or snippet.comment))
if snippet.height:
output.append(indent(":height: %s" % snippet.height))
if snippet.width:
output.append(indent(":width: %s" % snippet.width))
output.append("")
else:
output.append("![%s](%s)" % (snippet.caption or snippet.comment, snippet.args[0]))
output.append("")
elif snippet.name == "template":
if args.docs == "plain":
output.append("+++")
output.append(snippet.get_content())
output.append("+++")
elif args.docs == "rst":
output.append(".. code-block:: %s" % snippet.get_target_language())
output.append("")
output.append(indent(snippet.get_content()))
output.append("")
else:
output.append("```%s" % snippet.get_target_language())
output.append(snippet.get_content())
output.append("```")
output.append("")
else:
statement = snippet.get_statement(include_comment=False, include_register=False, include_stop=False)
if statement is not None:
line = snippet.comment.replace("#", "")
output.append("%s:" % line.capitalize())
output.append("")
if args.docs == "plain":
output.append("---")
output.append(statement)
output.append("---")
output.append("")
elif args.docs == "rst":
output.append(".. code-block:: bash")
output.append("")
output.append(indent(statement))
output.append("")
else:
output.append("```bash")
output.append(statement)
output.append("```")
output.append("")
if args.docs == "html":
_output = markdown("\n".join(output), extensions=['fenced_code'])
else:
_output = "\n".join(output)
print(_output)
if args.output_file:
write_file(args.output_file, _output)
else:
commands = list()
for snippet in loader.get_snippets():
# Explanations and screenshots don't produce usable statements but may be added as comments.
if snippet.name in ("explain", "screenshot"):
# commands.append("# %s" % snippet.content)
# commands.append("")
continue
statement = snippet.get_statement()
if statement is not None:
commands.append(statement)
commands.append("")
if args.color_enabled:
print(highlight_code("\n".join(commands), language="bash"))
else:
print("\n".join(commands))
if args.output_file:
write_file(args.output_file, "\n".join(commands))
exit(EXIT.OK)
if __name__ == '__main__':
execute()

@ -5,9 +5,10 @@ import sys
sys.path.insert(0, "../")
from scripttease.cli import main_command
from scripttease.cli import execute
if __name__ == '__main__':
sys.argv[0] = re.sub(r'(-script\.pyw|\.exe)?$', '', sys.argv[0])
sys.exit(main_command())
sys.exit(execute())
# old:
# sys.exit(main_command())

@ -1,15 +1,22 @@
# Imports
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from commonkit.logging import LoggingHelper
from commonkit.shell import EXIT
from ..lib.contexts import Context
from ..lib.loaders import load_variables
from ..variables import LOGGER_NAME
from ..variables import LOGGER_NAME, PATH_TO_SCRIPT_TEASE
from ..version import DATE as VERSION_DATE, VERSION
from . import initialize
from . import subcommands
# New:
from commonkit import highlight_code, indent, smart_cast, write_file
from commonkit.shell import EXIT
from markdown import markdown
import os
from scripttease.lib.contexts import Context
from scripttease.lib.factories import command_factory
from scripttease.lib.loaders import load_variables, INILoader, YMLLoader
DEBUG = 10
logging = LoggingHelper(colorize=True, name=LOGGER_NAME)
@ -18,7 +25,7 @@ log = logging.setup()
# Commands
def main_command():
def execute():
"""Process script configurations."""
__author__ = "Shawn Davis <shawn@develmaycare.com>"
@ -33,14 +40,99 @@ def main_command():
# Main argument parser from which sub-commands are created.
parser = ArgumentParser(description=__doc__, epilog=__help__, formatter_class=RawDescriptionHelpFormatter)
# Initialize sub-commands.
subparsers = parser.add_subparsers(
dest="subcommand",
help="Commands",
metavar="docs, inventory, script"
parser.add_argument(
"path",
default="steps.ini",
nargs="?",
help="The path to the configuration file."
)
parser.add_argument(
"-c",
"--color",
action="store_true",
dest="color_enabled",
help="Enable code highlighting for terminal output."
)
parser.add_argument(
"-C=",
"--context=",
action="append",
dest="variables",
help="Context variables for use in pre-parsing the config and templates. In the form of: name:value"
)
parser.add_argument(
"-d=",
"--docs=",
choices=["html", "markdown", "plain", "rst"],
# default="markdown",
dest="docs",
help="Output documentation instead of code."
)
parser.add_argument(
"-D",
"--debug",
action="store_true",
dest="debug_enabled",
help="Enable debug output."
)
parser.add_argument(
"-f=",
"--filter=",
action="append",
dest="filters",
help="Filter the commands in the form of: attribute:value"
)
parser.add_argument(
"-i=",
"--inventory=",
dest="inventory",
help="Copy an inventory item to a local directory."
)
parser.add_argument(
"-o=",
"--option=",
action="append",
dest="options",
help="Common command options in the form of: name:value"
)
initialize.subcommands(subparsers)
parser.add_argument(
"-P=",
"--profile=",
choices=["centos", "ubuntu"],
default="ubuntu",
dest="profile",
help="The OS profile to use."
)
parser.add_argument(
"-T=",
"--template-path=",
action="append",
dest="template_locations",
help="The location of template files that may be used with the template command."
)
parser.add_argument(
"-w=",
"--write=",
dest="output_file",
help="Write the output to disk."
)
parser.add_argument(
"-V=",
"--variables-file=",
dest="variables_file",
help="Load variables from a file."
)
# Access to the version number requires special consideration, especially
# when using sub parsers. The Python 3.3 behavior is different. See this
@ -67,8 +159,6 @@ def main_command():
log.debug("Namespace: %s" % args)
# Load resources for docs and script output.
if args.subcommand in ["docs", "script"]:
# Create the global context.
context = Context()
@ -78,42 +168,394 @@ def main_command():
context.variables[v.name] = v
if args.variables:
initialize.variables_from_cli(context, args.variables)
for token in args.variables:
try:
key, value = token.split(":")
context.add(key, smart_cast(value))
except ValueError:
context.add(token, True)
# Capture filters.
if args.filters:
filters = dict()
for token in args.filters:
key, value = token.split(":")
if key not in filters:
filters[key] = list()
filters[key].append(value)
# Handle global command options.
options = dict()
if args.options:
options = initialize.options_from_cli(args.options)
for token in args.options:
try:
key, value = token.split(":")
options[key] = smart_cast(value)
except ValueError:
options[token] = True
# The path may have been given as a file name (steps.ini), path, or an inventory name.
input_locations = [
args.path,
os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", args.path, "steps.ini"),
os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", args.path, "steps.yml"),
]
path = None
for location in input_locations:
if os.path.exists(location):
path = location
break
loader = initialize.loader(
args.command_file,
if path is None:
log.warning("Path does not exist: %s" % args.path)
exit(EXIT.INPUT)
# Load the commands.
if path.endswith(".ini"):
loader = INILoader(
path,
context=context,
locations=args.template_locations,
profile=args.profile,
**options
)
elif path.endswith(".yml"):
loader = YMLLoader(
path,
context=context,
options=options,
template_locations=args.template_locations
locations=args.template_locations,
profile=args.profile,
**options
)
if loader is None:
else:
log.error("Unsupported file format: %s" % path)
exit(EXIT.ERROR)
# Handle sub-commands.
if args.subcommand == "docs":
# noinspection PyUnboundLocalVariable
exit_code = subcommands.generate_docs(
loader,
output_file=args.output_file,
output_format=args.output_format
)
elif args.subcommand in ["inv", "inventory"]:
exit_code = subcommands.copy_inventory(args.name, to_path=args.to_path)
elif args.subcommand == "script":
# noinspection PyUnboundLocalVariable
exit_code = subcommands.generate_script(
loader,
if not loader.load():
log.error("Failed to load the input file: %s" % path)
exit(EXIT.ERROR)
# Generate output.
if args.docs:
output = list()
for command in loader.commands:
# Will this every happen?
# if command is None:
# continue
if command.name == "explain":
if command.header:
if args.docs == "plain":
output.append("***** %s *****" % command.name.title())
elif args.docs == "rst":
output.append(command.name.title())
output.append("=" * len(command.name))
else:
output.append("## %s" % command.name.title())
output.append("")
output.append(command.content)
output.append("")
elif command.name == "screenshot":
if args.docs == "html":
b = list()
b.append('<img src="%s"' % command.args[0])
b.append('alt="%s"' % command.caption or command.comment)
if command.classes:
b.append('class="%s"' % command.classes)
if command.height:
b.append('height="%s"' % command.height)
if command.width:
b.append('width="%s"' % command)
output.append(" ".join(b) + ">")
output.append("")
elif args.docs == "plain":
output.append(command.args[0])
output.append("")
elif args.docs == "rst":
output.append(".. figure:: %s" % command.args[0])
if command.caption:
output.append(indent(":alt: %s" % command.caption or command.comment))
if command.height:
output.append(indent(":height: %s" % command.height))
if command.width:
output.append(indent(":width: %s" % command.width))
output.append("")
else:
output.append("![%s](%s)" % (command.caption or command.comment, command.args[0]))
output.append("")
elif command.name == "template":
if args.docs == "plain":
output.append("+++")
output.append(command.get_content())
output.append("+++")
elif args.docs == "rst":
output.append(".. code-block:: %s" % command.get_target_language())
output.append("")
output.append(indent(command.get_content()))
output.append("")
else:
output.append("```%s" % command.get_target_language())
output.append(command.get_content())
output.append("```")
output.append("")
else:
statement = command.get_statement(include_comment=False, include_register=False, include_stop=False)
if statement is not None:
line = command.comment.replace("#", "")
output.append("%s:" % line.capitalize())
output.append("")
if args.docs == "plain":
output.append("---")
output.append(statement)
output.append("---")
output.append("")
elif args.docs == "rst":
output.append(".. code-block:: bash")
output.append("")
output.append(indent(statement))
output.append("")
else:
output.append("```bash")
output.append(statement)
output.append("```")
output.append("")
if args.docs == "html":
_output = markdown("\n".join(output), extensions=['fenced_code'])
else:
_output = "\n".join(output)
print(_output)
if args.output_file:
write_file(args.output_file, _output)
else:
commands = command_factory(loader)
output = list()
for command in commands:
# print("COMMAND", command)
# Explanations and screenshots don't produce usable statements but may be added as comments.
if command.name in ("explain", "screenshot"):
# commands.append("# %s" % command.content)
# commands.append("")
continue
statement = command.get_statement(include_comment=False)
if statement is not None:
output.append(statement)
output.append("")
if args.color_enabled:
print(highlight_code("\n".join(output), language="bash"))
else:
print("\n".join(output))
if args.output_file:
write_file(args.output_file, "\n".join(output))
exit(EXIT.OK)
def main_command():
"""Process script configurations."""
__author__ = "Shawn Davis <shawn@develmaycare.com>"
__date__ = VERSION_DATE
__help__ = """NOTES
This command is used to parse configuration files and output the commands.
"""
__version__ = VERSION
# Main argument parser from which sub-commands are created.
parser = ArgumentParser(description=__doc__, epilog=__help__, formatter_class=RawDescriptionHelpFormatter)
parser.add_argument(
"path",
default="commands.ini",
nargs="?",
help="The path to the configuration file."
)
parser.add_argument(
"-c",
"--color",
action="store_true",
dest="color_enabled",
help="Enable code highlighting for terminal output."
)
parser.add_argument(
"-C=",
"--context=",
action="append",
dest="variables",
help="Context variables for use in pre-parsing the config and templates. In the form of: name:value"
)
parser.add_argument(
"-d",
"--docs",
action="store_true",
dest="docs_enabled",
help="Output documentation instead of code."
)
# parser.add_argument(
# "-d=",
# "--docs=",
# choices=["html", "markdown", "plain", "rst"],
# dest="docs_enabled",
# help="Output documentation instead of code."
# )
parser.add_argument(
"-D",
"--debug",
action="store_true",
dest="debug_enabled",
help="Enable debug output."
)
parser.add_argument(
"-f=",
"--filter=",
action="append",
dest="filters",
help="Filter the commands in the form of: attribute:value"
)
parser.add_argument(
"-O=",
"--option=",
action="append",
dest="options",
help="Common command options in the form of: name:value"
)
# parser.add_argument(
# "-O=",
# "--output=",
# # default=os.path.join("prototype", "output"),
# dest="output_path",
# help="Output to the given directory. Defaults to ./prototype/output/"
# )
parser.add_argument(
"-s",
"--script",
action="store_true",
dest="script_enabled",
help="Output commands as a script."
)
parser.add_argument(
"-T=",
"--template-path=",
action="append",
dest="template_locations",
help="The location of template files that may be used with the template command."
)
parser.add_argument(
"-w=",
"--write=",
dest="output_file",
help="Write the output to disk."
)
parser.add_argument(
"-V=",
"--variables-file=",
dest="variables_file",
help="Load variables from a file."
)
# Access to the version number requires special consideration, especially
# when using sub parsers. The Python 3.3 behavior is different. See this
# answer: http://stackoverflow.com/questions/8521612/argparse-optional-subparser-for-version
parser.add_argument(
"-v",
action="version",
help="Show version number and exit.",
version=__version__
)
parser.add_argument(
"--version",
action="version",
help="Show verbose version information and exit.",
version="%(prog)s" + " %s %s by %s" % (__version__, __date__, __author__)
)
# Parse arguments.
args = parser.parse_args()
if args.debug_enabled:
log.setLevel(DEBUG)
log.debug("Namespace: %s" % args)
# Load context.
context = dict()
if args.variables:
context = initialize.context_from_cli(args.variables)
# Load additional context from file.
if args.variables_file:
variables = initialize.variables_from_file(args.variables_file)
if variables:
context.update(variables)
# Handle filters.
filters = None
if args.filters:
filters = initialize.filters_from_cli(args.filters)
# Handle options.
options = None
if args.options:
options = initialize.options_from_cli(args.options)
# Process the request.
if args.docs_enabled:
exit_code = subcommands.output_docs(
args.path,
context=context,
filters=filters,
locations=args.template_locations,
options=options
)
elif args.script_enabled:
exit_code = subcommands.output_script(
args.path,
color_enabled=args.color_enabled,
include_shebang=args.include_shebang,
output_file=args.output_file
context=context,
locations=args.template_locations,
options=options
)
else:
print("Unrecognized command: %s" % args.subcommand)
exit_code = EXIT.USAGE
exit_code = subcommands.output_commands(
args.path,
color_enabled=args.color_enabled,
context=context,
filters=filters,
locations=args.template_locations,
options=options
)
exit(exit_code)

@ -1,374 +1,126 @@
import logging
# Imports
from commonkit import smart_cast
from commonkit.shell import EXIT
from configparser import ConfigParser
import logging
import os
from ..lib.loaders import load_variables, INILoader, YMLLoader
from ..variables import LOGGER_NAME, PATH_TO_SCRIPT_TEASE
from ..constants import LOGGER_NAME
log = logging.getLogger(LOGGER_NAME)
# Exports
__all__ = (
"context_from_cli",
"filters_from_cli",
"options_from_cli",
"variables_from_file",
)
# Functions
def context_from_cli(variables):
"""Takes a list of variables given in the form of ``name:value`` and converts them to a dictionary.
:param variables: A list of strings of ``name:value`` pairs.
:type variables: list[str]
:rtype: dict
The ``value`` of the pair passes through "smart casting" to convert it to the appropriate Python data type.
"""
context = dict()
for i in variables:
key, value = i.split(":")
context[key] = smart_cast(value)
return context
def filters_from_cli(filters):
"""Takes a list of filters given in the form of ``name:value`` and converts them to a dictionary.
:param filters: A list of strings of ``attribute:value`` pairs.
:type filters: list[str]
:rtype: dict
"""
_filters = dict()
for i in filters:
key, value = i.split(":")
if key not in filters:
_filters[key] = list()
_filters[key].append(value)
return _filters
def options_from_cli(options):
"""Takes a list of variables given in the form of ``name:value`` and converts them to a dictionary.
:param options: A list of strings of ``name:value`` pairs.
:type options: list[str]
:rtype: dict
The ``value`` of the pair passes through "smart casting" to convert it to the appropriate Python data type.
"""
_options = dict()
for token in options:
try:
key, value = token.split(":")
for i in options:
key, value = i.split(":")
_options[key] = smart_cast(value)
except ValueError:
_options[token] = True
return _options
def loader(path, context=None, options=None, template_locations=None):
# The path may have been given as a file name (steps.ini), path, or an inventory name.
input_locations = [
path,
os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", path, "steps.ini"),
os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory", path, "steps.yml"),
]
path = None
for location in input_locations:
if os.path.exists(location):
path = location
break
if path is None:
log.warning("Path does not exist: %s" % path)
return None
def variables_from_file(path):
"""Loads variables from a given INI file.
# Initialize the loader.
if path.endswith(".ini"):
_loader = INILoader(
path,
context=context,
locations=template_locations,
**options
)
elif path.endswith(".yml"):
_loader = YMLLoader(
path,
context=context,
locations=template_locations,
**options
)
else:
log.error("Unsupported file format: %s" % path)
return None
:param path: The path to the INI file.
:type path: str
# Load the commands.
if not _loader.load():
log.error("Failed to load the input file: %s" % path)
return None
:rtype: dict | None
The resulting dictionary flattens the sections and values. For example:
.. code-block:: ini
[copyright]
name = ACME, Inc.
year = 2020
return _loader
[domain]
name = example.com
tld = example_com
The dictionary would contain:
def subcommands(subparsers):
"""Initialize sub-commands.
.. code-block:: python
:param subparsers: The subparsers instance from argparse.
{
'copyright_name': "ACME, Inc.",
'copyright_year': 2020,
'domain_name': "example.com",
'domain_tld': "example_com",
}
"""
sub = SubCommands(subparsers)
sub.docs()
sub.inventory()
sub.script()
def variables_from_cli(context, variables):
for token in variables:
try:
key, value = token.split(":")
context.add(key, smart_cast(value))
except ValueError:
context.add(token, True)
class SubCommands(object):
def __init__(self, subparsers):
self.subparsers = subparsers
def docs(self):
sub = self.subparsers.add_parser(
"docs",
help="Output documentation instead of code."
)
sub.add_argument(
"-o=",
"--output-format=",
choices=["html", "md", "plain", "rst"],
default="md",
dest="output_format",
help="The output format; HTML, Markdown, plain text, or ReStructuredText."
)
self._add_script_options(sub)
self._add_common_options(sub)
def inventory(self):
sub = self.subparsers.add_parser(
"inventory",
aliases=["inv"],
help="Copy an inventory item to a local directory."
)
sub.add_argument(
"name",
help="The name of the inventory item. Use ? to list available items."
)
sub.add_argument(
"-P=",
"--path=",
dest="to_path",
help="The path to where the item should be copied. Defaults to the current working directory."
)
self._add_common_options(sub)
def script(self):
sub = self.subparsers.add_parser(
"script",
help="Output the commands."
)
sub.add_argument(
"-c",
"--color",
action="store_true",
dest="color_enabled",
help="Enable code highlighting for terminal output."
)
sub.add_argument(
"-s",
"--shebang",
action="store_true",
dest="include_shebang",
help="Add the shebang to the beginning of the output."
)
self._add_script_options(sub)
self._add_common_options(sub)
def _add_common_options(self, sub):
sub.add_argument(
"-D",
"--debug",
action="store_true",
dest="debug_enabled",
help="Enable debug mode. Produces extra output."
)
sub.add_argument(
"-p",
action="store_true",
dest="preview_enabled",
help="Preview mode."
)
def _add_script_options(self, sub):
sub.add_argument(
"-C=",
"--context=",
action="append",
dest="variables",
help="Context variables for use in pre-parsing the config and templates. In the form of: name:value"
)
sub.add_argument(
"-i=",
"--input-file=",
default="steps.ini",
dest="steps_file",
help="The path to the configuration file."
)
# sub.add_argument(
# "-f=",
# "--filter=",
# action="append",
# dest="filters",
# help="Filter the commands in the form of: attribute:value"
# )
# Capture filters.
# if args.filters:
# filters = dict()
# for token in args.filters:
# key, value = token.split(":")
# if key not in filters:
# filters[key] = list()
#
# filters[key].append(value)
sub.add_argument(
"-O=",
"--option=",
action="append",
dest="options",
help="Common command options in the form of: name:value"
)
sub.add_argument(
"-P=",
"--profile=",
choices=["centos", "ubuntu"],
default="ubuntu",
dest="profile",
help="The OS profile to use."
)
sub.add_argument(
"-T=",
"--template-path=",
action="append",
dest="template_locations",
help="The location of template files that may be used with the template command."
)
sub.add_argument(
"-w=",
"--write=",
dest="output_file",
help="Write the output to disk."
)
sub.add_argument(
"-V=",
"--variables-file=",
dest="variables_file",
help="Load variables from a file."
)
# # Imports
#
# from commonkit import smart_cast
# from configparser import ConfigParser
# import logging
# import os
# from ..constants import LOGGER_NAME
#
# log = logging.getLogger(LOGGER_NAME)
#
# # Exports
#
# __all__ = (
# "context_from_cli",
# "filters_from_cli",
# "options_from_cli",
# "variables_from_file",
# )
#
# # Functions
#
#
# def context_from_cli(variables):
# """Takes a list of variables given in the form of ``name:value`` and converts them to a dictionary.
#
# :param variables: A list of strings of ``name:value`` pairs.
# :type variables: list[str]
#
# :rtype: dict
#
# The ``value`` of the pair passes through "smart casting" to convert it to the appropriate Python data type.
#
# """
# context = dict()
# for i in variables:
# key, value = i.split(":")
# context[key] = smart_cast(value)
#
# return context
#
#
# def filters_from_cli(filters):
# """Takes a list of filters given in the form of ``name:value`` and converts them to a dictionary.
#
# :param filters: A list of strings of ``attribute:value`` pairs.
# :type filters: list[str]
#
# :rtype: dict
#
# """
# _filters = dict()
# for i in filters:
# key, value = i.split(":")
# if key not in filters:
# _filters[key] = list()
#
# _filters[key].append(value)
#
# return _filters
#
#
# def options_from_cli(options):
# """Takes a list of variables given in the form of ``name:value`` and converts them to a dictionary.
#
# :param options: A list of strings of ``name:value`` pairs.
# :type options: list[str]
#
# :rtype: dict
#
# The ``value`` of the pair passes through "smart casting" to convert it to the appropriate Python data type.
#
# """
# _options = dict()
# for i in options:
# key, value = i.split(":")
# _options[key] = smart_cast(value)
#
# return _options
#
#
# def variables_from_file(path):
# """Loads variables from a given INI file.
#
# :param path: The path to the INI file.
# :type path: str
#
# :rtype: dict | None
#
# The resulting dictionary flattens the sections and values. For example:
#
# .. code-block:: ini
#
# [copyright]
# name = ACME, Inc.
# year = 2020
#
# [domain]
# name = example.com
# tld = example_com
#
# The dictionary would contain:
#
# .. code-block:: python
#
# {
# 'copyright_name': "ACME, Inc.",
# 'copyright_year': 2020,
# 'domain_name': "example.com",
# 'domain_tld': "example_com",
# }
#
# """
# if not os.path.exists(path):
# log.warning("Variables file does not exist: %s" % path)
# return None
#
# ini = ConfigParser()
# ini.read(path)
#
# variables = dict()
# for section in ini.sections():
# for key, value in ini.items(section):
# key = "%s_%s" % (section, key)
# variables[key] = smart_cast(value)
#
# return variables
if not os.path.exists(path):
log.warning("Variables file does not exist: %s" % path)
return None
ini = ConfigParser()
ini.read(path)
variables = dict()
for section in ini.sections():
for key, value in ini.items(section):
key = "%s_%s" % (section, key)
variables[key] = smart_cast(value)
return variables

@ -1,181 +1,153 @@
# Imports
from commonkit import copy_tree, highlight_code, indent, write_file
from commonkit import highlight_code
from commonkit.shell import EXIT
from markdown import markdown
import os
from ..lib.factories import command_factory
from ..constants import PROFILE
from ..variables import PATH_TO_SCRIPT_TEASE
from ..parsers import load_commands, load_config
# Exports
__all__ = (
"copy_inventory",
"generate_docs",
"generate_script",
"output_commands",
"output_docs",
"output_script",
)
# Functions
def copy_inventory(name, to_path=None):
"""Copy an inventory item to a path.
def output_commands(path, color_enabled=False, context=None, filters=None, locations=None, options=None):
"""Output commands found in a given configuration file.
:param name: The name of the inventory item. ``?`` will list available items.
:type name: str
:param path: The path to the configuration file.
:type path: str
:param to_path: The path to where the item should be copied. Defaults to the current working directory.
:type to_path: str
:param color_enabled: Indicates the output should be colorized.
:type color_enabled: bool
:param context: The context to be applied to the file before parsing it as configuration.
:type context: dict
:param filters: Output only those commands which match the given filters.
:type filters: dict
:param locations: The locations (paths) of additional resources.
:type locations: list[str]
:param options: Options to be applied to all commands.
:type options: dict
:rtype: int
:returns: An exit code.
"""
path = os.path.join(PATH_TO_SCRIPT_TEASE, "data", "inventory")
if name == "?":
for d in os.listdir(path):
print(d)
commands = load_commands(
path,
context=context,
filters=filters,
locations=locations,
options=options
)
if commands is None:
return EXIT.ERROR
return EXIT.OK
output = list()
for command in commands:
statement = command.get_statement(cd=True)
if statement is None:
continue
from_path = os.path.join(path, name)
output.append(statement)
output.append("")
if to_path is None:
to_path = os.path.join(os.getcwd(), name)
os.makedirs(to_path)
if color_enabled:
print(highlight_code("\n".join(output), language="bash"))
else:
print("\n".join(output))
if copy_tree(from_path, to_path):
return EXIT.OK
return EXIT.ERROR
def output_docs(path, context=None, filters=None, locations=None, options=None):
"""Output documentation for commands found in a given configuration file.
def generate_docs(loader, output_file=None, output_format="md", profile=PROFILE.UBUNTU):
"""Generate documentation from a commands file.
:param path: The path to the configuration file.
:type path: str
:param loader: The loader instance.
:type loader: BaseType[scripttease.lib.loaders.BaseLoader]
:param context: The context to be applied to the file before parsing it as configuration.
:type context: dict
:param output_file: The path to the output file.
:type output_file: str
:param filters: Output only those commands which match the given filters.
:type filters: dict
:param output_format: The output format; ``html``, ``md`` (Markdown, the default), ``plain`` (text), ``rst``
(ReStructuredText).
:type output_format: str
:param locations: The locations (paths) of additional resources.
:type locations: list[str]
:param profile: The operating system profile to use.
:type profile: str
:param options: Options to be applied to all commands.
:type options: dict
:rtype: int
:returns: An exit code.
"""
commands = command_factory(loader, profile=profile)
commands = load_commands(
path,
context=context,
filters=filters,
locations=locations,
options=options
)
if commands is None:
return EXIT.ERROR
count = 1
output = list()
for command in commands:
output.append("%s. %s" % (count, command.comment))
count += 1
if command.name in ("explain", "screenshot"):
output.append(command.get_output(output_format))
elif command.name == "template":
if output_format == "plain":
output.append("+++")
output.append(command.get_content())
output.append("+++")
output.append("")
elif output_format == "rst":
output.append(".. code-block:: %s" % command.get_target_language())
output.append("")
output.append(indent(command.get_content()))
output.append("")
else:
output.append("```%s" % command.get_target_language())
output.append(command.get_content())
output.append("```")
output.append("")
else:
statement = command.get_statement(include_comment=False, include_register=False, include_stop=False)
if statement is not None:
line = command.comment.replace("#", "")
output.append("%s:" % line.capitalize())
output.append("")
if output_format == "plain":
output.append("---")
output.append(statement)
output.append("---")
output.append("")
elif output_format == "rst":
output.append(".. code-block:: bash")
output.append("")
output.append(indent(statement))
output.append("")
else:
output.append("```bash")
output.append(statement)
output.append("```")
output.append("")
if output_format == "html":
_output = markdown("\n".join(output), extensions=['fenced_code'])
else:
_output = "\n".join(output)
print(_output)
if output_file:
write_file(output_file, _output)
print("\n".join(output))
return EXIT.OK
def generate_script(loader, color_enabled=False, include_shebang=False, output_file=None, profile=PROFILE.UBUNTU):
"""Generate statements from a commands file.
def output_script(path, color_enabled=False, context=None, filters=None, locations=None, options=None):
"""Output a script of commands found in a given configuration file.
:param loader: The loader instance.
:type loader: BaseType[scripttease.lib.loaders.BaseLoader]
:param path: The path to the configuration file.
:type path: str
:param color_enabled: Colorize the output.
:param color_enabled: Indicates the output should be colorized.
:type color_enabled: bool
:param include_shebang: Add the shebang to the beginning of the output.
:type include_shebang: bool
:param context: The context to be applied to the file before parsing it as configuration.
:type context: dict
:param filters: Output only those commands which match the given filters. NOT IMPLEMENTED.
:type filters: dict
:param output_file: The path to the output file.
:type output_file: str
:param locations: The locations (paths) of additional resources.
:type locations: list[str]
:param profile: The operating system profile to use.
:type profile: str
:param options: Options to be applied to all commands.
:type options: dict
:rtype: int
:returns: An exit code.
"""
commands = command_factory(loader, profile=profile)
if commands is None:
config = load_config(
path,
context=context,
locations=locations,
options=options
)
if config is None:
return EXIT.ERROR
output = list()
if include_shebang:
output.append("#! /usr/bin/env bash")
for command in commands:
if command.name in ("explain", "screenshot"):
continue
statement = command.get_statement(include_comment=True)
if statement is not None:
output.append(statement)
output.append("")
script = config.as_script()
if color_enabled:
print(highlight_code("\n".join(output), language="bash"))
print(highlight_code(script.to_string(), language="bash"))
else:
print("\n".join(output))
if output_file:
write_file(output_file, "\n".join(output))
print(script)
return EXIT.OK

@ -1,6 +0,0 @@
[package]
description = Install Nextcloud.
docs = https://nextcloud.com
tags = collaboration
title = Nextcloud
version = 0.1.0-d

@ -1,6 +0,0 @@
[package]
description = Install PostgreSQL.
docs = https://postgresql.org
tags = database, postgres
title = PostgreSQL
version = 0.1.0-d

@ -3,7 +3,7 @@ explain: In this tutorial, we are going to install Radicale.
header: yes
[make sure a maintenance root exists]
dir: /var/www/maint/www
mkdir: /var/www/maint/www
group: www-data
owner: www-data
recursive: yes
@ -12,14 +12,14 @@ recursive: yes
explain: The maintenance root is used to register an SSL certificate (below) before the site is completed and (later) after the site is live.
[install radicale]
pip: radicale
pip3: radicale
[install radicale screenshot]
screenshot: images/install.png
caption: Radical Installed
[create radicale configuration directory]
dir: /etc/radicale/config
mkdir: /etc/radicale/config
owner: radicale
recursive: yes
@ -27,7 +27,7 @@ recursive: yes
template: config.ini /etc/radicale/config/config.ini
[create the radicale user]
user: radicale
user.add: radicale
home: /
login: /sbin/nologin
system: yes

@ -1,6 +0,0 @@
[package]
description = Set up an Ubuntu server.
docs = https://ubuntu.com
tags = operating system
title = Ubuntu
version = 0.1.0-d

@ -24,47 +24,8 @@ __all__ = (
class Command(object):
"""A command collects parameters and establishes the standard interface for creating a command line statement."""
def __init__(self, statement, cd=None, comment=None, condition=None, name=None, prefix=None, register=None, shell=None, stop=False, sudo=None, tags=None, **kwargs):
"""Initialize a command.
:param statement: The command statement.
:type statement: str
:param cd: Change directory to this path.
:type cd: str
:param comment: A comment on the command.
:type comment: str
:param condition: A condition to be met before the command should execute. This must be in the form of a Bash
conditional. For example, ``-f path/to/file.txt``
:type condition: str
:param name: The canonical name of the command. Provided automatically when using the command factory.
:type name: str
:param prefix: Any valid statement that should come before the command statement.
:type prefix: str
:param register: Register the result of the command to this variable name.
:type register: str
:param shell: The shell to use. Not used by Script Tease, but may be important to integrators.
:type shell: str
:param stop: Don't continue with additional commands if this one fails.
:type stop: bool
:param sudo: A username or boolean that indicates sudo should be used.
:type sudo: bool | str
:param tags: A list of tags that help classify the command.
:type tags: list[str]
kwargs are added to the Command instance as ``options`` and are dynamically accessible.
"""
def __init__(self, statement, cd=None, comment=None, condition=None, name=None, prefix=None, register=None, stop=False, sudo=None, tags=None, **kwargs):
self.cd = cd
self.comment = comment
self.condition = condition
@ -72,7 +33,6 @@ class Command(object):
self.number = None
self.prefix = prefix
self.register = register
self.shell = shell
self.statement = statement
self.stop = stop
self.tags = tags or list()
@ -163,7 +123,6 @@ class Command(object):
class Content(object):
"""A special command-like resource for capturing non-statement content such as explanations and screenshots."""
def __init__(self, content_type, caption=None, css=None, heading=None, height=None, image=None, message=None,
width=None, **kwargs):
@ -269,16 +228,12 @@ class Content(object):
if self.width is not None:
a.append(indent(":width: %s" % self.width, 8))
a.append("")
else:
if self.caption:
a.append("%s: %s" % (self.caption, self.image))
else:
a.append(self.image)
a.append("")
return "\n".join(a)
def _get_message_output(self, output_format):
@ -604,48 +559,19 @@ class Sudo(object):
class Template(object):
"""A command-like resource that may be used to process template files."""
PARSER_JINJA = "jinja2"
PARSER_PYTHON = "python"
PARSER_SIMPLE = "simple"
def __init__(self, source, target, backup=True, parser=PARSER_JINJA, **kwargs):
"""Initialize a template.
:param source: The path to the template file.
:type source: str
:param target: The path to where the rendered template should be saved.
:type target: str
:param backup: Indicates whether a backup of an existing target should be created.
:type backup: bool
:param parser: The parser to use; ``jinja``, ``python``, or ``simple``.
:type parser: str
kwargs are the same as that of :py:class:`scripttease.lib.commands.base.Command`
Python templates are parsed using string interpolation. For example, ``%(variable_name)s``.
Simple templates look for variables in the form of ``$variable_name$``.
"""
self.backup_enabled = backup
self.cd = kwargs.pop("cd", None)
self.comment = kwargs.pop("comment", "create template %s" % target)
self.condition = kwargs.pop("condition", None)
# self.context = kwargs.pop("context", dict())
self.context = kwargs.pop("context", dict())
self.name = "template"
self.parser = parser
self.prefix = kwargs.pop("prefix", None)
self.language = kwargs.pop("lang", None)
self.locations = kwargs.pop("locations", list())
self.register = kwargs.pop("register", None)
self.source = os.path.expanduser(source)
self.stop = kwargs.pop("stop", False)
self.tags = kwargs.pop("tags", list())
self.target = target
sudo = kwargs.pop("sudo", None)
@ -658,10 +584,10 @@ class Template(object):
else:
self.sudo = Sudo()
self.context = kwargs
self.kwargs = kwargs
def __getattr__(self, item):
return self.context.get(item)
return self.kwargs.get(item)
# def __str__(self):
# return "template"
@ -748,11 +674,6 @@ class Template(object):
return "\n".join(lines)
def get_target_language(self):
"""Get the target language of the template output. Used when generating documentation.
:rtype: str
"""
if self.language is not None:
return self.language

@ -39,11 +39,9 @@ __all__ = (
def apache(op, **kwargs):
"""Execute an Apache-related command.
:param op: The operation to perform; ``reload``, ``restart``, ``start``, ``stop``, ``test``.
:type op: str
- op (str): The operation to perform; reload, restart, start, stop, test.
"""
# See https://unix.stackexchange.com/questions/258854/disable-and-enable-modules-in-apache-centos7
if op == "reload":
return apache_reload(**kwargs)
elif op == "restart":
@ -59,7 +57,6 @@ def apache(op, **kwargs):
def apache_reload(**kwargs):
"""Reload the apache service."""
kwargs.setdefault("comment", "reload apache")
kwargs.setdefault("register", "apache_reloaded")
@ -67,7 +64,6 @@ def apache_reload(**kwargs):
def apache_restart(**kwargs):
"""Restart the apache service."""
kwargs.setdefault("comment", "restart apache")
kwargs.setdefault("register", "apache_restarted")
@ -75,7 +71,6 @@ def apache_restart(**kwargs):
def apache_start(**kwargs):
"""Start the apache service."""
kwargs.setdefault("comment", "start apache")
kwargs.setdefault("register", "apache_started")
@ -83,14 +78,12 @@ def apache_start(**kwargs):
def apache_stop(**kwargs):
"""Stop the apache service."""
kwargs.setdefault("comment", "stop apache")
return Command("apachectl –k stop", **kwargs)
def apache_test(**kwargs):
"""Run a configuration test on apache."""
kwargs.setdefault("comment", "check apache configuration")
kwargs.setdefault("register", "apache_checks_out")
@ -100,8 +93,7 @@ def apache_test(**kwargs):
def service_reload(service, **kwargs):
"""Reload a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "reload %s service" % service)
@ -113,8 +105,7 @@ def service_reload(service, **kwargs):
def service_restart(service, **kwargs):
"""Restart a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "restart %s service" % service)
@ -126,8 +117,7 @@ def service_restart(service, **kwargs):
def service_start(service, **kwargs):
"""Start a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "start %s service" % service)
@ -139,8 +129,7 @@ def service_start(service, **kwargs):
def service_stop(service, **kwargs):
"""Stop a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "stop %s service" % service)
@ -152,8 +141,7 @@ def service_stop(service, **kwargs):
def system(op, **kwargs):
"""Perform a system operation.
:param op: The operation to perform; ``reboot``, ``update``, ``upgrade``.
:type op: str
- op (str): The operation to perform; reboot, update, upgrade.
"""
if op == "reboot":
@ -169,8 +157,7 @@ def system(op, **kwargs):
def system_install(package, **kwargs):
"""Install a system-level package.
:param package: The name of the package to install.
:type package: str
- name (str): The name of the package to install.
"""
kwargs.setdefault("comment", "install system package %s" % package)
@ -179,7 +166,6 @@ def system_install(package, **kwargs):
def system_reboot(**kwargs):
"""Reboot the system."""
kwargs.setdefault("comment", "reboot the system")
return Command("reboot", **kwargs)
@ -188,8 +174,7 @@ def system_reboot(**kwargs):
def system_uninstall(package, **kwargs):
"""Uninstall a system-level package.
:param package: The name of the package to remove.
:type package: str
- name (str): The name of the package to uninstall.
"""
kwargs.setdefault("comment", "remove system package %s" % package)
@ -198,14 +183,12 @@ def system_uninstall(package, **kwargs):
def system_update(**kwargs):
"""Update the system's package info."""
kwargs.setdefault("comment", "update system package info")
return Command("yum check-update", **kwargs)
def system_upgrade(**kwargs):
"""updated the system."""
kwargs.setdefault("comment", "upgrade the system")
return Command("yum update -y", **kwargs)
@ -214,20 +197,11 @@ def system_upgrade(**kwargs):
def user(name, groups=None, home=None, op="add", password=None, **kwargs):
"""Create or remove a user.
:param name: The username.
:type name: str
:param groups: A list of groups to which the user should belong.
:type groups: list | str
:param home: The path to the user's home directory.
:type home: str
:param op: The operation to perform; ``add`` or ``remove``.
:type op:
:param password: The user's password. (NOT IMPLEMENTED)
:type password: str
- name (str): The user name.
- groups (str | list): A list of groups to which the user should belong.
- home (str): The path to the user's home directory.
- op (str); The operation to perform; ``add`` or ``remove``.
- password (str): The user's password. (NOT IMPLEMENTED)
"""
if op == "add":

@ -1,29 +1,28 @@
from ...constants import EXCLUDED_KWARGS
from .base import Command
"""
[run django checks]
django: check
def django(management_command, *args, excluded_kwargs=None, **kwargs):
"""Common function for assembling Django management commands.
:param management_command: The name of the management command.
:type management_command: str
[export fixtures]
django: dump lookups.Category
:param excluded_kwargs: A dictionary of kwargs that should be excluded from the management command parameters.
:param excluded_kwargs: dict
[import fixtures]
django: load lookups.Category
:rtype: scripttease.lib.commands.base.Command
[migrate the database]
django: migrate
If provided, args are passed directly to the command as positional parameters.
[collect static files]
django: static
Any provided kwargs are converted to long form parameters. For example, database="alternative_db" becomes
``--database="alternative_db"``.
[create super user (ad hoc command)]
django: createsuperuser root
A kwarg with a ``True`` value becomes a long form parameter with no value. For example, natural_foreign=True becomes
``--natural-foreign``.
"""
from ...constants import EXCLUDED_KWARGS
from .base import Command
Finally, any kwarg that is not a string is passed without quotes. For example, testing=1 becomes ``--testing=1``.
"""
def django(management_command, *args, excluded_kwargs=None, **kwargs):
# The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default.
excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS
@ -62,92 +61,37 @@ def django(management_command, *args, excluded_kwargs=None, **kwargs):
def django_check(**kwargs):
"""Run Django checks."""
kwargs.setdefault("comment", "run django checks")
kwargs.setdefault("register", "django_checks_out")
return django("check", **kwargs)
def django_createsuperuser(username, email=None, **kwargs):
"""Create a superuser account.
:param username: The name for the user account.
:type username: str
:param email: The user's email address. Optional, but recommended because the account must be created without a
password.
:type email: str
"""
kwargs.setdefault("comment", "create the %s superuser" % username)
kwargs['username'] = username
kwargs['noinput'] = True
if email is not None:
kwargs['email'] = email
return django("createsuperuser", **kwargs)
def django_dump(target, path=None, **kwargs):
"""Dump data fixtures.
:param target: The app name or ``app.ModelName``.
:type target: str
:param path: The path to the fixture file.
:type path: str
"""
kwargs.setdefault("comment", "dump app/model data for %s" % target)
kwargs.setdefault("format", "json")
kwargs.setdefault("indent", 4)
app = target
file_name = "%s/initial.%s" % (app, kwargs['format'])
if "." in target:
app, model = target.split(".")
file_name = "%s/%s.%s" % (app, model.lower(), kwargs['format'])
if path is None:
path = "../fixtures/%s" % file_name
path = "../fixtures/%s.%s" % (target, kwargs['format'])
return django("dumpdata", target, "> %s" % path, **kwargs)
def django_load(target, path=None, **kwargs):
"""Load data fixtures.
:param target: The app name or ``app.ModelName``.
:type target: str
:param path: The path to the fixture file.
:type path: str
"""
kwargs.setdefault("comment", "load app/model data from %s" % target)
input_format = kwargs.pop("format", "json")
app = target
file_name = "%s/initial.%s" % (app, input_format)
if "." in target:
app, model = target.split(".")
file_name = "%s/%s.%s" % (app, model.lower(), input_format)
if path is None:
path = "../fixtures/%s" % file_name
path = "../fixtures/%s.%s" % (target, input_format)
return django("loaddata", path, **kwargs)
def django_migrate(**kwargs):
"""Apply database migrations."""
kwargs.setdefault("comment", "apply database migrations")
return django("migrate", **kwargs)
def django_static(**kwargs):
"""Collect static files."""
kwargs.setdefault("comment", "collect static files")
kwargs.setdefault("noinput", True)
return django("collectstatic", **kwargs)
@ -156,8 +100,6 @@ def django_static(**kwargs):
DJANGO_MAPPINGS = {
'django': django,
'django.check': django_check,
'django.collectstatic': django_static,
'django.createsuperuser': django_createsuperuser,
'django.dump': django_dump,
'django.dumpdata': django_dump,
'django.load': django_load,

@ -3,21 +3,6 @@ from ...exceptions import InvalidInput
def dialog(message, height=15, title="Message", width=100, **kwargs):
"""Display a graphical feedback box to the user. Note that the ``dialog`` command must be available.
:param message: The message to be displayed.
:type message: str
:param height: The height of the dialog box.
:type height: int
:param title: The title displayed.
:type title: str
:param width: The width of the dialog box.
:type width: int
"""
statement = list()
statement.append("dialog --clear")
statement.append('--backtitle "%s"' % title)
@ -28,60 +13,18 @@ def dialog(message, height=15, title="Message", width=100, **kwargs):
def echo(message, **kwargs):
"""Display a message.
:param message: The message to be displayed.
:type message: str
"""
return Command('echo "%s"' % message, **kwargs)
def explain(message, heading=None, **kwargs):
"""Create an explanation for documentation.
:param message: The message to be displayed.
:type message: str
:param heading: Optional heading for the output.
:type heading: str
"""
return Content("explain", message=message, heading=heading, **kwargs)
def screenshot(image, caption=None, css=None, height=None, width=None, **kwargs):
"""Create a screenshot for documentation.
:param image: The URL or path to the image file.
:type image: str
:param caption: A caption for the image.
:type caption: str
:param css: CSS classes to be applied to the image tag.
:type css: str
:param height: The maximum height of the image.
:type height: int
:param width: The maximum widht of the image.
:type width: int
"""
return Content("screenshot", caption=caption, css=css, height=height, image=image, width=width, **kwargs)
def screenshot(image, caption=None, height=None, width=None, **kwargs):
return Content("screenshot", caption=caption, height=height, image=image, width=width, **kwargs)
def slack(message, url=None, **kwargs):
"""Send a message to Slack.
:param message: The message to be displayed.
:type message: str
:param url: The channel URL.
:type url: str
"""
if url is None:
raise InvalidInput("Slack command requires a url parameter.")
@ -94,18 +37,6 @@ def slack(message, url=None, **kwargs):
def twist(message, title="Notice", url=None, **kwargs):
"""Send a message to Twist.
:param message: The message to be displayed.
:type message: str
:param title: A title for the message.
:type title: str
:param url: The channel URL.
:type url: str
"""
if url is None:
raise InvalidInput("Twist command requires a url parameter.")

@ -18,27 +18,7 @@ __all__ = (
)
def mysql(command, *args, excluded_kwargs=None, host="localhost", password=None, port=3306, user="root", **kwargs):
"""Get a mysql-related command using commonly required parameters.
:param command: The name of the command.
:type command: str
:param excluded_kwargs: Keyword arguments to exclude from automatic switch creation.
:type excluded_kwargs: list[str]
:param host: The host name.
:type host: str
:param password: The password to use.
:type password: str
:param port: The TCP port number.
:type port: int
:param user: The username that will be used to execute the command.
"""
def mysql(command, *args, host="localhost", excluded_kwargs=None, password=None, port=3306, user="root", **kwargs):
# The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default.
excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS
@ -83,15 +63,6 @@ def mysql(command, *args, excluded_kwargs=None, host="localhost", password=None,
def mysql_create(database, owner=None, **kwargs):
"""Create a MySQL database.
:param database: The database name.
:type database: str
:param owner: The owner (user/role name) of the new database.
:type owner: str
"""
kwargs.setdefault("comment", "create mysql database")
command = mysql("mysqladmin create", database, **kwargs)
@ -104,27 +75,12 @@ def mysql_create(database, owner=None, **kwargs):
def mysql_drop(database, **kwargs):
"""Remove a MySQL database.
:param database: The database name.
:type database: str
"""
kwargs.setdefault("comment", "drop %s mysql database" % database)
return mysql("mysqladmin drop", database, **kwargs)
def mysql_dump(database, path=None, **kwargs):
"""Export a MySQL database.
:param database: The database name.
:type database: str
:param path: The name/path of the export file. Defaults the database name plus ``.sql``.
:type path: str
"""
kwargs.setdefault("comment", "dump mysql database")
if path is None:
@ -134,12 +90,6 @@ def mysql_dump(database, path=None, **kwargs):
def mysql_exists(database, **kwargs):
"""Determine if a MySQL database exists.
:param database: The database name.
:type database: str
"""
kwargs.setdefault("comment", "determine if %s mysql database exists" % database)
kwargs.setdefault("register", "%s_exists" % database)
@ -155,14 +105,13 @@ def mysql_exists(database, **kwargs):
def mysql_grant(to, database=None, privileges="ALL", **kwargs):
"""Grant privileges to a user.
:param to: The username to which privileges are granted.
:type to: str
:param database: The database name.
:type database: str
:param privileges: The privileges to be granted.
:type privileges: str
- to (str): The user name to which privileges are granted.
- database (str): The database name.
- host (str): The database host name or IP address.
- password (str): The password for the user with sufficient access privileges to execute the command.
- port (int): The TCP port number of the MySQL service running on the host.
- privileges (str): The privileges to be granted.
- user (str): The name of the user with sufficient access privileges to execute the command.
"""
kwargs.setdefault("comment", "grant mysql privileges to %s" % to)
@ -185,39 +134,12 @@ def mysql_grant(to, database=None, privileges="ALL", **kwargs):
def mysql_load(database, path, **kwargs):
"""Load data into a MySQL database.
:param database: The database name.
:type database: str
:param path: The path to the file to be loaded.
:type path: str
"""
kwargs.setdefault("comment", "load data into a mysql database")
return mysql("mysql", database, "< %s" % path, **kwargs)
def mysql_user(name, admin_pass=None, admin_user="root", op="create", password=None, **kwargs):
"""Work with a MySQL user.
:param name: The username.
:type name: str
:param admin_pass: The password for the user with admin privileges.
:type admin_pass: str
:param admin_user: The username of the user with admin privileges.
:type admin_user: str
:param op: The operation to perform: ``create``, ``drop``, ``exists``.
:type op: str
:param password: The password for a new user.
:type password: str
"""
host = kwargs.get("host", "localhost")
if op == "create":

@ -12,33 +12,12 @@ __all__ = (
"pgsql_drop",
"pgsql_dump",
"pgsql_exists",
"pgsql_grant",
"pgsql_load",
"pgsql_user",
)
def pgsql(command, *args, excluded_kwargs=None, host="localhost", password=None, port=5432, user="postgres", **kwargs):
"""Get a postgres-related command using commonly required parameters.
:param command: The name of the command.
:type command: str
:param excluded_kwargs: Keyword arguments to exclude from automatic switch creation.
:type excluded_kwargs: list[str]
:param host: The host name.
:type host: str
:param password: The password to use.
:type password: str
:param port: The TCP port number.
:type port: int
:param user: The username that will be used to execute the command.
"""
def pgsql(command, *args, host="localhost", excluded_kwargs=None, password=None, port=5432, user="postgres", **kwargs):
# The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default.
excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS
@ -80,18 +59,6 @@ def pgsql(command, *args, excluded_kwargs=None, host="localhost", password=None,
def pgsql_create(database, owner=None, template=None, **kwargs):
"""Create a PostgreSQL database.
:param database: The database name.
:type database: str
:param owner: The owner (user/role name) of the new database.
:type owner: str
:param template: The database template name to use, if any.
:type template: str
"""
kwargs.setdefault("comment", "create %s postgres database" % database)
if owner is not None:
@ -120,27 +87,12 @@ def pgsql_create(database, owner=None, template=None, **kwargs):
def pgsql_drop(database, **kwargs):
"""Remove a PostgreSQL database.
:param database: The database name.
:type database: str
"""
kwargs.setdefault("comment", "drop %s postgres database" % database)
return pgsql("dropdb", database, **kwargs)
def pgsql_dump(database, path=None, **kwargs):
"""Export a PostgreSQL database.
:param database: The database name.
:type database: str
:param path: The name/path of the export file. Defaults the database name plus ``.sql``.
:type path: str
"""
kwargs.setdefault("comment", "dump postgres database")
kwargs.setdefault("column_inserts", True)
@ -154,12 +106,6 @@ def pgsql_dump(database, path=None, **kwargs):
def pgsql_exists(database, **kwargs):
"""Determine if a PostgreSQL database exists.
:param database: The database name.
:type database: str
"""
kwargs.setdefault("comment", "determine if %s postgres database exists" % database)
kwargs.setdefault("register", "%s_exists" % database)
@ -169,68 +115,7 @@ def pgsql_exists(database, **kwargs):
return command
def pgsql_grant(to, database=None, privileges="ALL", schema=None, table=None, **kwargs):
"""Grant privileges to a user.
:param to: The username to which privileges are granted.
:type to: str
:param database: The database name. Required.
:type database: str
:param privileges: The privileges to be granted. See https://www.postgresql.org/docs/current/sql-grant.html
:type privileges: str
:param schema: The schema to which the privileges apply.
:type schema: str
:param table: The table name to which privileges apply.
:type table: str
.. note::
A schema or table is required and the privileges must be compatible with the target object.
"""
if database is None:
raise InvalidInput("Database is required.")
kwargs.setdefault("comment", "grant postgres privileges to %s" % to)
kwargs['dbname'] = database
if schema is not None:
target = "SCHEMA %s" % schema
elif table is not None:
target = "TABLE %s" % table
else:
raise InvalidInput("Either schema or table is required.")
_privileges = privileges
if privileges.lower() == "all":
_privileges = "ALL PRIVILEGES"
# See https://www.postgresql.org/docs/current/sql-grant.html
sql = "GRANT %(privileges)s ON %(target)s TO %(user)s" % {
'privileges': _privileges,
'target': target,
'user': to,
}
command = pgsql("psql", **kwargs)
command.statement += ' -c "%s"' % sql
return command
def pgsql_load(database, path, **kwargs):
"""Load data into a PostgreSQL database.
:param database: The database name.
:type database: str
:param path: The path to the file to be loaded.
:type path: str
"""
kwargs.setdefault("comment", "load data into a postgres database")
kwargs['dbname'] = database
@ -240,24 +125,6 @@ def pgsql_load(database, path, **kwargs):
def pgsql_user(name, admin_pass=None, admin_user="postgres", op="create", password=None, **kwargs):
"""Work with a PostgreSQL user.
:param name: The username.
:type name: str
:param admin_pass: The password for the user with admin privileges.
:type admin_pass: str
:param admin_user: The username of the user with admin privileges.
:type admin_user: str
:param op: The operation to perform: ``create``, ``drop``, ``exists``.
:type op: str
:param password: The password for a new user.
:type password: str
"""
if op == "create":
kwargs.setdefault("comment", "create %s postgres user" % name)
@ -269,7 +136,7 @@ def pgsql_user(name, admin_pass=None, admin_user="postgres", op="create", passwo
command.statement += " -c \"ALTER USER %s WITH ENCRYPTED PASSWORD '%s';\"" % (name, password)
return command
elif op in ("drop", "remove"):
elif op == "drop":
kwargs.setdefault("comment", "remove %s postgres user" % name)
return pgsql("dropuser", name, password=admin_pass, user=admin_user, **kwargs)
@ -293,7 +160,6 @@ PGSQL_MAPPINGS = {
'pgsql.drop': pgsql_drop,
'pgsql.dump': pgsql_dump,
'pgsql.exists': pgsql_exists,
'pgsql.grant': pgsql_grant,
# 'pgsql.sql': pgsql_exec,
'pgsql.user': pgsql_user,
}

@ -2,12 +2,6 @@ from .base import Command
def php_module(name, **kwargs):
"""Enable a PHP module.
:param name: The module name.
:type name: str
"""
statement = "phpenmod %s" % name
return Command(statement, **kwargs)

@ -5,11 +5,8 @@ from .base import Command, MultipleCommands, Prompt
def append(path, content=None, **kwargs):
"""Append content to a file.
:param path: The path to the file.
:type path: str
:param content: The content to be appended.
:type content: str
- path (str): The path to the file.
- content (str): The content to be appended.
"""
kwargs.setdefault("comment", "append to %s" % path)
@ -23,26 +20,12 @@ def archive(from_path, absolute=False, exclude=None, file_name="archive.tgz", st
**kwargs):
"""Create a file archive.
:param from_path: The path that should be archived.
:type from_path: str
:param absolute: Set to ``True`` to preserve the leading slash.
:type absolute: bool
:param exclude: A pattern to be excluded from the archive.
:type exclude: str
:param file_name: The name of the archive file.
:type file_name: str
:param strip: Remove the specified number of leading elements from the path.
:type strip: int
:param to_path: Where the archive should be created. This should *not* include the file name.
:type to_path: str
:param view: View the output of the command as it happens.
:type view: bool
- from_path (str): The path that should be archived.
- absolute (bool): Set to ``True`` to preserve the leading slash.
- exclude (str): A pattern to be excluded from the archive.
- strip (int): Remove the specified number of leading elements from the path.
- to_path (str): Where the archive should be created. This should *not* include the file name.
- view (bool): View the output of the command as it happens.
"""
tokens = ["tar"]
@ -73,14 +56,9 @@ def archive(from_path, absolute=False, exclude=None, file_name="archive.tgz", st
def certbot(domain_name, email=None, webroot=None, **kwargs):
"""Get new SSL certificate from Let's Encrypt.
:param domain_name: The domain name for which the SSL certificate is requested.
:type domain_name: str
:param email: The email address of the requester sent to the certificate authority. Required.
:type email: str
:param webroot: The directory where the challenge file will be created.
:type webroot: str
- domain_name (str): The domain name for which the SSL certificate is requested.
- email (str): The email address of the requester sent to the certificate authority. Required.
- webroot (str): The directory where the challenge file will be created.
"""
_email = email or os.environ.get("SCRIPTTEASE_CERTBOT_EMAIL", None)
@ -102,17 +80,10 @@ def certbot(domain_name, email=None, webroot=None, **kwargs):
def copy(from_path, to_path, overwrite=False, recursive=False, **kwargs):
"""Copy a file or directory.
:param from_path: The file or directory to be copied.
:type from_path: str
:param to_path: The location to which the file or directory should be copied.
:type to_path: str
:param overwrite: Indicates files and directories should be overwritten if they exist.
:type overwrite: bool
:param recursive: Copy sub-directories.
:type recursive: bool
- from_path (str): The file or directory to be copied.
- to_path (str): The location to which the file or directory should be copied.
- overwrite (bool): Indicates files and directories should be overwritten if they exist.
- recursive (bool): Copy sub-directories.
"""
kwargs.setdefault("comment", "copy %s to %s" % (from_path, to_path))
@ -135,14 +106,9 @@ def copy(from_path, to_path, overwrite=False, recursive=False, **kwargs):
def directory(path, group=None, mode=None, owner=None, recursive=False, **kwargs):
"""Create a directory.
:param path: The path to be created.
:type path: str
:param mode: The access permissions of the new directory.
:type mode: int | str
:param recursive: Create all directories along the path.
:type recursive: bool
- path (str): The path to be created.
- mode (int | str): The access permissions of the new directory.
- recursive (bool): Create all directories along the path.
"""
comment = kwargs.pop("comment", "create directory %s" % path)
@ -189,23 +155,12 @@ def directory(path, group=None, mode=None, owner=None, recursive=False, **kwargs
def extract(from_path, absolute=False, exclude=None, strip=None, to_path=None, view=False, **kwargs):
"""Extract a file archive.
:param from_path: The path to be extracted.
:type from_path: str
:param absolute: Set to ``True`` to preserve the leading slash.
:type absolute: bool
:param exclude: A pattern to be excluded from the extraction.
:type exclude: str
:param strip: Remove the specified number of leading elements from the path.
:type strip: int
:param to_path: Where the extraction should occur.
:type to_path: str
:param view: View the output of the command as it happens.
:type view: bool
- from_path (str): The path that should be archived.
- absolute (bool): Set to ``True`` to preserve the leading slash.
- exclude (str): A pattern to be excluded from the archive.
- strip (int): Remove the specified number of leading elements from the path.
- to_path (str): Where the archive should be extracted. This should *not* include the file name.
- view (bool): View the output of the command as it happens.
"""
_to_path = to_path or "./"
@ -237,14 +192,9 @@ def extract(from_path, absolute=False, exclude=None, strip=None, to_path=None, v
def link(source, force=False, target=None, **kwargs):
"""Create a symlink.
:param source: The source of the link.
:type source: str
:param force: Force the creation of the link.
:type force: bool
:param target: The name or path of the target. Defaults to the base name of the source path.
:type target: str
- source (str): The source of the link.
- force (bool): Force the creation of the link.
- target (str): The name or path of the target. Defaults to the base name of the source path.
"""
_target = target or os.path.basename(source)
@ -265,11 +215,8 @@ def link(source, force=False, target=None, **kwargs):
def move(from_path, to_path, **kwargs):
"""Move a file or directory.
:param from_path: The current path.
:type from_path: str
:param to_path: The new path.
:type to_path: str
- from_path (str): The current path.
- to_path (str): The new path.
"""
kwargs.setdefault("comment", "move %s to %s" % (from_path, to_path))
@ -281,20 +228,11 @@ def move(from_path, to_path, **kwargs):
def perms(path, group=None, mode=None, owner=None, recursive=False, **kwargs):
"""Set permissions on a file or directory.
:param path: The path to be changed.
:type path: str
:param group: The name of the group to be applied.
:type group: str
:param mode: The access permissions of the file or directory.
:type mode: int | str
:param owner: The name of the user to be applied.
:type owner: str
:param recursive: Update all files and directories along the path.
:type recursive: bool
- path (str): The path to be changed.
- group (str): The name of the group to be applied.
- mode (int | str): The access permissions of the file or directory.
- owner (str): The name of the user to be applied.
- recursive: Create all directories along the path.
"""
comment = kwargs.pop("comment", "set permissions on %s" % path)
@ -354,26 +292,13 @@ def perms(path, group=None, mode=None, owner=None, recursive=False, **kwargs):
def prompt(name, back_title="Input", choices=None, default=None, dialog=False, help_text=None, label=None, **kwargs):
"""Prompt the user for input.
:param name: The programmatic name of the input.
:type name: str
:param back_title: The back title used with the dialog command.
:type back_title: str
:param choices: A list of valid choices.
:type choices: list | str
:param default: The default value.
:type default: str
:param dialog: Use a dialog command for the prompt.
:type dialog: bool
:param help_text: The text to display with the dialog command.
:type help_text: str
:param label: The label for the input.
:type label: str
- name (str): The programmatic name of the input.
- back_title (str): The back title used with the dialog command.
- choices (str | list): A list of valid choices.
- default: The default value.
- dialog (bool): Use a dialog command for the prompt.
- help_text (str): The text to display with the dialog command.
- label (str): The label for the input.
"""
return Prompt(
@ -391,14 +316,9 @@ def prompt(name, back_title="Input", choices=None, default=None, dialog=False, h
def remove(path, force=False, recursive=False, **kwargs):
"""Remove a file or directory.
:param path: The path to be removed.
:type path: str
:param force: Force the removal.
:type force: bool
:param recursive: Remove all directories along the path.
:type recursive: bool
- path (str): The path to be removed.
- force (bool): Force the removal.
- recursive (bool): Remove all directories along the path.
"""
kwargs.setdefault("comment", "remove %s" % path)
@ -416,23 +336,14 @@ def remove(path, force=False, recursive=False, **kwargs):
return Command(" ".join(statement), **kwargs)
def replace(path, backup=".b", delimiter="/", find=None, sub=None, **kwargs):
def replace(path, backup=".b", delimiter="/", find=None, replace=None, **kwargs):
"""Find and replace text in a file.
:param path: The path to the file to be edited.
:type path: str
:param backup: The backup file extension to use.
:type backup: str
:param delimiter: The pattern delimiter.
:type delimiter: str
:param find: The old text. Required.
:param find: str
:param sub: The new text. Required.
:type sub: str
- path (str): The path to the file to be edited.
- backup (str): The backup file extension to use.
- delimiter (str): The pattern delimiter.
- find (str): The old text. Required.
- replace (str): The new text. Required.
"""
@ -443,7 +354,7 @@ def replace(path, backup=".b", delimiter="/", find=None, sub=None, **kwargs):
'delimiter': delimiter,
'path': path,
'pattern': find,
'replace': sub,
'replace': replace,
}
template = "sed -i %(backup)s 's%(delimiter)s%(pattern)s%(delimiter)s%(replace)s%(delimiter)sg' %(path)s"
@ -454,12 +365,7 @@ def replace(path, backup=".b", delimiter="/", find=None, sub=None, **kwargs):
def run(statement, **kwargs):
"""Run any command.
:param statement: The statement to be executed.
:type statement: str
"""
"""Run any command."""
kwargs.setdefault("comment", "run statement")
return Command(statement, **kwargs)
@ -469,35 +375,16 @@ def rsync(source, target, delete=False, exclude=None, host=None, key_file=None,
recursive=True, user=None, **kwargs):
"""Synchronize a directory structure.
:param source: The source directory.
:type source: str
:param target: The target directory.
:type target: str
:param delete: Indicates target files that exist in source but not in target should be removed.
:type delete: bool
:param exclude: The path to an exclude file.
:type exclude: str
:param host: The host name or IP address.
:type host: str
:param key_file: The privacy SSH key (path) for remote connections. User expansion is automatically applied.
:type key_file: str
:param links: Include symlinks in the sync.
:type links: bool
:param port: The SSH port to use for remote connections.
:type port: int
:param recursive: Indicates source contents should be recursively synchronized.
:type recursive: bool
:param user: The username to use for remote connections.
:type user: str
- source (str): The source directory.
- target (str): The target directory.
- delete (bool): Indicates target files that exist in source but not in target should be removed.
- exclude (str): The path to an exclude file.
- host (str): The host name or IP address. This causes the command to run over SSH.
- key_file (str): The privacy SSH key (path) for remote connections. User expansion is automatically applied.
- links (bool): Include symlinks in the sync.
- port (int): The SSH port to use for remote connections.
- recursive (bool): Indicates source contents should be recursively synchronized.
- user (str): The user name to use for remote connections.
"""
# - guess: When ``True``, the ``host``, ``key_file``, and ``user`` will be guessed based on the base name of
@ -568,23 +455,12 @@ def rsync(source, target, delete=False, exclude=None, host=None, key_file=None,
def scopy(from_path, to_path, host=None, key_file=None, port=22, user=None, **kwargs):
"""Copy a file or directory to a remote server.
:param from_path: The source of the copy.
:type from_path: str
:param to_path: The remote target of the copy.
:type to_path: str
:param host: The host name or IP address. Required.
:type host: str
:param key_file: The privacy SSH key (path) for remote connections. User expansion is automatically applied.
:type key_file: str
:param port: The SSH port to use for remote connections.
:type port: int
:param user: The username to use for remote connections.
:type user: str
- from_path (str): The source directory.
- to_path (str): The target directory.
- host (str): The host name or IP address. Required.
- key_file (str): The privacy SSH key (path) for remote connections. User expansion is automatically applied.
- port (int): The SSH port to use for remote connections.
- user (str): The user name to use for remote connections.
"""
kwargs.setdefault("comment", "copy %s to remote %s" % (from_path, to_path))
@ -615,23 +491,16 @@ def scopy(from_path, to_path, host=None, key_file=None, port=22, user=None, **kw
def sync(source, target, delete=False, exclude=None, links=True, recursive=True, **kwargs):
"""Synchronize a local directory structure.
:param source: The source directory.
:type source: str
:param target: The target directory.
:type target: str
:param delete: Indicates target files that exist in source but not in target should be removed.
:type delete: bool
:param exclude: The path to an exclude file.
:type exclude: str
:param links: Include symlinks in the sync.
:type links: bool
:param recursive: Indicates source contents should be recursively synchronized.
:type recursive: bool
- source (str): The source directory.
- target (str): The target directory.
- delete (bool): Indicates target files that exist in source but not in target should be removed.
- exclude (str): The path to an exclude file.
- host (str): The host name or IP address. This causes the command to run over SSH.
- key_file (str): The privacy SSH key (path) for remote connections. User expansion is automatically applied.
- links (bool): Include symlinks in the sync.
- port (int): The SSH port to use for remote connections.
- recursive (bool): Indicates source contents should be recursively synchronized.
- user (str): The user name to use for remote connections.
"""
# - guess: When ``True``, the ``host``, ``key_file``, and ``user`` will be guessed based on the base name of
@ -683,8 +552,7 @@ def sync(source, target, delete=False, exclude=None, links=True, recursive=True,
def touch(path, **kwargs):
"""Touch a file or directory.
:param path: The file or directory to touch.
:type path: str
- path (str): The file or directory to touch.
"""
kwargs.setdefault("comment", "touch %s" % path)
@ -695,8 +563,7 @@ def touch(path, **kwargs):
def wait(seconds, **kwargs):
"""Pause execution for a number of seconds.
:param seconds: The number of seconds to wait.
:type seconds: int
- seconds (int): The number of seconds to wait.
"""
kwargs.setdefault("comment", "pause for %s seconds" % seconds)
@ -707,11 +574,8 @@ def wait(seconds, **kwargs):
def write(path, content=None, **kwargs):
"""Write to a file.
:param path: The file to be written.
:type path: str
:param content: The content to be written. Note: If omitted, this command is equivalent to ``touch``.
:type content: str
- path (str): The file to be written.
- content (str): The content to be written. Note: If omitted, this command is equivalent to ``touch``.
"""
_content = content or ""
@ -746,7 +610,6 @@ POSIX_MAPPINGS = {
'run': run,
'rsync': rsync,
'scopy': scopy,
'ssl': certbot,
'sync': sync,
'touch': touch,
'wait': wait,

@ -4,20 +4,11 @@ from .base import Command
def python_pip(name, op="install", upgrade=False, venv=None, version=3, **kwargs):
"""Use pip to install or uninstall a Python package.
:param name: The name of the package.
:type name: str
:param op: The operation to perform; ``install``, ``remove``
:type op: str
:param upgrade: Upgrade an installed package.
:type upgrade: bool
:param venv: The name of the virtual environment to load.
:type venv: str
:param version: The Python version to use, e.g. ``2`` or ``3``.
:type version: int
- name (str): The name of the package.
- op (str): The operation to perform; install, uninstall
- upgrade (bool): Upgrade an installed package.
- venv (str): The name of the virtual environment to load.
- version (int): The Python version to use, e.g. ``2`` or ``3``.
"""
manager = "pip"
@ -66,8 +57,7 @@ def python_pip_file(path, venv=None, version=3, **kwargs):
def python_virtualenv(name, **kwargs):
"""Create a Python virtual environment.
:param name: The name of the environment to create.
:type name: str
- name (str): The name of the environment to create.
"""
kwargs.setdefault("comment", "create %s virtual environment" % name)

@ -1,7 +1,7 @@
# Imports
from commonkit import split_csv
from .base import Command
from .base import Command, Template
from .django import DJANGO_MAPPINGS
from .messages import MESSAGE_MAPPINGS
from .mysql import MYSQL_MAPPINGS
@ -44,8 +44,7 @@ __all__ = (
def apache(op, **kwargs):
"""Execute an Apache-related command.
:param op: The operation to perform; ``reload``, ``restart``, ``start``, ``stop``, ``test``.
:type op: str
- op (str): The operation to perform; reload, restart, start, stop, test.
"""
if op == "reload":
@ -65,8 +64,7 @@ def apache(op, **kwargs):
def apache_disable_module(module, **kwargs):
"""Disable an Apache module.
:param module: The name of the module.
:type module: str
- name (str): The module name.
"""
kwargs.setdefault("comment", "disable %s apache module" % module)
@ -77,8 +75,7 @@ def apache_disable_module(module, **kwargs):
def apache_disable_site(site, **kwargs):
"""Disable an Apache site.
:param site: The site/domain name.
:type site: str
- name (str): The domain name.
"""
kwargs.setdefault("comment", "disable %s apache site" % site)
@ -89,8 +86,7 @@ def apache_disable_site(site, **kwargs):
def apache_enable_module(module, **kwargs):
"""Enable an Apache module.
:param module: The name of the module.
:type module: str
- name (str): The module name.
"""
kwargs.setdefault("comment", "enable %s apache module" % module)
@ -101,8 +97,6 @@ def apache_enable_module(module, **kwargs):
def apache_enable_site(site, **kwargs):
"""Enable an Apache site.
:param site: The site/domain name.
:type site: str
"""
kwargs.setdefault("comment", "enable %s apache module" % site)
@ -111,7 +105,6 @@ def apache_enable_site(site, **kwargs):
def apache_reload(**kwargs):
"""Reload the apache service."""
kwargs.setdefault("comment", "reload apache")
kwargs.setdefault("register", "apache_reloaded")
@ -119,7 +112,6 @@ def apache_reload(**kwargs):
def apache_restart(**kwargs):
"""Restart the apache service."""
kwargs.setdefault("comment", "restart apache")
kwargs.setdefault("register", "apache_restarted")
@ -127,7 +119,6 @@ def apache_restart(**kwargs):
def apache_start(**kwargs):
"""Start the apache service."""
kwargs.setdefault("comment", "start apache")
kwargs.setdefault("register", "apache_started")
@ -135,14 +126,12 @@ def apache_start(**kwargs):
def apache_stop(**kwargs):
"""Stop the apache service."""
kwargs.setdefault("comment", "stop apache")
return Command("service apache2 stop", **kwargs)
def apache_test(**kwargs):
"""Run a configuration test on apache."""
kwargs.setdefault("comment", "check apache configuration")
kwargs.setdefault("register", "apache_checks_out")
@ -152,8 +141,7 @@ def apache_test(**kwargs):
def service_reload(service, **kwargs):
"""Reload a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "reload %s service" % service)
@ -165,8 +153,7 @@ def service_reload(service, **kwargs):
def service_restart(service, **kwargs):
"""Restart a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "restart %s service" % service)
@ -178,8 +165,7 @@ def service_restart(service, **kwargs):
def service_start(service, **kwargs):
"""Start a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "start %s service" % service)
@ -191,8 +177,7 @@ def service_start(service, **kwargs):
def service_stop(service, **kwargs):
"""Stop a service.
:param service: The service name.
:type service: str
- name (str): The service name.
"""
kwargs.setdefault("comment", "stop %s service" % service)
@ -204,8 +189,7 @@ def service_stop(service, **kwargs):
def system(op, **kwargs):
"""Perform a system operation.
:param op: The operation to perform; ``reboot``, ``update``, ``upgrade``.
:type op: str
- op (str): The operation to perform; reboot, update, upgrade.
"""
if op == "reboot":
@ -221,8 +205,7 @@ def system(op, **kwargs):
def system_install(package, **kwargs):
"""Install a system-level package.
:param package: The name of the package to install.
:type package: str
- name (str): The name of the package to install.
"""
kwargs.setdefault("comment", "install system package %s" % package)
@ -231,7 +214,6 @@ def system_install(package, **kwargs):
def system_reboot(**kwargs):
"""Reboot the system."""
kwargs.setdefault("comment", "reboot the system")
return Command("reboot", **kwargs)
@ -240,8 +222,7 @@ def system_reboot(**kwargs):
def system_uninstall(package, **kwargs):
"""Uninstall a system-level package.
:param package: The name of the package to remove.
:type package: str
- name (str): The name of the package to uninstall.
"""
kwargs.setdefault("comment", "remove system package %s" % package)
@ -250,14 +231,12 @@ def system_uninstall(package, **kwargs):
def system_update(**kwargs):
"""Update the system's package info."""
kwargs.setdefault("comment", "update system package info")
return Command("apt update -y", **kwargs)
def system_upgrade(**kwargs):
"""updated the system."""
kwargs.setdefault("comment", "upgrade the system")
return Command("apt upgrade -y", **kwargs)
@ -266,20 +245,11 @@ def system_upgrade(**kwargs):
def user(name, groups=None, home=None, op="add", password=None, **kwargs):
"""Create or remove a user.
:param name: The username.
:type name: str
:param groups: A list of groups to which the user should belong.
:type groups: list | str
:param home: The path to the user's home directory.
:type home: str
:param op: The operation to perform; ``add`` or ``remove``.
:type op:
:param password: The user's password. (NOT IMPLEMENTED)
:type password: str
- name (str): The user name.
- groups (str | list): A list of groups to which the user should belong.
- home (str): The path to the user's home directory.
- op (str); The operation to perform; ``add`` or ``remove``.
- password (str): The user's password. (NOT IMPLEMENTED)
"""
if op == "add":

@ -66,7 +66,7 @@ def command_factory(loader, excluded_kwargs=None, mappings=None, profile=PROFILE
command.number = number
if command.name == "template":
command.context.update(loader.get_context())
command.context = loader.get_context()
commands.append(command)

@ -125,7 +125,8 @@ class BaseLoader(File):
:param path: The path to the command file.
:type path: str
:param context: Global context that may be used to parse the command file and templates.
:param context: Global context that may be used when to parse the command file, snippets, and templates. This is
converted to a ``dict`` when passed to a Snippet or Template.
:type context: scripttease.lib.contexts.Context
:param locations: A list of paths where templates and other external files may be found. The ``templates/``

@ -61,7 +61,6 @@ class INILoader(BaseLoader):
# by double quotes.
if command_name in ("explain", "screenshot"):
args.append(value)
count += 1
continue
# Arguments surrounded by quotes are considered to be one argument. All others are split into a

@ -20,11 +20,11 @@ setup(
author='Shawn Davis',
author_email='shawn@develmaycare.com',
url='https://develmaycare.com/products/python/scripttease/',
download_url='https://gittraction.com/diff6/python-scripttease',
download_url='https://github.com/develmaycare/python-scripttease',
project_urls={
'Documentation': "https://docs.diff6.com/en/python-scripttease/latest/",
'Source': "https://gittraction.com/diff6/python-scripttease",
'Tracker': "https://gittraction.com/diff6/python-scripttease/issues"
'Documentation': "https://docs.develmaycare.com/en/python-scripttease/latest/",
'Source': "https://github.com/develmaycare/python-scripttease",
'Tracker': "https://github.com/develmaycare/python-scripttease/issues/"
},
packages=find_packages(exclude=["tests", "tests.*"]),
include_package_data=True,

@ -1,9 +0,0 @@
[create the deploy user]
user: deploy
groups: sudo, www-data
home: /var/www
sudo: yes
[remove a user]
user: bob
op: remove

@ -1,9 +0,0 @@
from scripttease.lib.factories import command_factory
from scripttease.lib.loaders import INILoader
def test_user_commands():
ini = INILoader("tests/examples/users.ini")
ini.load()
commands = command_factory(ini)
print(commands)

@ -255,22 +255,6 @@ class TestSudo(object):
class TestTemplate(object):
def test_getattr(self):
context = {
'testing': "yes",
'times': 123,
}
t = Template(
"tests/examples/templates/simple.txt",
"tests/tmp/simple.txt",
backup=False,
# context=context,
parser=Template.PARSER_SIMPLE,
**context
)
assert t.testing == "yes"
assert t.times == 123
def test_get_content(self):
context = {
'testing': "yes",
@ -280,9 +264,8 @@ class TestTemplate(object):
"tests/examples/templates/simple.txt",
"tests/tmp/simple.txt",
backup=False,
# context=context,
parser=Template.PARSER_SIMPLE,
**context
context=context,
parser=Template.PARSER_SIMPLE
)
content = t.get_content()
assert "I am testing? yes" in content
@ -296,9 +279,8 @@ class TestTemplate(object):
"tests/examples/templates/simple.sh.txt",
"tests/tmp/simple.sh",
backup=False,
# context=context,
parser=Template.PARSER_SIMPLE,
**context
context=context,
parser=Template.PARSER_SIMPLE
)
content = t.get_content()
assert "I am testing? yes" in content
@ -312,8 +294,7 @@ class TestTemplate(object):
"tests/examples/templates/good.j2.txt",
"tests/tmp/good.txt",
backup=False,
# context=context
**context
context=context
)
content = t.get_content()
assert "I am testing? yes" in content
@ -329,13 +310,7 @@ class TestTemplate(object):
'testing': True,
'times': 3,
}
t = Template(
"tests/examples/templates/settings.py",
"test/tmp/settings.py",
# context=context,
parser=Template.PARSER_PYTHON,
**context
)
t = Template("tests/examples/templates/settings.py", "test/tmp/settings.py", context=context, parser=Template.PARSER_PYTHON)
content = t.get_content()
assert "TESTING = True" in content
assert "TOTAL_TIMES = 3" in content
@ -348,13 +323,12 @@ class TestTemplate(object):
t = Template(
"tests/examples/templates/simple.txt",
"tests/tmp/simple.txt",
# context=context,
context=context,
comment="A simple parser example.",
parser=Template.PARSER_SIMPLE,
register="template_created",
stop=True,
sudo=Sudo(user="root"),
**context
sudo=Sudo(user="root")
)
s = t.get_statement()
assert "I am testing? yes" in s
@ -367,11 +341,10 @@ class TestTemplate(object):
t = Template(
"tests/examples/templates/simple.sh.txt",
"tests/tmp/simple.txt",
# context=context,
context=context,
parser=Template.PARSER_SIMPLE,
stop=True,
sudo="root",
**context
sudo="root"
)
s = t.get_statement()
assert "I am testing? yes" in s
@ -384,9 +357,8 @@ class TestTemplate(object):
t = Template(
"tests/examples/templates/good.j2.txt",
"tests/tmp/good.txt",
# context=context,
sudo=True,
**context
context=context,
sudo=True
)
s = t.get_statement()
assert "I am testing? yes" in s

@ -35,15 +35,6 @@ def test_django_collect_static():
assert "source python/bin/activate" in s
def test_django_createsuperuser():
c = django_createsuperuser("root", email="root@example.com")
s = c.get_statement()
assert "./manage.py createsuperuser" in s
assert '--username="root"' in s
assert '--email="root@example.com"' in s
assert '--noinput' in s
def test_django_dumpdata():
c = django_dump("projects")
s = c.get_statement()
@ -51,27 +42,15 @@ def test_django_dumpdata():
assert "projects >" in s
assert '--format="json"' in s
assert "--indent=4" in s
assert "../fixtures/projects/initial.json" in s
c = django_dump("projects.Category")
s = c.get_statement()
assert "./manage.py dumpdata" in s
assert "projects.Category >" in s
assert '--format="json"' in s
assert "--indent=4" in s
assert "../fixtures/projects/category.json" in s
assert "../fixtures/projects.json" in s
def test_django_loaddata():
c = django_load("projects")
s = c.get_statement()
print(s)
assert "./manage.py loaddata" in s
assert "../fixtures/projects/initial.json" in s
c = django_load("projects.Category")
s = c.get_statement()
assert "./manage.py loaddata" in s
assert "../fixtures/projects/category.json" in s
assert "../fixtures/projects.json" in s
def test_django_migrate():

@ -23,23 +23,6 @@ def test_pgsql_exists():
assert "testing_exists" in s
def test_pgsql_grant():
with pytest.raises(InvalidInput):
pgsql_grant("bob")
with pytest.raises(InvalidInput):
pgsql_grant("bob", database="testing")
c = pgsql_grant("bob", database="testing", schema="public")
s = c.get_statement()
assert '--dbname="testing"' in s
assert "GRANT ALL PRIVILEGES ON SCHEMA public TO bob" in s
c = pgsql_grant("bob", database="testing", table="testing")
s = c.get_statement()
assert "GRANT ALL PRIVILEGES ON TABLE testing TO bob" in s
def test_pgsql_drop():
c = pgsql_drop("testing")
s = c.get_statement()

@ -208,7 +208,7 @@ def test_scopy():
def test_sed():
c = replace("/path/to/file.txt", find="testing", sub="123")
c = replace("/path/to/file.txt", find="testing", replace="123")
s = c.get_statement()
assert "sed -i .b" in s
assert "s/testing/123/g" in s

Loading…
Cancel
Save