Updated the CLI interface.

development
Shawn Davis 2 years ago
parent 7a64eeff09
commit 6de3956dc6
  1. 5
      Makefile
  2. 2
      RELEASE.txt
  3. 75
      docs/source/reference.rst
  4. 114
      help/docs/commands/django.md
  5. 35
      help/docs/index.md
  6. 17
      help/docs/profiles/centos.md
  7. 17
      help/docs/profiles/ubuntu.md
  8. 25
      help/mkdocs.yml
  9. 8
      meta.ini
  10. 382
      sandbox/cli.py
  11. 7
      sandbox/tease.py
  12. 540
      scripttease/cli/__init__.py
  13. 456
      scripttease/cli/initialize.py
  14. 210
      scripttease/cli/subcommands.py
  15. 6
      scripttease/data/inventory/nextcloud/meta.ini
  16. 6
      scripttease/data/inventory/pgsql/meta.ini
  17. 8
      scripttease/data/inventory/radicale/steps.ini
  18. 6
      scripttease/data/inventory/ubuntu/meta.ini
  19. 17
      scripttease/lib/commands/base.py
  20. 82
      scripttease/lib/commands/django.py
  21. 1
      scripttease/lib/commands/posix.py
  22. 2
      scripttease/lib/commands/ubuntu.py
  23. 2
      scripttease/lib/factories.py
  24. 1
      scripttease/lib/loaders/ini.py
  25. 6
      setup.py
  26. 9
      tests/examples/users.ini
  27. 9
      tests/test_lib_commands.py
  28. 52
      tests/test_lib_commands_base.py
  29. 27
      tests/test_lib_commands_django.py

@ -9,6 +9,11 @@ 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 `cat RELEASE.txt`)
VERSION := $(strip `cat VERSION.txt`)
# Attempt to load a local makefile which may override any of the values above.
-include local.makefile

@ -1 +1 @@
7.0.0-a
7.0.0-a

@ -18,18 +18,15 @@ Library
Commands
--------
.. automodule:: scripttease.library.commands.base
.. automodule:: scripttease.lib.commands.base
:members:
:show-inheritance:
:special-members: __init__
Overlays
--------
Common
Centos
......
.. automodule:: scripttease.library.overlays.common
.. automodule:: scripttease.lib.commands.centos
:members:
:show-inheritance:
:special-members: __init__
@ -37,7 +34,31 @@ Common
Django
......
.. automodule:: scripttease.library.overlays.django
.. automodule:: scripttease.lib.commands.django
:members:
:show-inheritance:
:special-members: __init__
Messages
........
.. automodule:: scripttease.lib.commands.messages
:members:
:show-inheritance:
:special-members: __init__
MySQL
......
.. automodule:: scripttease.lib.commands.mysql
:members:
:show-inheritance:
:special-members: __init__
PHP
...
.. automodule:: scripttease.lib.commands.php
:members:
:show-inheritance:
:special-members: __init__
@ -45,7 +66,7 @@ Django
Postgres
........
.. automodule:: scripttease.library.overlays.pgsql
.. automodule:: scripttease.lib.commands.pgsql
:members:
:show-inheritance:
:special-members: __init__
@ -53,7 +74,15 @@ Postgres
Posix
.....
.. automodule:: scripttease.library.overlays.posix
.. automodule:: scripttease.lib.commands.posix
:members:
:show-inheritance:
:special-members: __init__
Python
......
.. automodule:: scripttease.lib.commands.python
:members:
:show-inheritance:
:special-members: __init__
@ -61,34 +90,34 @@ Posix
Ubuntu
......
.. automodule:: scripttease.library.overlays.ubuntu
.. automodule:: scripttease.lib.commands.ubuntu
:members:
:show-inheritance:
:special-members: __init__
Scripts
-------
Contexts
========
.. automodule:: scripttease.library.scripts
.. automodule:: scripttease.contexts
:members:
:show-inheritance:
:special-members: __init__
Factory
=======
Factories
=========
.. automodule:: scripttease.factory
.. automodule:: scripttease.factories
:members:
:show-inheritance:
:special-members: __init__
Parsers
Loaders
=======
Base
----
.. automodule:: scripttease.parsers.base
.. automodule:: scripttease.lib.loaders.base
:members:
:show-inheritance:
:special-members: __init__
@ -96,15 +125,7 @@ Base
Config (INI)
------------
.. automodule:: scripttease.parsers.ini
:members:
:show-inheritance:
:special-members: __init__
Utils
-----
.. automodule:: scripttease.parsers.utils
.. automodule:: scripttease.lib.loaders.ini
:members:
:show-inheritance:
:special-members: __init__

@ -4,29 +4,31 @@ 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.
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
```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
```ini
[run database migrations]
django.migrate:
settings: tenants.example_com.settings
[dump some data]
django.dump: path/to/dump.json
indent: 4
natural_foreign: yes
natural_primary: yes
- dump some data:
django: dumpdata
indent: 4
natural_foreign: yes
natural_primary: yes
```
## Available Commands
@ -35,66 +37,77 @@ Options provided in the command configuration file are automatically converted t
```ini
[run django checks]
django: check
django.check:
stop: yes
```
### collectstatic
Alias: static
Collect static files.
```ini
[collect static files]
django.static:
```
```yaml
- run django checks:
django: check
### createsuperuser
Create a superuser account.
```ini
[create the root user account]
django.createsuperuser: root
email: root@example.com
```
### dumpdata
Alias: dump
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`.
- 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: dumpdata
app: projects
django.dump: projects
[dump project categories]
django: dumpdata
app: projects
model: Category
django.dump: projects.Category
path: local/projects/fixtures/default-categories.json
```
### loaddata
Load fixture data.
Alias: load
- 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
Load fixture data.
Run database migrations.
- 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
[run database migrations]
django: migrate
```
[load project categories]
django.load: projects
path: local/projects/fixtures/default-categories.json
```yaml
- run database migrations:
django: migrate
```
### static
### migrate
Collect static files.
Run database migrations.
```ini
[collect static files]
django: static
```
```yaml
- collect static files:
django: static
[run database migrations]
django.migrate:
stop: yes
```
## Custom or Ad Hoc Commands
@ -108,11 +121,4 @@ 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
```

@ -6,14 +6,18 @@ Script Tease is a library and command line tool for generating Bash commands pro
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 untested.
## Concepts
### Command Generation
Script Tease may be used in two (2) ways:
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 [command file configuration](config/command-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 may be used in your own implementation.
@ -21,29 +25,30 @@ This documentation focuses on the second method, but the developer docs may be u
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.
```ini
[install apache]
install: apache2
!!! 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.
```yaml
- install apache
install: apache2
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.
### Representing Commands
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:
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.
- 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`.
#### Profiles
The first approach complicates things when detecting actual sub-commands (below). Script Tease supports both of these approaches.
Profiles contain command functions that are specific to an operating system. Not all operating systems support the same commands.
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.
!!! note
At present, the only fully defined operating systems are for Cent OS and Ubuntu.
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.
Profiles import and appropriate all other available commands.
## Terms and Definitions

@ -76,11 +76,11 @@ stop: postgresql
### system
With with the system.
Work with the system.
- `system.reboot`
- `system.update`
- `system.upgrade`
- `reboot`
- `update`
- `upgrade`
### uninstall
@ -111,14 +111,17 @@ Create a user:
```ini
[create the deploy user]
user.add: deploy
groups: www-data
user: deploy
groups: sudo, www-data
home: /var/www
sudo: yes
```
Remove a user:
```ini
[remove bob]
user.remove: bob
user: bob
op: remove
sudo: yes
```

@ -76,11 +76,11 @@ stop: postgresql
### system
With with the system.
Work with the system.
- `system.reboot`
- `system.update`
- `system.upgrade`
- `reboot`
- `update`
- `upgrade`
### uninstall
@ -111,14 +111,17 @@ Create a user:
```ini
[create the deploy user]
user.add: deploy
groups: www-data
user: deploy
groups: sudo, www-data
home: /var/www
sudo: yes
```
Remove a user:
```ini
[remove bob]
user.remove: bob
user: bob
op: remove
sudo: yes
```

@ -6,6 +6,7 @@ markdown_extensions:
- admonition
- attr_list
- def_list
- pymdownx.superfences
nav:
- Home: index.md
- Configuration:
@ -13,14 +14,30 @@ nav:
- Variables: config/variables.md
- Commands:
- Django: commands/django.md
- Messages: commands/messages.md
- MySQL: commands/mysql.md
- Messages: commands/messages.md
- PHP: commands/php.md
- Postgres: commands/pgsql.md
- POSIX: commands/posix.md
- Python: commands/python.md
- Profiles:
- CentOS: profiles/centos.md
- Ubuntu: profiles/ubuntu.md
# - Developer Reference: /developers/
repo_name: GitLab
repo_url: https://git.sixgunsoftware.com/python-scripttease
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

@ -1,9 +1,15 @@
[project]
category = developer
description = A collection of classes and commands for automated command line scripting using Pythonn.
description = A collection of classes and commands for automated command line scripting using Python.
icon = fas fa-scroll
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

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

@ -1,22 +1,15 @@
# Imports
from argparse import ArgumentParser, RawDescriptionHelpFormatter
from commonkit.logging import LoggingHelper
from ..variables import LOGGER_NAME, PATH_TO_SCRIPT_TEASE
from commonkit.shell import EXIT
from ..lib.contexts import Context
from ..lib.loaders import load_variables
from ..variables import LOGGER_NAME
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)
@ -25,7 +18,7 @@ log = logging.setup()
# Commands
def execute():
def main_command():
"""Process script configurations."""
__author__ = "Shawn Davis <shawn@develmaycare.com>"
@ -40,99 +33,14 @@ def execute():
# 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"
# Initialize sub-commands.
subparsers = parser.add_subparsers(
dest="subcommand",
help="Commands",
metavar="docs, inventory, script"
)
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."
)
initialize.subcommands(subparsers)
# Access to the version number requires special consideration, especially
# when using sub parsers. The Python 3.3 behavior is different. See this
@ -158,404 +66,54 @@ def execute():
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
# Load resources for docs and script output.
if args.subcommand in ["docs", "script"]:
# Create the global context.
context = Context()
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)
if args.variables_file:
variables = load_variables(args.variables_file)
for v in variables:
context.variables[v.name] = v
# Capture filters.
if args.filters:
filters = dict()
for token in args.filters:
key, value = token.split(":")
if key not in filters:
filters[key] = list()
if args.variables:
initialize.variables_from_cli(context, args.variables)
filters[key].append(value)
# Handle global command options.
options = dict()
if args.options:
options = initialize.options_from_cli(args.options)
# 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,
loader = initialize.loader(
args.command_file,
context=context,
locations=args.template_locations,
profile=args.profile,
**options
options=options,
template_locations=args.template_locations
)
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)
# 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
if loader is None:
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.script_enabled:
exit_code = subcommands.output_script(
args.path,
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,
color_enabled=args.color_enabled,
context=context,
locations=args.template_locations,
options=options
include_shebang=args.include_shebang,
output_file=args.output_file
)
else:
exit_code = subcommands.output_commands(
args.path,
color_enabled=args.color_enabled,
context=context,
filters=filters,
locations=args.template_locations,
options=options
)
print("Unrecognized command: %s" % args.subcommand)
exit_code = EXIT.USAGE
exit(exit_code)

@ -1,126 +1,374 @@
# Imports
import logging
from commonkit import smart_cast
from configparser import ConfigParser
from commonkit.shell import EXIT
import logging
import os
from ..constants import LOGGER_NAME
from ..lib.loaders import load_variables, INILoader, YMLLoader
from ..variables import LOGGER_NAME, PATH_TO_SCRIPT_TEASE
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)
for token in options:
try:
key, value = token.split(":")
_options[key] = smart_cast(value)
except ValueError:
_options[token] = True
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:
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
.. code-block:: ini
# 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
[copyright]
name = ACME, Inc.
year = 2020
# Load the commands.
if not _loader.load():
log.error("Failed to load the input file: %s" % path)
return None
[domain]
name = example.com
tld = example_com
return _loader
The dictionary would contain:
.. code-block:: python
def subcommands(subparsers):
"""Initialize sub-commands.
{
'copyright_name': "ACME, Inc.",
'copyright_year': 2020,
'domain_name': "example.com",
'domain_tld': "example_com",
}
:param subparsers: The subparsers instance from argparse.
"""
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
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="commands.ini",
dest="command_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

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

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

@ -0,0 +1,6 @@
[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]
mkdir: /var/www/maint/www
dir: /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]
pip3: radicale
pip: radicale
[install radicale screenshot]
screenshot: images/install.png
caption: Radical Installed
[create radicale configuration directory]
mkdir: /etc/radicale/config
dir: /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.add: radicale
user: radicale
home: /
login: /sbin/nologin
system: yes

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

@ -228,12 +228,16 @@ 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):
@ -566,12 +570,19 @@ class Template(object):
def __init__(self, source, target, backup=True, parser=PARSER_JINJA, **kwargs):
self.backup_enabled = backup
self.context = kwargs.pop("context", dict())
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.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)
@ -584,10 +595,10 @@ class Template(object):
else:
self.sudo = Sudo()
self.kwargs = kwargs
self.context = kwargs
def __getattr__(self, item):
return self.kwargs.get(item)
return self.context.get(item)
# def __str__(self):
# return "template"

@ -23,6 +23,27 @@ from .base import Command
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
:param excluded_kwargs: A dictionary of kwargs that should be excluded from the management command parameters.
:param excluded_kwargs: dict
:rtype: scripttease.lib.commands.base.Command
If provided, args are passed directly to the command as positional parameters.
Any provided kwargs are converted to long form parameters. For example, database="alternative_db" becomes
``--database="alternative_db"``.
A kwarg with a ``True`` value becomes a long form parameter with no value. For example, natural_foreign=True becomes
``--natural-foreign``.
Finally, any kwarg that is not a string is passed without quotes. For example, testing=1 becomes ``--testing=1``.
"""
# The excluded parameters (filtered below) may vary based on implementation. We do, however, need a default.
excluded_kwargs = excluded_kwargs or EXCLUDED_KWARGS
@ -61,37 +82,92 @@ 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.%s" % (target, kwargs['format'])
path = "../fixtures/%s" % file_name
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.%s" % (target, input_format)
path = "../fixtures/%s" % file_name
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)
@ -100,6 +176,8 @@ 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,

@ -610,6 +610,7 @@ POSIX_MAPPINGS = {
'run': run,
'rsync': rsync,
'scopy': scopy,
'ssl': certbot,
'sync': sync,
'touch': touch,
'wait': wait,

@ -1,7 +1,7 @@
# Imports
from commonkit import split_csv
from .base import Command, Template
from .base import Command
from .django import DJANGO_MAPPINGS
from .messages import MESSAGE_MAPPINGS
from .mysql import MYSQL_MAPPINGS

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

@ -61,6 +61,7 @@ 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://github.com/develmaycare/python-scripttease',
download_url='https://gittraction.com/diff6/python-scripttease',
project_urls={
'Documentation': "https://docs.develmaycare.com/en/python-scripttease/latest/",
'Source': "https://github.com/develmaycare/python-scripttease",
'Tracker': "https://github.com/develmaycare/python-scripttease/issues/"
'Source': "https://gittraction.com/diff6/python-scripttease",
'Tracker': "https://gittraction.com/diff6/python-scripttease/issues"
},
packages=find_packages(exclude=["tests", "tests.*"]),
include_package_data=True,

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

@ -0,0 +1,9 @@
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,6 +255,22 @@ 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",
@ -264,8 +280,9 @@ class TestTemplate(object):
"tests/examples/templates/simple.txt",
"tests/tmp/simple.txt",
backup=False,
context=context,
parser=Template.PARSER_SIMPLE
# context=context,
parser=Template.PARSER_SIMPLE,
**context
)
content = t.get_content()
assert "I am testing? yes" in content
@ -279,8 +296,9 @@ class TestTemplate(object):
"tests/examples/templates/simple.sh.txt",
"tests/tmp/simple.sh",
backup=False,
context=context,
parser=Template.PARSER_SIMPLE
# context=context,
parser=Template.PARSER_SIMPLE,
**context
)
content = t.get_content()
assert "I am testing? yes" in content
@ -294,7 +312,8 @@ 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
@ -310,7 +329,13 @@ class TestTemplate(object):
'testing': True,
'times': 3,
}
t = Template("tests/examples/templates/settings.py", "test/tmp/settings.py", context=context, parser=Template.PARSER_PYTHON)
t = Template(
"tests/examples/templates/settings.py",
"test/tmp/settings.py",
# context=context,
parser=Template.PARSER_PYTHON,
**context
)
content = t.get_content()
assert "TESTING = True" in content
assert "TOTAL_TIMES = 3" in content
@ -323,12 +348,13 @@ 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")
sudo=Sudo(user="root"),
**context
)
s = t.get_statement()
assert "I am testing? yes" in s
@ -341,10 +367,11 @@ 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"
sudo="root",
**context
)
s = t.get_statement()
assert "I am testing? yes" in s
@ -357,8 +384,9 @@ class TestTemplate(object):
t = Template(
"tests/examples/templates/good.j2.txt",
"tests/tmp/good.txt",
context=context,
sudo=True
# context=context,
sudo=True,
**context
)
s = t.get_statement()
assert "I am testing? yes" in s

@ -35,6 +35,15 @@ 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()
@ -42,15 +51,27 @@ def test_django_dumpdata():
assert "projects >" in s
assert '--format="json"' in s
assert "--indent=4" in s
assert "../fixtures/projects.json" 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
def test_django_loaddata():
c = django_load("projects")
s = c.get_statement()
print(s)
assert "./manage.py loaddata" in s
assert "../fixtures/projects.json" 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
def test_django_migrate():

Loading…
Cancel
Save