From df98fb3f4dc168ebf19eec1e82772e0c988665d2 Mon Sep 17 00:00:00 2001 From: Todd Leonhardt Date: Mon, 23 Dec 2024 00:04:55 -0500 Subject: [PATCH] Convert from Sphinx and reStructuredText to MkDocs and Markdown for documentation TODO: 1. Finish correcting translation - I stopped at features/disable_commands 2. Get all the autodoc API stuff working properly 3. Get ReadTheDocs working properly --- .github/CODEOWNERS | 2 +- .github/CONTRIBUTING.md | 100 ++-- .github/workflows/doc.yml | 16 +- .gitignore | 1 + .readthedocs.yaml | 16 +- Pipfile | 6 +- docs/.gitignore | 2 - docs/Makefile | 89 ---- docs/api/ansi.md | 7 + docs/api/ansi.rst | 5 - docs/api/argparse_completer.md | 7 + docs/api/argparse_completer.rst | 5 - docs/api/argparse_custom.md | 63 +++ docs/api/argparse_custom.rst | 35 -- docs/api/cmd.md | 86 ++++ docs/api/cmd.rst | 79 ---- docs/api/command_definition.md | 7 + docs/api/command_definition.rst | 5 - docs/api/constants.md | 26 + docs/api/constants.rst | 29 -- docs/api/decorators.md | 3 + docs/api/decorators.rst | 5 - docs/api/exceptions.md | 25 + docs/api/exceptions.rst | 20 - docs/api/history.md | 25 + docs/api/history.rst | 20 - docs/api/index.md | 29 ++ docs/api/index.rst | 62 --- docs/api/parsing.md | 83 ++++ docs/api/parsing.rst | 78 --- docs/api/plugin.md | 61 +++ docs/api/plugin.rst | 45 -- docs/api/plugin_external_test.md | 9 + docs/api/plugin_external_test.rst | 9 - docs/api/py_bridge.md | 7 + docs/api/py_bridge.rst | 5 - docs/api/table_creator.md | 46 ++ docs/api/table_creator.rst | 35 -- docs/api/utils.md | 145 ++++++ docs/api/utils.rst | 102 ---- docs/conf.py | 196 -------- docs/doc_conventions.md | 59 +++ docs/doc_conventions.rst | 228 --------- docs/examples/alternate_event_loops.md | 57 +++ docs/examples/alternate_event_loops.rst | 78 --- docs/examples/first_app.md | 227 +++++++++ docs/examples/first_app.rst | 323 ------------- docs/examples/index.md | 8 + docs/examples/index.rst | 8 - docs/features/argument_processing.md | 303 ++++++++++++ docs/features/argument_processing.rst | 377 --------------- docs/features/builtin_commands.md | 111 +++++ docs/features/builtin_commands.rst | 161 ------- docs/features/clipboard.md | 27 ++ docs/features/clipboard.rst | 41 -- docs/features/commands.md | 155 ++++++ docs/features/commands.rst | 224 --------- docs/features/completion.md | 85 ++++ docs/features/completion.rst | 151 ------ docs/features/disable_commands.md | 74 +++ docs/features/disable_commands.rst | 103 ---- docs/features/embedded_python_shells.md | 55 +++ docs/features/embedded_python_shells.rst | 93 ---- docs/features/generating_output.md | 79 ++++ docs/features/generating_output.rst | 160 ------- docs/features/help.md | 163 +++++++ docs/features/help.rst | 193 -------- docs/features/history.md | 168 +++++++ docs/features/history.rst | 269 ----------- docs/features/hooks.md | 211 +++++++++ docs/features/hooks.rst | 340 -------------- docs/features/index.md | 32 ++ docs/features/index.rst | 32 -- docs/features/initialization.md | 120 +++++ docs/features/initialization.rst | 166 ------- docs/features/misc.md | 73 +++ docs/features/misc.rst | 101 ---- docs/features/modular_commands.md | 305 ++++++++++++ docs/features/modular_commands.rst | 359 -------------- docs/features/multiline_commands.md | 18 + docs/features/multiline_commands.rst | 36 -- docs/features/os.md | 125 +++++ docs/features/os.rst | 177 ------- docs/features/packaging.md | 29 ++ docs/features/packaging.rst | 39 -- docs/features/plugins.md | 104 ++++ docs/features/plugins.rst | 159 ------- docs/features/prompt.md | 42 ++ docs/features/prompt.rst | 72 --- docs/features/redirection.md | 55 +++ docs/features/redirection.rst | 80 ---- docs/features/scripting.md | 403 ++++++++++++++++ docs/features/scripting.rst | 523 --------------------- docs/features/settings.md | 116 +++++ docs/features/settings.rst | 188 -------- docs/features/shortcuts_aliases_macros.md | 91 ++++ docs/features/shortcuts_aliases_macros.rst | 117 ----- docs/features/startup_commands.md | 46 ++ docs/features/startup_commands.rst | 75 --- docs/features/table_creation.md | 17 + docs/features/table_creation.rst | 40 -- docs/features/transcripts.md | 159 +++++++ docs/features/transcripts.rst | 202 -------- docs/index.md | 68 +++ docs/index.rst | 107 ----- docs/make.bat | 113 ----- docs/migrating/incompatibilities.md | 21 + docs/migrating/incompatibilities.rst | 61 --- docs/migrating/index.md | 8 + docs/migrating/index.rst | 14 - docs/migrating/minimum.md | 41 ++ docs/migrating/minimum.rst | 51 -- docs/migrating/next_steps.md | 25 + docs/migrating/next_steps.rst | 61 --- docs/migrating/summary.rst | 15 - docs/migrating/why.md | 30 ++ docs/migrating/why.rst | 82 ---- docs/overview/alternatives.md | 16 + docs/overview/alternatives.rst | 54 --- docs/overview/index.md | 13 + docs/overview/index.rst | 15 - docs/overview/installation.md | 111 +++++ docs/overview/installation.rst | 162 ------- docs/overview/integrating.md | 15 + docs/overview/integrating.rst | 35 -- docs/overview/resources.md | 8 + docs/overview/resources.rst | 13 - docs/overview/summary.rst | 22 - docs/plugins/external_test.md | 62 +++ docs/plugins/external_test.rst | 82 ---- docs/plugins/index.md | 7 + docs/plugins/index.rst | 7 - docs/requirements.txt | 7 +- docs/testing.md | 21 + docs/testing.rst | 70 --- mkdocs.yml | 191 ++++++++ pyproject.toml | 128 ++--- tasks.py | 35 +- 138 files changed, 4901 insertions(+), 7098 deletions(-) delete mode 100644 docs/.gitignore delete mode 100644 docs/Makefile create mode 100644 docs/api/ansi.md delete mode 100644 docs/api/ansi.rst create mode 100644 docs/api/argparse_completer.md delete mode 100644 docs/api/argparse_completer.rst create mode 100644 docs/api/argparse_custom.md delete mode 100644 docs/api/argparse_custom.rst create mode 100644 docs/api/cmd.md delete mode 100644 docs/api/cmd.rst create mode 100644 docs/api/command_definition.md delete mode 100644 docs/api/command_definition.rst create mode 100644 docs/api/constants.md delete mode 100644 docs/api/constants.rst create mode 100644 docs/api/decorators.md delete mode 100644 docs/api/decorators.rst create mode 100644 docs/api/exceptions.md delete mode 100644 docs/api/exceptions.rst create mode 100644 docs/api/history.md delete mode 100644 docs/api/history.rst create mode 100644 docs/api/index.md delete mode 100644 docs/api/index.rst create mode 100644 docs/api/parsing.md delete mode 100644 docs/api/parsing.rst create mode 100644 docs/api/plugin.md delete mode 100644 docs/api/plugin.rst create mode 100644 docs/api/plugin_external_test.md delete mode 100644 docs/api/plugin_external_test.rst create mode 100644 docs/api/py_bridge.md delete mode 100644 docs/api/py_bridge.rst create mode 100644 docs/api/table_creator.md delete mode 100644 docs/api/table_creator.rst create mode 100644 docs/api/utils.md delete mode 100644 docs/api/utils.rst delete mode 100644 docs/conf.py create mode 100644 docs/doc_conventions.md delete mode 100644 docs/doc_conventions.rst create mode 100644 docs/examples/alternate_event_loops.md delete mode 100644 docs/examples/alternate_event_loops.rst create mode 100644 docs/examples/first_app.md delete mode 100644 docs/examples/first_app.rst create mode 100644 docs/examples/index.md delete mode 100644 docs/examples/index.rst create mode 100644 docs/features/argument_processing.md delete mode 100644 docs/features/argument_processing.rst create mode 100644 docs/features/builtin_commands.md delete mode 100644 docs/features/builtin_commands.rst create mode 100644 docs/features/clipboard.md delete mode 100644 docs/features/clipboard.rst create mode 100644 docs/features/commands.md delete mode 100644 docs/features/commands.rst create mode 100644 docs/features/completion.md delete mode 100644 docs/features/completion.rst create mode 100644 docs/features/disable_commands.md delete mode 100644 docs/features/disable_commands.rst create mode 100644 docs/features/embedded_python_shells.md delete mode 100644 docs/features/embedded_python_shells.rst create mode 100644 docs/features/generating_output.md delete mode 100644 docs/features/generating_output.rst create mode 100644 docs/features/help.md delete mode 100644 docs/features/help.rst create mode 100644 docs/features/history.md delete mode 100644 docs/features/history.rst create mode 100644 docs/features/hooks.md delete mode 100644 docs/features/hooks.rst create mode 100644 docs/features/index.md delete mode 100644 docs/features/index.rst create mode 100644 docs/features/initialization.md delete mode 100644 docs/features/initialization.rst create mode 100644 docs/features/misc.md delete mode 100644 docs/features/misc.rst create mode 100644 docs/features/modular_commands.md delete mode 100644 docs/features/modular_commands.rst create mode 100644 docs/features/multiline_commands.md delete mode 100644 docs/features/multiline_commands.rst create mode 100644 docs/features/os.md delete mode 100644 docs/features/os.rst create mode 100644 docs/features/packaging.md delete mode 100644 docs/features/packaging.rst create mode 100644 docs/features/plugins.md delete mode 100644 docs/features/plugins.rst create mode 100644 docs/features/prompt.md delete mode 100644 docs/features/prompt.rst create mode 100644 docs/features/redirection.md delete mode 100644 docs/features/redirection.rst create mode 100644 docs/features/scripting.md delete mode 100644 docs/features/scripting.rst create mode 100644 docs/features/settings.md delete mode 100644 docs/features/settings.rst create mode 100644 docs/features/shortcuts_aliases_macros.md delete mode 100644 docs/features/shortcuts_aliases_macros.rst create mode 100644 docs/features/startup_commands.md delete mode 100644 docs/features/startup_commands.rst create mode 100644 docs/features/table_creation.md delete mode 100644 docs/features/table_creation.rst create mode 100644 docs/features/transcripts.md delete mode 100644 docs/features/transcripts.rst create mode 100644 docs/index.md delete mode 100644 docs/index.rst delete mode 100755 docs/make.bat create mode 100644 docs/migrating/incompatibilities.md delete mode 100644 docs/migrating/incompatibilities.rst create mode 100644 docs/migrating/index.md delete mode 100644 docs/migrating/index.rst create mode 100644 docs/migrating/minimum.md delete mode 100644 docs/migrating/minimum.rst create mode 100644 docs/migrating/next_steps.md delete mode 100644 docs/migrating/next_steps.rst delete mode 100644 docs/migrating/summary.rst create mode 100644 docs/migrating/why.md delete mode 100644 docs/migrating/why.rst create mode 100644 docs/overview/alternatives.md delete mode 100644 docs/overview/alternatives.rst create mode 100644 docs/overview/index.md delete mode 100644 docs/overview/index.rst create mode 100644 docs/overview/installation.md delete mode 100644 docs/overview/installation.rst create mode 100644 docs/overview/integrating.md delete mode 100644 docs/overview/integrating.rst create mode 100644 docs/overview/resources.md delete mode 100644 docs/overview/resources.rst delete mode 100644 docs/overview/summary.rst create mode 100644 docs/plugins/external_test.md delete mode 100644 docs/plugins/external_test.rst create mode 100644 docs/plugins/index.md delete mode 100644 docs/plugins/index.rst create mode 100644 docs/testing.md delete mode 100644 docs/testing.rst create mode 100644 mkdocs.yml diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 1693b82ca..252703bb5 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -41,7 +41,7 @@ cmd2/table_creator.py @kmvanbrunt cmd2/transcript.py @kotfu cmd2/utils.py @tleonhardt @kotfu @kmvanbrunt -# Sphinx documentation +# Documentation docs/* @tleonhardt @kotfu # Examples diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5abfbec74..2167755f4 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -11,12 +11,12 @@ We welcome pull requests from cmd2 users and seasoned Python developers alike! F Remember to feel free to ask for help by leaving a comment within the Issue. -Working on your first pull request? You can learn how from the +Working on your first pull request? You can learn how from the [GitHub Docs](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request). ###### If you've found a bug that is not on the board, [follow these steps](../README.md#found-a-bug). --------------------------------------------------------------------------------- +--- ## Contribution guidelines @@ -46,16 +46,16 @@ The tables below list all prerequisites along with the minimum required version #### Prerequisites to run cmd2 applications -| Prerequisite | Minimum Version | -|------------------------------------------------------|-----------------| -| [python](https://www.python.org/downloads/) | `3.8` | -| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` | -| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` | +| Prerequisite | Minimum Version | +| --------------------------------------------------- | --------------- | +| [python](https://www.python.org/downloads/) | `3.8` | +| [pyperclip](https://github.com/asweigart/pyperclip) | `1.8.2` | +| [wcwidth](https://pypi.python.org/pypi/wcwidth) | `0.2.12` | #### Additional prerequisites to build and publish cmd2 | Prerequisite | Minimum Version | -|----------------------------------------------------------|-----------------| +| -------------------------------------------------------- | --------------- | | [build](https://pypi.org/project/build/) | `1.2.2` | | [setuptools](https://pypi.org/project/setuptools/) | `72.1.0` | | [setuptools-scm](https://github.com/pypa/setuptools-scm) | `8.0.4` | @@ -63,22 +63,20 @@ The tables below list all prerequisites along with the minimum required version #### Additional prerequisites for developing cmd2 -| Prerequisite | Minimum Version | Purpose | -|----------------------------------------------------------------------|-----------------|----------------------------| -| [codecov](http://doc.pytest.org/en/latest/) | `2.1.13` | Cover coverage reporting | -| [doc8](https://github.com/PyCQA/doc8) | `1.1.2` | Sphinx style checker | -| [invoke](https://www.pyinvoke.org/) | `2.2.0` | Command automation | -| [mypy](https://mypy-lang.org/) | `1.13.0` | Static type checker | -| [pytest](https://docs.pytest.org/en/stable/) | `3.0.6` | Unit and integration tests | -| [pytest-cov](http://doc.pytest.org/en/latest/) | `6.0.0` | Pytest code coverage | -| [pytest-mock](https://pypi.org/project/pytest-mock/) | `3.14.0` | Pytest mocker fixture | -| [sphinx](https://www.sphinx-doc.org/en/master/) | `8.1.3` | Documentation | -| [sphinx-autobuild](hhttps://pypi.org/project/sphinx-autobuild/) | `2024.10.3` | Rebuild docs on changes | -| [sphinx-rtd-theme](https://github.com/readthedocs/sphinx_rtd_theme ) | `3.0.1` | Sphinx theme for RTD | -| [ruff](https://github.com/astral-sh/ruff) | `0.7.3` | Fast linter and formatter | -| [uv](https://github.com/astral-sh/uv) | `0.5.1` | Python package management | - - +| Prerequisite | Minimum Version | Purpose | +| ------------------------------------------------------------------------------------------ | --------------- | --------------------------------- | +| [codecov](http://doc.pytest.org/en/latest/) | `2.1.13` | Cover coverage reporting | +| [invoke](https://www.pyinvoke.org/) | `2.2.0` | Command automation | +| [mypy](https://mypy-lang.org/) | `1.13.0` | Static type checker | +| [pytest](https://docs.pytest.org/en/stable/) | `3.0.6` | Unit and integration tests | +| [pytest-cov](http://doc.pytest.org/en/latest/) | `6.0.0` | Pytest code coverage | +| [pytest-mock](https://pypi.org/project/pytest-mock/) | `3.14.0` | Pytest mocker fixture | +| [mkdocs-include-markdown-plugin](https://pypi.org/project/mkdocs-include-markdown-plugin/) | `7.1.2` | MkDocs Plugin include MkDn | +| [mkdocs-macros-plugin](https://mkdocs-macros-plugin.readthedocs.io/) | `1.3.7` | MkDocs Plugin for macros | +| [mkdocs-material](https://squidfunk.github.io/mkdocs-material/) | `9.5.49` | Documentation | +| [mkdocstrings](https://mkdocstrings.github.io/) | `0.27.0` | Automatic documentation from code | +| [ruff](https://github.com/astral-sh/ruff) | `0.7.3` | Fast linter and formatter | +| [uv](https://github.com/astral-sh/uv) | `0.5.1` | Python package management | If Python is already installed in your machine, run the following commands to validate the versions: @@ -89,11 +87,12 @@ $ pip freeze | grep pyperclip If your versions are lower than the prerequisite versions, you should update. -If you do not already have Python installed on your machine, we recommend using [uv](https://github.com/astral-sh/uv) +If you do not already have Python installed on your machine, we recommend using [uv](https://github.com/astral-sh/uv) for all of your Python needs because it is extremely fast, meets all Python installation and packaging needs, and works -on all platforms (Windows, Mac, and Linux). You can install `uv` using instructions at the link above. +on all platforms (Windows, Mac, and Linux). You can install `uv` using instructions at the link above. You can then install multiple versions of Python using `uv` like so: + ```sh uv python install 3.10 3.11 3.12 3.13 ``` @@ -147,22 +146,22 @@ Do this prior to every time you create a branch for a PR: 1. Make sure you are on the `master` branch > ```sh - > $ git status - > On branch master - > Your branch is up-to-date with 'origin/master'. - > ``` +> $ git status +> On branch master +> Your branch is up-to-date with 'origin/master'. +> ``` > If your aren't on `master`, resolve outstanding files and commits and checkout the `master` branch > ```sh - > $ git checkout master - > ``` +> $ git checkout master +> ``` 2. Do a pull with rebase against `upstream` > ```sh - > $ git pull --rebase upstream master - > ``` +> $ git pull --rebase upstream master +> ``` > This will pull down all of the changes to the official master branch, without making an additional commit in your > local repo. @@ -170,8 +169,8 @@ Do this prior to every time you create a branch for a PR: 3. (_Optional_) Force push your updated master branch to your GitHub fork > ```sh - > $ git push origin master --force - > ``` +> $ git push origin master --force +> ``` > This will overwrite the master branch of your fork. @@ -213,8 +212,8 @@ package from the source. `cmd2` has support for using [uv](https://github.com/astral-sh/uv) for development. -`uv` is single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, and more. `cmd2` -contains configuration for using `uv` in it's `pyproject.toml` file which makes it extremely easy to setup a `cmd2` +`uv` is single tool to replace `pip`, `pip-tools`, `pipx`, `poetry`, `pyenv`, `twine`, `virtualenv`, and more. `cmd2` +contains configuration for using `uv` in it's `pyproject.toml` file which makes it extremely easy to setup a `cmd2` development environment using `uv`. To create a virtual environment and install everything needed for `cmd2` development using `uv`, do the following @@ -238,6 +237,7 @@ uv run examples/basic.py ``` Alternatively you can activate the virtual environment using the OS-specific command such as this on Linux or macOS: + ```sh source .venv/bin/activate ``` @@ -316,7 +316,7 @@ primarily related to continuous integration and release deployment. #### Changes to the documentation files If you made changes to any file in the `/docs` directory, you need to build the -Sphinx documentation and make sure your changes look good: +MkDocs documentation and make sure your changes look good: ```sh $ uv inv docs @@ -337,7 +337,7 @@ served (usually [http://localhost:8000](http://localhost:8000)). ### Code Quality Checks You should have some sort of [PEP 8](https://www.python.org/dev/peps/pep-0008/)-based linting running in your editor or -IDE or at the command line before you commit code. `cmd2` uses [ruff](https://github.com/astral-sh/ruff) as part of +IDE or at the command line before you commit code. `cmd2` uses [ruff](https://github.com/astral-sh/ruff) as part of its continuous integration (CI) process for both linting and auto-formatting. > Please do not ignore any linting errors in code you write or modify, as they are meant to **help** you and to ensure a @@ -440,7 +440,7 @@ nothing to commit, working directory clean any outstanding files/commits and checkout master `git checkout master` 2. Create a branch off of `master` with git: `git checkout -B - branch/name-here` **Note:** Branch naming is important. Use a name like +branch/name-here` **Note:** Branch naming is important. Use a name like `fix/short-fix-description` or `feature/short-feature-description`. Review the [Contribution Guidelines](#contribution-guidelines) for more detail. @@ -449,7 +449,7 @@ nothing to commit, working directory clean 4. Check your `git status` to see unstaged files 5. Add your edited files: `git add path/to/filename.ext` You can also do: `git - add .` to add all unstaged files. Take care, though, because you can +add .` to add all unstaged files. Take care, though, because you can accidentally add files you don't want added. Review your `git status` first. 6. Commit your edits: `git commit -m "Brief description of commit"`. Do not add the issue number in the commit message. @@ -482,17 +482,17 @@ how to do it. 4. The title (also called the subject) of your PR should be descriptive of your changes and succinctly indicate what is being fixed - - **Do not add the issue number in the PR title or commit message** + - **Do not add the issue number in the PR title or commit message** - - Examples: `Add test cases for Unicode support`; `Correct typo in overview documentation` + - Examples: `Add test cases for Unicode support`; `Correct typo in overview documentation` 5. In the body of your PR include a more detailed summary of the changes you made and why - - If the PR is meant to fix an existing bug/issue, then, at the end of - your PR's description, append the keyword `closes` and #xxxx (where xxxx - is the issue number). Example: `closes #1337`. This tells GitHub to - close the existing issue if the PR is merged. + - If the PR is meant to fix an existing bug/issue, then, at the end of + your PR's description, append the keyword `closes` and #xxxx (where xxxx + is the issue number). Example: `closes #1337`. This tells GitHub to + close the existing issue if the PR is merged. 6. Indicate what local testing you have done (e.g. what OS and version(s) of Python did you run the unit test suite with) @@ -632,8 +632,8 @@ mostly automated. The manual steps are all git operations. Here's the checklist: 1. Make sure all the unit tests pass with `invoke pytest` or `py.test` 1. Make sure latest year in `LICENSE` matches current year 1. Make sure `CHANGELOG.md` describes the version and has the correct release date -1. Add a git tag representing the version number using ``invoke tag x.y.z`` - * Where x, y, and z are all small non-negative integers +1. Add a git tag representing the version number using `invoke tag x.y.z` + - Where x, y, and z are all small non-negative integers 1. (Optional) Run `invoke pypi-test` to clean, build, and upload a new release to [Test PyPi](https://test.pypi.org) 1. Run `invoke pypi` to clean, build, and upload a new release to [PyPi](https://pypi.org/) diff --git a/.github/workflows/doc.yml b/.github/workflows/doc.yml index 6fe26d4b5..2725fabbf 100644 --- a/.github/workflows/doc.yml +++ b/.github/workflows/doc.yml @@ -2,7 +2,7 @@ # https://docs.github.com/en/actions/reference/workflow-syntax-for-github-actions name: Doc -on: [ push, pull_request, workflow_dispatch ] +on: [push, pull_request, workflow_dispatch] permissions: contents: read @@ -11,21 +11,21 @@ jobs: doc: strategy: matrix: - os: [ ubuntu-latest ] - python-version: [ "3.12" ] + os: [ubuntu-latest] + python-version: ["3.12"] fail-fast: false runs-on: ${{ matrix.os }} steps: - - uses: actions/checkout@v4 # https://github.com/actions/checkout + - uses: actions/checkout@v4 # https://github.com/actions/checkout with: # Only a single commit is fetched by default, for the ref/SHA that triggered the workflow. # Set fetch-depth: 0 to fetch all history for all branches and tags. fetch-depth: 0 # Needed for setuptools_scm to work correctly - name: Set up Python - uses: actions/setup-python@v5 # https://github.com/actions/setup-python + uses: actions/setup-python@v5 # https://github.com/actions/setup-python with: python-version: ${{ matrix.python-version }} - name: Install python prerequisites - run: pip install -U --user pip setuptools setuptools-scm sphinx sphinx-autobuild sphinx-rtd-theme . plugins/ext_test - - name: Sphinx documentation build - run: python -m sphinx -M html docs docs/_build -nvWT + run: pip install -U --user pip setuptools setuptools-scm mkdocs-include-markdown-plugin mkdocs-macros-plugin mkdocs-material . plugins/ext_test + - name: MkDocs documentation build + run: mkdocs build diff --git a/.gitignore b/.gitignore index dce5b08c1..12886f839 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Python development, test, and build __pycache__ +target build dist *.egg-info diff --git a/.readthedocs.yaml b/.readthedocs.yaml index 4c8b13b5b..30b00c8a1 100644 --- a/.readthedocs.yaml +++ b/.readthedocs.yaml @@ -1,22 +1,18 @@ -# Read the Docs configuration file for Sphinx projects -# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details +# Read the Docs configuration file for MkDocs projects +# See https://docs.readthedocs.io/en/stable/intro/mkdocs.html for details # Required version: 2 # Set the OS, Python version and other tools you might need build: - os: ubuntu-22.04 + os: ubuntu-24.04 tools: python: "3.12" -# Build documentation in the "docs/" directory with Sphinx -sphinx: - configuration: docs/conf.py - # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs - # builder: "dirhtml" - # Fail on all warnings to avoid broken references - # fail_on_warning: true +# Build documentation in the "docs/" directory with MkDocs +mkdocs: + configuration: mkdocs.yml # Optionally build your docs in additional formats such as PDF and ePub # formats: diff --git a/Pipfile b/Pipfile index f8997310a..6b5ccb32e 100644 --- a/Pipfile +++ b/Pipfile @@ -24,9 +24,9 @@ pytest-cov = "*" pytest-mock = "*" ruff = "*" setuptools-scm = "*" -sphinx = "*" -sphinx-autobuild = "*" -sphinx-rtd-theme = "*" +mkdocs-include-markdown-plugin = "*" +mkdocs-macros-plugin = "*" +mkdocs-material = "*" twine = ">=1.11" [pipenv] diff --git a/docs/.gitignore b/docs/.gitignore deleted file mode 100644 index d4e11e5ea..000000000 --- a/docs/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -_build - diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index 433ddd3cd..000000000 --- a/docs/Makefile +++ /dev/null @@ -1,89 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -nvWT -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = _build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . - -.PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/cmd2.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/cmd2.qhc" - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ - "run these through (pdf)latex." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/docs/api/ansi.md b/docs/api/ansi.md new file mode 100644 index 000000000..21be7dcbb --- /dev/null +++ b/docs/api/ansi.md @@ -0,0 +1,7 @@ +# cmd2.ansi + +TODO replace with mkdocstrings: + + > ::: {.automodule members=""} + > cmd2.ansi + > ::: diff --git a/docs/api/ansi.rst b/docs/api/ansi.rst deleted file mode 100644 index e361bd4fd..000000000 --- a/docs/api/ansi.rst +++ /dev/null @@ -1,5 +0,0 @@ -cmd2.ansi -========= - -.. automodule:: cmd2.ansi - :members: diff --git a/docs/api/argparse_completer.md b/docs/api/argparse_completer.md new file mode 100644 index 000000000..6dc91c12a --- /dev/null +++ b/docs/api/argparse_completer.md @@ -0,0 +1,7 @@ +# cmd2.argparse[completer]{#completer} + +TODO replace with mkdocstrings: + + > ::: {.automodule members=""} + > cmd2.argparse[completer]{#completer} + > ::: diff --git a/docs/api/argparse_completer.rst b/docs/api/argparse_completer.rst deleted file mode 100644 index 0e277582c..000000000 --- a/docs/api/argparse_completer.rst +++ /dev/null @@ -1,5 +0,0 @@ -cmd2.argparse_completer -======================= - -.. automodule:: cmd2.argparse_completer - :members: diff --git a/docs/api/argparse_custom.md b/docs/api/argparse_custom.md new file mode 100644 index 000000000..acad38d33 --- /dev/null +++ b/docs/api/argparse_custom.md @@ -0,0 +1,63 @@ +# cmd2.argparse[custom]{#custom} + +TODO replace with mkdocstrings: + + > ::: {.automodule members=""} + > cmd2.argparse[custom]{#custom} + > ::: + +## Added Accessor Methods + +TODO replace with mkdocstrings: + + > ::: autofunction + > [action_get_choices_callable]{#action_get_choices_callable} + > ::: + > + > ::: autofunction + > [action_set_choices_provider]{#action_set_choices_provider} + > ::: + > + > ::: autofunction + > [action_set_completer]{#action_set_completer} + > ::: + > + > ::: autofunction + > [action_get_descriptive_header]{#action_get_descriptive_header} + > ::: + > + > ::: autofunction + > [action_set_descriptive_header]{#action_set_descriptive_header} + > ::: + > + > ::: autofunction + > [action_get_nargs_range]{#action_get_nargs_range} + > ::: + > + > ::: autofunction + > [action_set_nargs_range]{#action_set_nargs_range} + > ::: + > + > ::: autofunction + > [action_get_suppress_tab_hint]{#action_get_suppress_tab_hint} + > ::: + > + > ::: autofunction + > [action_set_suppress_tab_hint]{#action_set_suppress_tab_hint} + > ::: + > + > ::: autofunction + > [ArgumentParser_get_ap_completer_type]{#argumentparser_get_ap_completer_type} + > ::: + > + > ::: autofunction + > [ArgumentParser_set_ap_completer_type]{#argumentparser_set_ap_completer_type} + > ::: + +## Subcommand Removal + +TODO replace with mkdocstrings: + + > ::: autofunction + > [SubParsersAction_remove_parser]{#subparsersaction_remove_parser} + > ::: diff --git a/docs/api/argparse_custom.rst b/docs/api/argparse_custom.rst deleted file mode 100644 index 49897b055..000000000 --- a/docs/api/argparse_custom.rst +++ /dev/null @@ -1,35 +0,0 @@ -cmd2.argparse_custom -==================== - -.. automodule:: cmd2.argparse_custom - :members: - - -Added Accessor Methods ----------------------- -.. autofunction:: _action_get_choices_callable - -.. autofunction:: _action_set_choices_provider - -.. autofunction:: _action_set_completer - -.. autofunction:: _action_get_descriptive_header - -.. autofunction:: _action_set_descriptive_header - -.. autofunction:: _action_get_nargs_range - -.. autofunction:: _action_set_nargs_range - -.. autofunction:: _action_get_suppress_tab_hint - -.. autofunction:: _action_set_suppress_tab_hint - -.. autofunction:: _ArgumentParser_get_ap_completer_type - -.. autofunction:: _ArgumentParser_set_ap_completer_type - - -Subcommand Removal ------------------- -.. autofunction:: _SubParsersAction_remove_parser diff --git a/docs/api/cmd.md b/docs/api/cmd.md new file mode 100644 index 000000000..03f21fe73 --- /dev/null +++ b/docs/api/cmd.md @@ -0,0 +1,86 @@ +# cmd2.Cmd + +TODO replace with mkdocstrings: + + > :::::::::::::::: {.autoclass members=""} + > cmd2.Cmd + > + > ::: automethod + > ::: + > + > ::: attribute + > default[error]{#error} + > + > The error message displayed when a non-existent command is run. Default: `{} is not a recognized command, alias, or macro.` + > ::: + > + > ::: attribute + > help[error]{#error} + > + > The error message displayed to the user when they request help for a command with no help defined. Default: `No help on {}` + > ::: + > + > ::: attribute + > prompt + > + > The prompt issued to solicit input. The default value is `(Cmd)`. See `features/prompt:Prompt`{.interpreted-text role="ref"} for more information. + > ::: + > + > ::: attribute + > continuation[prompt]{#prompt} + > + > The prompt issued to solicit input for the 2nd and subsequent lines of a `multiline command `{.interpreted-text role="ref"} Default: `>`. + > ::: + > + > ::: attribute + > echo + > + > If `True`, output the prompt and user input before executing the command. When redirecting a series of commands to an output file, this allows you to see the command in the output. + > ::: + > + > ::: attribute + > settable + > + > This dictionary contains the name and description of all settings available to users. + > + > Users use the `features/builtin_commands:set`{.interpreted-text role="ref"} command to view and modify settings. Settings are stored in instance attributes with the same name as the setting. + > ::: + > + > ::: attribute + > history + > + > A record of previously entered commands. + > + > This attribute is an instance of `cmd2.history.History`{.interpreted-text role="class"}, and each command is an instance of `cmd2.Statement`{.interpreted-text role="class"}. + > ::: + > + > ::: attribute + > statement[parser]{#parser} + > + > An instance of `cmd2.parsing.StatementParser`{.interpreted-text role="class"} initialized and configured appropriately for parsing user input. + > ::: + > + > ::: attribute + > intro + > + > Set an introduction message which is displayed to the user before the `features/hooks:Command Processing Loop`{.interpreted-text role="ref"} begins. + > ::: + > + > ::: attribute + > py[bridge_name]{#bridge_name} + > + > The symbol name which `features/scripting:Python Scripts`{.interpreted-text role="ref"} run using the `features/builtin_commands:run_pyscript`{.interpreted-text role="ref"} command can use to reference the parent `cmd2` application. + > ::: + > + > ::: attribute + > allow[clipboard]{#clipboard} + > + > If `True`, `cmd2` will allow output to be written to or appended to the operating system pasteboard. If `False`, this capability will not be allowed. See `features/clipboard:Clipboard Integration`{.interpreted-text role="ref"} for more information. + > ::: + > + > ::: attribute + > suggest[similar_command]{#similar_command} + > + > If `True`, `cmd2` will attempt to suggest the most similar command when the user types a command that does not exist. Default: `False`. + > ::: + > :::::::::::::::: diff --git a/docs/api/cmd.rst b/docs/api/cmd.rst deleted file mode 100644 index 4fbb8ccd5..000000000 --- a/docs/api/cmd.rst +++ /dev/null @@ -1,79 +0,0 @@ -cmd2.Cmd -======== - -.. autoclass:: cmd2.Cmd - :members: - - .. automethod:: __init__ - - .. attribute:: default_error - - The error message displayed when a non-existent command is run. - Default: ``{} is not a recognized command, alias, or macro.`` - - .. attribute:: help_error - - The error message displayed to the user when they request help for a - command with no help defined. - Default: ``No help on {}`` - - .. attribute:: prompt - - The prompt issued to solicit input. The default value is ``(Cmd)``. - See :ref:`features/prompt:Prompt` for more information. - - .. attribute:: continuation_prompt - - The prompt issued to solicit input for the 2nd and subsequent lines - of a :ref:`multiline command ` - Default: ``>``. - - .. attribute:: echo - - If ``True``, output the prompt and user input before executing the command. - When redirecting a series of commands to an output file, this allows you to - see the command in the output. - - .. attribute:: settable - - This dictionary contains the name and description of all settings - available to users. - - Users use the :ref:`features/builtin_commands:set` command to view and - modify settings. Settings are stored in instance attributes with the - same name as the setting. - - .. attribute:: history - - A record of previously entered commands. - - This attribute is an instance of :class:`cmd2.history.History`, and - each command is an instance of :class:`cmd2.Statement`. - - .. attribute:: statement_parser - - An instance of :class:`cmd2.parsing.StatementParser` initialized and - configured appropriately for parsing user input. - - .. attribute:: intro - - Set an introduction message which is displayed to the user before - the :ref:`features/hooks:Command Processing Loop` begins. - - .. attribute:: py_bridge_name - - The symbol name which :ref:`features/scripting:Python Scripts` run - using the :ref:`features/builtin_commands:run_pyscript` command can use - to reference the parent ``cmd2`` application. - - .. attribute:: allow_clipboard - - If ``True``, ``cmd2`` will allow output to be written to or appended to - the operating system pasteboard. If ``False``, this capability will not - be allowed. See :ref:`features/clipboard:Clipboard Integration` for more - information. - - .. attribute:: suggest_similar_command - - If ``True``, ``cmd2`` will attempt to suggest the most similar command - when the user types a command that does not exist. Default: ``False``. diff --git a/docs/api/command_definition.md b/docs/api/command_definition.md new file mode 100644 index 000000000..df90f7b50 --- /dev/null +++ b/docs/api/command_definition.md @@ -0,0 +1,7 @@ +# cmd2.command[definition]{#definition} + +TODO replace with mkdocstrings: + + > ::: {.automodule members=""} + > cmd2.command[definition]{#definition} + > ::: diff --git a/docs/api/command_definition.rst b/docs/api/command_definition.rst deleted file mode 100644 index cfb7082ed..000000000 --- a/docs/api/command_definition.rst +++ /dev/null @@ -1,5 +0,0 @@ -cmd2.command_definition -======================= - -.. automodule:: cmd2.command_definition - :members: diff --git a/docs/api/constants.md b/docs/api/constants.md new file mode 100644 index 000000000..13f9b21c4 --- /dev/null +++ b/docs/api/constants.md @@ -0,0 +1,26 @@ +# cmd2.constants + +TODO replace with mkdocstrings: + + > ::::: automodule + > cmd2.constants + > + > ::: data + > DEFAULT[SHORTCUTS]{#shortcuts} + > + > If you do not supply shortcuts to `cmd2.Cmd.__init__`{.interpreted-text role="meth"}, the shortcuts defined here will be used instead. + > ::: + > + > ::: data + > COMMAND[NAME]{#name} + > + > Used by `cmd2.Cmd.disable_command`{.interpreted-text role="meth"} and `cmd2.Cmd.disable_category`{.interpreted-text role="meth"}. Those methods allow you to selectively disable single commands or an entire category of commands. Should you want to include the name of the command in the error message displayed to the user when they try and run a disabled command, you can include this constant in the message where you would like the name of the command to appear. `cmd2` will replace this constant with the name of the command the user tried to run before displaying the error message. + > + > This constant is imported into the package namespace; the preferred syntax to import and reference it is: + > + > import cmd2 + > errmsg = "The {} command is currently disabled.".format(cmd2.COMMAND_NAME) + > + > See `src/examples/help_categories.py` for an example. + > ::: + > ::::: diff --git a/docs/api/constants.rst b/docs/api/constants.rst deleted file mode 100644 index b48ba4629..000000000 --- a/docs/api/constants.rst +++ /dev/null @@ -1,29 +0,0 @@ -cmd2.constants -============== - -.. automodule:: cmd2.constants - - .. data:: DEFAULT_SHORTCUTS - - If you do not supply shortcuts to :meth:`cmd2.Cmd.__init__`, the shortcuts - defined here will be used instead. - - - .. data:: COMMAND_NAME - - Used by :meth:`cmd2.Cmd.disable_command` and - :meth:`cmd2.Cmd.disable_category`. Those methods allow you to selectively - disable single commands or an entire category of commands. Should you want - to include the name of the command in the error message displayed to the - user when they try and run a disabled command, you can include this - constant in the message where you would like the name of the command to - appear. ``cmd2`` will replace this constant with the name of the command - the user tried to run before displaying the error message. - - This constant is imported into the package namespace; the preferred syntax - to import and reference it is:: - - import cmd2 - errmsg = "The {} command is currently disabled.".format(cmd2.COMMAND_NAME) - - See ``src/examples/help_categories.py`` for an example. diff --git a/docs/api/decorators.md b/docs/api/decorators.md new file mode 100644 index 000000000..40cefa6f5 --- /dev/null +++ b/docs/api/decorators.md @@ -0,0 +1,3 @@ +# cmd2.decorators + +::: cmd2.decorators diff --git a/docs/api/decorators.rst b/docs/api/decorators.rst deleted file mode 100644 index c78dcc662..000000000 --- a/docs/api/decorators.rst +++ /dev/null @@ -1,5 +0,0 @@ -cmd2.decorators -=============== - -.. automodule:: cmd2.decorators - :members: diff --git a/docs/api/exceptions.md b/docs/api/exceptions.md new file mode 100644 index 000000000..026604347 --- /dev/null +++ b/docs/api/exceptions.md @@ -0,0 +1,25 @@ +# cmd2.exceptions + +Custom cmd2 exceptions + +TODO replace with mkdocstrings: + + > ::: {.autoclass members=""} + > cmd2.exceptions.SkipPostcommandHooks + > ::: + > + > ::: {.autoclass members=""} + > cmd2.exceptions.Cmd2ArgparseError + > ::: + > + > ::: {.autoclass members=""} + > cmd2.exceptions.CommandSetRegistrationError + > ::: + > + > ::: {.autoclass members=""} + > cmd2.exceptions.CompletionError + > ::: + > + > ::: {.autoclass members=""} + > cmd2.exceptions.PassThroughException + > ::: diff --git a/docs/api/exceptions.rst b/docs/api/exceptions.rst deleted file mode 100644 index efa26957e..000000000 --- a/docs/api/exceptions.rst +++ /dev/null @@ -1,20 +0,0 @@ -cmd2.exceptions -=============== - -Custom cmd2 exceptions - - -.. autoclass:: cmd2.exceptions.SkipPostcommandHooks - :members: - -.. autoclass:: cmd2.exceptions.Cmd2ArgparseError - :members: - -.. autoclass:: cmd2.exceptions.CommandSetRegistrationError - :members: - -.. autoclass:: cmd2.exceptions.CompletionError - :members: - -.. autoclass:: cmd2.exceptions.PassThroughException - :members: diff --git a/docs/api/history.md b/docs/api/history.md new file mode 100644 index 000000000..349b371e0 --- /dev/null +++ b/docs/api/history.md @@ -0,0 +1,25 @@ +# cmd2.history + +Classes for storing the history of previously entered commands. + +TODO replace with mkdocstrings: + + > ::: {.autoclass members=""} + > cmd2.history.History + > ::: + > + > ::::: {.autoclass members=""} + > cmd2.history.HistoryItem + > + > ::: attribute + > statement + > + > The `~cmd2.Statement`{.interpreted-text role="class"} object parsed from user input + > ::: + > + > ::: attribute + > idx + > + > The 1-based index of this statement in the history list + > ::: + > ::::: diff --git a/docs/api/history.rst b/docs/api/history.rst deleted file mode 100644 index 5658238de..000000000 --- a/docs/api/history.rst +++ /dev/null @@ -1,20 +0,0 @@ -cmd2.history -=============== - -Classes for storing the history of previously entered commands. - - -.. autoclass:: cmd2.history.History - :members: - - -.. autoclass:: cmd2.history.HistoryItem - :members: - - .. attribute:: statement - - The :class:`~cmd2.Statement` object parsed from user input - - .. attribute:: idx - - The 1-based index of this statement in the history list diff --git a/docs/api/index.md b/docs/api/index.md new file mode 100644 index 000000000..97d02d62b --- /dev/null +++ b/docs/api/index.md @@ -0,0 +1,29 @@ +--- +render_macros: true +--- + +# API Reference + +These pages document the public API for `cmd2`. If a method, class, function, attribute, or constant is not documented here, consider it private and subject to change. There are many classes, methods, functions, and constants in the source code which do not begin with an underscore but are not documented here. When looking at the source code for this library, you cannot safely assume that because something doesn't start with an underscore, it is a public API. + +If a release of this library changes any of the items documented here, the version number will be incremented according to the [Semantic Version Specification](https://semver.org). + +This documentation is for `cmd2` version {{ version }}. + +**Modules** + +- `api/cmd:cmd2.Cmd`{.interpreted-text role="ref"} - functions and attributes of the main class in this library +- `api/ansi:cmd2.ansi`{.interpreted-text role="ref"} - convenience classes and functions for generating ANSI escape sequences to style text in the terminal +- `api/argparse_completer:cmd2.argparse_completer`{.interpreted-text role="ref"} - classes for `argparse`-based tab completion +- `api/argparse_custom:cmd2.argparse_custom`{.interpreted-text role="ref"} - classes and functions for extending `argparse` +- `api/command_definition:cmd2.command_definition`{.interpreted-text role="ref"} - supports the definition of commands in separate classes to be composed into cmd2.Cmd +- `api/constants:cmd2.constants`{.interpreted-text role="ref"} - just like it says on the tin +- `api/decorators:cmd2.decorators`{.interpreted-text role="ref"} - decorators for `cmd2` commands +- `api/exceptions:cmd2.exceptions`{.interpreted-text role="ref"} - custom `cmd2` exceptions +- `api/history:cmd2.history`{.interpreted-text role="ref"} - classes for storing the history of previously entered commands +- `api/parsing:cmd2.parsing`{.interpreted-text role="ref"} - classes for parsing and storing user input +- `api/plugin:cmd2.plugin`{.interpreted-text role="ref"} - data classes for hook methods +- `api/py_bridge:cmd2.py_bridge`{.interpreted-text role="ref"} - classes for bridging calls from the embedded python environment to the host app +- `api/table_creator:cmd2.table_creator`{.interpreted-text role="ref"} - table creation module +- `api/utils:cmd2.utils`{.interpreted-text role="ref"} - various utility classes and functions +- `api/plugin_external_test:cmd2_ext_test`{.interpreted-text role="ref"} - External test plugin diff --git a/docs/api/index.rst b/docs/api/index.rst deleted file mode 100644 index 1f5e1625e..000000000 --- a/docs/api/index.rst +++ /dev/null @@ -1,62 +0,0 @@ -API Reference -============= - -These pages document the public API for ``cmd2``. If a method, class, function, -attribute, or constant is not documented here, consider it private and subject -to change. There are many classes, methods, functions, and constants in the -source code which do not begin with an underscore but are not documented here. -When looking at the source code for this library, you cannot safely assume -that because something doesn't start with an underscore, it is a public API. - -If a release of this library changes any of the items documented here, the -version number will be incremented according to the `Semantic Version -Specification `_. - -This documentation is for ``cmd2`` version |version|. - -.. toctree:: - :maxdepth: 1 - :hidden: - - cmd - ansi - argparse_completer - argparse_custom - constants - command_definition - decorators - exceptions - history - parsing - plugin - py_bridge - table_creator - utils - plugin_external_test - -**Modules** - -- :ref:`api/cmd:cmd2.Cmd` - functions and attributes of the main - class in this library -- :ref:`api/ansi:cmd2.ansi` - convenience classes and functions for generating - ANSI escape sequences to style text in the terminal -- :ref:`api/argparse_completer:cmd2.argparse_completer` - classes for - ``argparse``-based tab completion -- :ref:`api/argparse_custom:cmd2.argparse_custom` - classes and functions - for extending ``argparse`` -- :ref:`api/command_definition:cmd2.command_definition` - supports the - definition of commands in separate classes to be composed into cmd2.Cmd -- :ref:`api/constants:cmd2.constants` - just like it says on the tin -- :ref:`api/decorators:cmd2.decorators` - decorators for ``cmd2`` - commands -- :ref:`api/exceptions:cmd2.exceptions` - custom ``cmd2`` exceptions -- :ref:`api/history:cmd2.history` - classes for storing the history - of previously entered commands -- :ref:`api/parsing:cmd2.parsing` - classes for parsing and storing - user input -- :ref:`api/plugin:cmd2.plugin` - data classes for hook methods -- :ref:`api/py_bridge:cmd2.py_bridge` - classes for bridging calls from the - embedded python environment to the host app -- :ref:`api/table_creator:cmd2.table_creator` - table creation module -- :ref:`api/utils:cmd2.utils` - various utility classes and functions -- :ref:`api/plugin_external_test:cmd2_ext_test` - External test plugin diff --git a/docs/api/parsing.md b/docs/api/parsing.md new file mode 100644 index 000000000..c29926fc0 --- /dev/null +++ b/docs/api/parsing.md @@ -0,0 +1,83 @@ +# cmd2.parsing + +Classes for parsing and storing user input. + +TODO replace with mkdocstrings: + + > :::: {.autoclass members=""} + > cmd2.parsing.StatementParser + > + > ::: automethod + > ::: + > :::: + > + > ::::::::::::: {.autoclass members=""} + > cmd2.Statement + > + > ::: attribute + > command + > + > The name of the command after shortcuts and macros have been expanded + > ::: + > + > ::: attribute + > args + > + > The arguments to the command as a string with spaces between the words, excluding output redirection and command terminators. If the user used quotes in their input, they remain here, and you will have to handle them on your own. + > ::: + > + > ::: attribute + > arg[list]{#list} + > + > The arguments to the command as a list, excluding output redirection and command terminators. Each argument is represented as an element in the list. Quoted arguments remain quoted. If you want to remove the quotes, use `cmd2.utils.strip_quotes`{.interpreted-text role="func"} or use `argv[1:]` + > ::: + > + > ::: attribute + > raw + > + > If you want full access to exactly what the user typed at the input prompt you can get it, but you'll have to parse it on your own, including: + > + > > - shortcuts and aliases + > > - quoted commands and arguments + > > - output redirection + > > - multi-line command terminator handling + > + > If you use multiline commands, all the input will be passed to you in this string, but there will be embedded newlines where the user hit return to continue the command on the next line. + > ::: + > + > ::: attribute + > multiline[command]{#command} + > + > If the command is a multi-line command, the name of the command will be in this attribute. Otherwise, it will be an empty string. + > ::: + > + > ::: attribute + > terminator + > + > If the command is a multi-line command, this attribute contains the termination character entered by the user to signal the end of input + > ::: + > + > ::: attribute + > suffix + > + > Any characters present between the input terminator and the output redirection tokens. + > ::: + > + > ::: attribute + > pipe[to]{#to} + > + > If the user piped the output to a shell command, this attribute contains the entire shell command as a string. Otherwise it is an empty string. + > ::: + > + > ::: attribute + > output + > + > If output was redirected by the user, this contains the redirection token, i.e. `>>`. + > ::: + > + > ::: attribute + > output[to]{#to} + > + > If output was redirected by the user, this contains the requested destination with quotes preserved. + > ::: + > ::::::::::::: diff --git a/docs/api/parsing.rst b/docs/api/parsing.rst deleted file mode 100644 index fa726700b..000000000 --- a/docs/api/parsing.rst +++ /dev/null @@ -1,78 +0,0 @@ -cmd2.parsing -=============== - -Classes for parsing and storing user input. - - -.. autoclass:: cmd2.parsing.StatementParser - :members: - - .. automethod:: __init__ - - -.. autoclass:: cmd2.Statement - :members: - - .. attribute:: command - - The name of the command after shortcuts and macros have been expanded - - .. attribute:: args - - The arguments to the command as a string with spaces between the words, - excluding output redirection and command terminators. If the user used - quotes in their input, they remain here, and you will have to handle them - on your own. - - .. attribute:: arg_list - - The arguments to the command as a list, excluding output - redirection and command terminators. Each argument is represented as an - element in the list. Quoted arguments remain quoted. If you want to - remove the quotes, use :func:`cmd2.utils.strip_quotes` or use - ``argv[1:]`` - - .. attribute:: raw - - If you want full access to exactly what the user typed at the input - prompt you can get it, but you'll have to parse it on your own, - including: - - - shortcuts and aliases - - quoted commands and arguments - - output redirection - - multi-line command terminator handling - - If you use multiline commands, all the input will be passed to you in - this string, but there will be embedded newlines where the user hit - return to continue the command on the next line. - - .. attribute:: multiline_command - - If the command is a multi-line command, the name of the command will be - in this attribute. Otherwise, it will be an empty string. - - .. attribute:: terminator - - If the command is a multi-line command, this attribute contains the - termination character entered by the user to signal the end of input - - .. attribute:: suffix - - Any characters present between the input terminator and the output - redirection tokens. - - .. attribute:: pipe_to - - If the user piped the output to a shell command, this attribute contains - the entire shell command as a string. Otherwise it is an empty string. - - .. attribute:: output - - If output was redirected by the user, this contains the redirection - token, i.e. ``>>``. - - .. attribute:: output_to - - If output was redirected by the user, this contains the requested destination with - quotes preserved. diff --git a/docs/api/plugin.md b/docs/api/plugin.md new file mode 100644 index 000000000..d8d77394a --- /dev/null +++ b/docs/api/plugin.md @@ -0,0 +1,61 @@ +# cmd2.plugin + +TODO replace with mkdocstrings: + + > ::::: {.autoclass members=""} + > cmd2.plugin.PostparsingData + > + > ::: attribute + > stop + > + > Request the command loop terminate by setting `True` + > ::: + > + > ::: attribute + > statement + > + > The `~cmd2.Statement`{.interpreted-text role="class"} object parsed from user input + > ::: + > ::::: + > + > :::: {.autoclass members=""} + > cmd2.plugin.PrecommandData + > + > ::: attribute + > statement + > + > The `~cmd2.Statement`{.interpreted-text role="class"} object parsed from user input + > ::: + > :::: + > + > ::::: {.autoclass members=""} + > cmd2.plugin.PostcommandData + > + > ::: attribute + > stop + > + > Request the command loop terminate by setting `True` + > ::: + > + > ::: attribute + > statement + > + > The `~cmd2.Statement`{.interpreted-text role="class"} object parsed from user input + > ::: + > ::::: + > + > ::::: {.autoclass members=""} + > cmd2.plugin.CommandFinalizationData + > + > ::: attribute + > stop + > + > Request the command loop terminate by setting `True` + > ::: + > + > ::: attribute + > statement + > + > The `~cmd2.Statement`{.interpreted-text role="class"} object parsed from user input + > ::: + > ::::: diff --git a/docs/api/plugin.rst b/docs/api/plugin.rst deleted file mode 100644 index 4e554b335..000000000 --- a/docs/api/plugin.rst +++ /dev/null @@ -1,45 +0,0 @@ -cmd2.plugin -=========== - -.. autoclass:: cmd2.plugin.PostparsingData - :members: - - .. attribute:: stop - - Request the command loop terminate by setting ``True`` - - .. attribute:: statement - - The :class:`~cmd2.Statement` object parsed from user input - - -.. autoclass:: cmd2.plugin.PrecommandData - :members: - - .. attribute:: statement - - The :class:`~cmd2.Statement` object parsed from user input - - -.. autoclass:: cmd2.plugin.PostcommandData - :members: - - .. attribute:: stop - - Request the command loop terminate by setting ``True`` - - .. attribute:: statement - - The :class:`~cmd2.Statement` object parsed from user input - - -.. autoclass:: cmd2.plugin.CommandFinalizationData - :members: - - .. attribute:: stop - - Request the command loop terminate by setting ``True`` - - .. attribute:: statement - - The :class:`~cmd2.Statement` object parsed from user input diff --git a/docs/api/plugin_external_test.md b/docs/api/plugin_external_test.md new file mode 100644 index 000000000..bd8be3978 --- /dev/null +++ b/docs/api/plugin_external_test.md @@ -0,0 +1,9 @@ +# cmd2[ext_test]{#ext_test} + +External Test Plugin + +TODO replace with mkdocstrings: + + > ::: {.autoclass members=""} + > cmd2[ext_test.ExternalTestMixin]{#ext_test.externaltestmixin} + > ::: diff --git a/docs/api/plugin_external_test.rst b/docs/api/plugin_external_test.rst deleted file mode 100644 index 58450b11b..000000000 --- a/docs/api/plugin_external_test.rst +++ /dev/null @@ -1,9 +0,0 @@ -cmd2_ext_test -============= - -External Test Plugin - - -.. autoclass:: cmd2_ext_test.ExternalTestMixin - :members: - diff --git a/docs/api/py_bridge.md b/docs/api/py_bridge.md new file mode 100644 index 000000000..e5c930a9b --- /dev/null +++ b/docs/api/py_bridge.md @@ -0,0 +1,7 @@ +# cmd2.py[bridge]{#bridge} + +TODO replace with mkdocstrings: + + > ::: {.automodule members=""} + > cmd2.py[bridge]{#bridge} + > ::: diff --git a/docs/api/py_bridge.rst b/docs/api/py_bridge.rst deleted file mode 100644 index 28a70b597..000000000 --- a/docs/api/py_bridge.rst +++ /dev/null @@ -1,5 +0,0 @@ -cmd2.py_bridge -============== - -.. automodule:: cmd2.py_bridge - :members: diff --git a/docs/api/table_creator.md b/docs/api/table_creator.md new file mode 100644 index 000000000..efcc0777e --- /dev/null +++ b/docs/api/table_creator.md @@ -0,0 +1,46 @@ +# cmd2.table[creator]{#creator} + +TODO replace with mkdocstrings: + + > ::: {.autoclass members="" undoc-members=""} + > cmd2.table[creator.HorizontalAlignment]{#creator.horizontalalignment} + > ::: + > + > ::: {.autoclass members="" undoc-members=""} + > cmd2.table[creator.VerticalAlignment]{#creator.verticalalignment} + > ::: + > + > :::: {.autoclass members=""} + > cmd2.table[creator.Column]{#creator.column} + > + > ::: automethod + > ::: + > :::: + > + > :::: {.autoclass members=""} + > cmd2.table[creator.TableCreator]{#creator.tablecreator} + > + > ::: automethod + > ::: + > :::: + > + > :::: {.autoclass members=""} + > cmd2.table[creator.SimpleTable]{#creator.simpletable} + > + > ::: automethod + > ::: + > :::: + > + > :::: {.autoclass members=""} + > cmd2.table[creator.BorderedTable]{#creator.borderedtable} + > + > ::: automethod + > ::: + > :::: + > + > :::: {.autoclass members=""} + > cmd2.table[creator.AlternatingTable]{#creator.alternatingtable} + > + > ::: automethod + > ::: + > :::: diff --git a/docs/api/table_creator.rst b/docs/api/table_creator.rst deleted file mode 100644 index 00dd70ba2..000000000 --- a/docs/api/table_creator.rst +++ /dev/null @@ -1,35 +0,0 @@ -cmd2.table_creator -================== - -.. autoclass:: cmd2.table_creator.HorizontalAlignment - :members: - :undoc-members: - -.. autoclass:: cmd2.table_creator.VerticalAlignment - :members: - :undoc-members: - -.. autoclass:: cmd2.table_creator.Column - :members: - - .. automethod:: __init__ - -.. autoclass:: cmd2.table_creator.TableCreator - :members: - - .. automethod:: __init__ - -.. autoclass:: cmd2.table_creator.SimpleTable - :members: - - .. automethod:: __init__ - -.. autoclass:: cmd2.table_creator.BorderedTable - :members: - - .. automethod:: __init__ - -.. autoclass:: cmd2.table_creator.AlternatingTable - :members: - - .. automethod:: __init__ diff --git a/docs/api/utils.md b/docs/api/utils.md new file mode 100644 index 000000000..14f156b26 --- /dev/null +++ b/docs/api/utils.md @@ -0,0 +1,145 @@ +# cmd2.utils + +## Settings + +TODO replace with mkdocstrings: + + > :::: {.autoclass members=""} + > cmd2.utils.Settable + > + > ::: automethod + > ::: + > :::: + +## Quote Handling + +TODO replace with mkdocstrings: + + > ::: autofunction + > cmd2.utils.is[quoted]{#quoted} + > ::: + > + > ::: autofunction + > cmd2.utils.quote[string]{#string} + > ::: + > + > ::: autofunction + > cmd2.utils.quote[string_if_needed]{#string_if_needed} + > ::: + > + > ::: autofunction + > cmd2.utils.strip[quotes]{#quotes} + > ::: + > + > ::: autofunction + > cmd2.utils.quote[specific_tokens]{#specific_tokens} + > ::: + > + > ::: autofunction + > cmd2.utils.unquote[specific_tokens]{#specific_tokens} + > ::: + +## IO Handling + +TODO replace with mkdocstrings: + + > ::: {.autoclass members=""} + > cmd2.utils.StdSim + > ::: + > + > ::: {.autoclass members=""} + > cmd2.utils.ByteBuf + > ::: + > + > ::: {.autoclass members=""} + > cmd2.utils.ProcReader + > ::: + +## Tab Completion + +TODO replace with mkdocstrings: + + > :::::: autoclass + > cmd2.utils.CompletionMode + > + > ::: attribute + > NONE + > + > Tab completion will be disabled during read[input]{#input}() call. Use of custom up-arrow history supported. + > ::: + > + > ::: attribute + > COMMANDS + > + > read[input]{#input}() will tab complete cmd2 commands and their arguments. cmd2's command line history will be used for up arrow if history is not provided. Otherwise use of custom up-arrow history supported. + > ::: + > + > ::: attribute + > CUSTOM + > + > read[input]{#input}() will tab complete based on one of its following parameters (choices, choices[provider]{#provider}, completer, parser). Use of custom up-arrow history supported + > ::: + > :::::: + > + > :::: autoclass + > cmd2.utils.CustomCompletionSettings + > + > ::: automethod + > ::: + > :::: + +## Text Alignment + +TODO replace with mkdocstrings: + + > ::: {.autoclass members="" undoc-members=""} + > cmd2.utils.TextAlignment + > ::: + > + > ::: autofunction + > cmd2.utils.align[text]{#text} + > ::: + > + > ::: autofunction + > cmd2.utils.align[left]{#left} + > ::: + > + > ::: autofunction + > cmd2.utils.align[right]{#right} + > ::: + > + > ::: autofunction + > cmd2.utils.align[center]{#center} + > ::: + > + > ::: autofunction + > cmd2.utils.truncate[line]{#line} + > ::: + +## Miscellaneous + +TODO replace with mkdocstrings: + + > ::: autofunction + > cmd2.utils.to[bool]{#bool} + > ::: + > + > ::: autofunction + > cmd2.utils.categorize + > ::: + > + > ::: autofunction + > cmd2.utils.remove[duplicates]{#duplicates} + > ::: + > + > ::: autofunction + > cmd2.utils.alphabetical[sort]{#sort} + > ::: + > + > ::: autofunction + > cmd2.utils.natural[sort]{#sort} + > ::: + > + > ::: autofunction + > cmd2.utils.suggest[similar]{#similar} + > ::: diff --git a/docs/api/utils.rst b/docs/api/utils.rst deleted file mode 100644 index 82644cbb5..000000000 --- a/docs/api/utils.rst +++ /dev/null @@ -1,102 +0,0 @@ -cmd2.utils -========== - - -Settings --------- - -.. autoclass:: cmd2.utils.Settable - :members: - - .. automethod:: __init__ - - -Quote Handling --------------- - -.. autofunction:: cmd2.utils.is_quoted - -.. autofunction:: cmd2.utils.quote_string - -.. autofunction:: cmd2.utils.quote_string_if_needed - -.. autofunction:: cmd2.utils.strip_quotes - -.. autofunction:: cmd2.utils.quote_specific_tokens - -.. autofunction:: cmd2.utils.unquote_specific_tokens - - -IO Handling ------------ - -.. autoclass:: cmd2.utils.StdSim - :members: - -.. autoclass:: cmd2.utils.ByteBuf - :members: - -.. autoclass:: cmd2.utils.ProcReader - :members: - - -Tab Completion --------------- - -.. autoclass:: cmd2.utils.CompletionMode - - .. attribute:: NONE - - Tab completion will be disabled during read_input() call. Use of custom - up-arrow history supported. - - .. attribute:: COMMANDS - - read_input() will tab complete cmd2 commands and their arguments. - cmd2's command line history will be used for up arrow if history is not - provided. Otherwise use of custom up-arrow history supported. - - .. attribute:: CUSTOM - - read_input() will tab complete based on one of its following parameters - (choices, choices_provider, completer, parser). Use of custom up-arrow - history supported - -.. autoclass:: cmd2.utils.CustomCompletionSettings - - .. automethod:: __init__ - - -Text Alignment --------------- - -.. autoclass:: cmd2.utils.TextAlignment - :members: - :undoc-members: - -.. autofunction:: cmd2.utils.align_text - -.. autofunction:: cmd2.utils.align_left - -.. autofunction:: cmd2.utils.align_right - -.. autofunction:: cmd2.utils.align_center - -.. autofunction:: cmd2.utils.truncate_line - - -Miscellaneous -------------- - -.. autofunction:: cmd2.utils.to_bool - -.. autofunction:: cmd2.utils.categorize - -.. autofunction:: cmd2.utils.remove_duplicates - -.. autofunction:: cmd2.utils.alphabetical_sort - -.. autofunction:: cmd2.utils.natural_sort - -.. autofunction:: cmd2.utils.suggest_similar - diff --git a/docs/conf.py b/docs/conf.py deleted file mode 100644 index 110c36849..000000000 --- a/docs/conf.py +++ /dev/null @@ -1,196 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -cmd2 documentation build configuration file, created by -sphinx-quickstart on Wed Feb 10 12:05:28 2010. - -This file is execfile()d with the current directory set to its -containing dir. - -Note that not all possible configuration values are present in this -autogenerated file. - -All configuration values have a default; values that are commented out -serve to show the default. - -If extensions (or modules to document with autodoc) are in another directory, -add these directories to sys.path here. If the directory is relative to the -documentation root, use os.path.abspath to make it absolute, like shown here. -""" - -import sys -from importlib.metadata import version as get_version -from os.path import abspath, dirname - -# Make sure we get the version of this copy of cmd2 -root_path = dirname(dirname(abspath(__file__))) -cmd2_ext_test_path = f'{root_path}/plugins/ext_test/' -sys.path.insert(1, root_path) # cmd2 -sys.path.insert(2, cmd2_ext_test_path) # cmd2_ext_test - -# Import for custom theme from Read the Docs -import sphinx_rtd_theme # noqa E402 - -# -- General configuration ----------------------------------------------------- - -# If your documentation needs a minimal Sphinx version, state it here. -# -# needs_sphinx = '1.0' - -# Add any Sphinx extension module names here, as strings. They can be extensions -# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = [ - 'sphinx.ext.autodoc', - 'sphinx.ext.autosectionlabel', - 'sphinx.ext.intersphinx', - 'sphinx.ext.doctest', - 'sphinx.ext.todo', -] - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix(es) of source filenames. -# You can specify multiple suffix as a list of string: -# -# source_suffix = ['.rst', '.md'] -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -project = 'cmd2' -copyright = '2010-2024, cmd2 contributors' -author = 'cmd2 contributors' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# release will look like x.y.z -release: str = get_version("cmd2") -# for example take major/minor -version: str = ".".join(release.split('.')[:2]) - -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# -# This is also used if you do content translation via gettext catalogs. -# Usually you set "language" from the command line for these cases. -language = 'en' - -# List of patterns, relative to source directory, that match files and -# directories to ignore when looking for source files. -# This patterns also effect to html_static_path and html_extra_path -exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - -# If true, `todo` and `todoList` produce output, else they produce nothing. -todo_include_todos = False - -# configure autosectionlabel extension -autosectionlabel_prefix_document = True - -# -- Options for HTML output --------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. See the documentation for -# a list of builtin themes. - -# Custom theme from ReadTheDocs -html_theme = 'sphinx_rtd_theme' - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -# -# html_theme_options = {} - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - - -# -- Options for HTMLHelp output ------------------------------------------ - -# Output file base name for HTML help builder. -htmlhelp_basename = 'cmd2doc' - - -# -- Options for LaTeX output -------------------------------------------------- - -latex_elements = { - # The paper size ('letterpaper' or 'a4paper'). - # - # 'papersize': 'letterpaper', - # The font size ('10pt', '11pt' or '12pt'). - # - # 'pointsize': '10pt', - # Additional stuff for the LaTeX preamble. - # - # 'preamble': '', - # Latex figure (float) alignment - # - # 'figure_align': 'htbp', -} - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, -# author, documentclass [howto, manual, or own class]). -latex_documents = [ - (master_doc, 'cmd2.tex', 'cmd2 Documentation', 'Catherine Devlin and Todd Leonhardt', 'manual'), -] - - -# -- Options for manual page output --------------------------------------- - -# One entry per manual page. List of tuples -# (source start file, name, description, authors, manual section). -man_pages = [(master_doc, 'cmd2', 'cmd2 Documentation', [author], 1)] - - -# -- Options for Texinfo output ------------------------------------------- - -# Grouping the document tree into Texinfo files. List of tuples -# (source start file, target name, title, author, -# dir menu entry, description, category) -texinfo_documents = [ - ( - master_doc, - 'cmd2', - 'cmd2 Documentation', - author, - 'cmd2', - 'A python package for building powerful command-line interpreter (CLI) programs.', - 'Miscellaneous', - ), -] - - -# -- Options for Extensions ------------------------------------------- -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = { - 'python 3': ('https://docs.python.org/3/', None), -} - -# options for autodoc -autodoc_default_options = {'member-order': 'bysource'} - -# Ignore nitpicky warnings from autodoc which are occurring for very new versions of Sphinx and autodoc -# They seem to be happening because autodoc is now trying to add hyperlinks to docs for typehint classes -nitpick_ignore = [ - ('py:class', 'ArgparseCommandFunc'), - ('py:class', 'argparse._SubParsersAction'), - ('py:class', 'cmd2.command_definition.CommandSetType'), - ('py:class', 'cmd2.decorators.CommandParent'), - ('py:class', 'cmd2.decorators.CommandParentType'), - ('py:class', 'cmd2.utils._T'), - ('py:class', 'CommandParent'), - ('py:class', 'frame'), - ('py:class', 'RawCommandFuncOptionalBoolReturn'), - ('py:class', 'types.FrameType'), - ('py:obj', 'cmd2.decorators.CommandParent'), -] diff --git a/docs/doc_conventions.md b/docs/doc_conventions.md new file mode 100644 index 000000000..394dac94d --- /dev/null +++ b/docs/doc_conventions.md @@ -0,0 +1,59 @@ +# Documentation Conventions + +## Guiding Principles + +Follow the [Documentation Principles](http://www.writethedocs.org/guide/writing/docs-principles/) described by [Write The Docs](http://www.writethedocs.org) + +In addition: + +- We have gone to great lengths to retain compatibility with the standard library cmd, the documentation should make it easy for developers to understand how to move from cmd to cmd2, and what benefits that will provide +- We should provide both descriptive and reference documentation. +- API reference documentation should be generated from docstrings in the code +- Documentation should include rich hyperlinking to other areas of the documentation, and to the API reference + +## Style Checker + +We strongly encourage all developers to use [Prettier](https://prettier.io/) for formatting all **Markdown** and YAML files. The easiest way to do this is to integrated it with your IDE and configure your IDE to format on save. You can also install `prettier` either using `npm` or OS package manager such as `brew` or `apt`. + +## Naming Files + +All source files in the documentation must: + +- have all lower case file names +- if the name has multiple words, separate them with an underscore +- end in '.rst' + +## Indenting + +In Markdown all indenting is significant. Use 4 spaces per indenting level. + +## Wrapping + +Hard wrap all text so that line lengths are no greater than 120 characters. It makes everything easier when editing documentation, and has no impact on reading documentation because we render to html. + +## Titles and Headings + +Reference the [Markdown Basic Syntax](https://www.markdownguide.org/basic-syntax/) for synatx basics or [The Markdown Guide](https://www.markdownguide.org/) for a more complete reference. + +## Inline Code + +Code blocks can be created in two ways: + +- Indent the block - this will show as a monospace code block, but won't include highighting +- use the triple backticks followed by the code language, e.e. `python` and close with triple backticks + +If you want to show non-Python code, like shell commands, then use a different language such as `javascript`, `shell`, `json`, etc. + +## Links + +See the [Links](https://www.markdownguide.org/basic-syntax/) Markdown syntax documentation. + +## API Documentation + +The API documentation is mostly pulled from docstrings in the source code using the MkDocs [mkdocstrings](https://mkdocstrings.github.io/) plugin. + +TODO: Complete this section once this has been implemented + +## Referencing cmd2 + +Whenever you reference `cmd2` in the documentation, enclose it in backticks. This indicates to Markdown that this words represents code and will stand out when rendered as HTML. diff --git a/docs/doc_conventions.rst b/docs/doc_conventions.rst deleted file mode 100644 index fc96fd582..000000000 --- a/docs/doc_conventions.rst +++ /dev/null @@ -1,228 +0,0 @@ -Documentation Conventions -========================= - -Guiding Principles ------------------- - -Follow the `Documentation Principles -`_ described by -`Write The Docs `_ - -In addition: - -- We have gone to great lengths to retain compatibility with the standard - library cmd, the documentation should make it easy for developers to - understand how to move from cmd to cmd2, and what benefits that will provide -- We should provide both descriptive and reference documentation. -- API reference documentation should be generated from docstrings in the code -- Documentation should include rich hyperlinking to other areas of the - documentation, and to the API reference - - -Style Checker -------------- - -Use `doc8 `_ to check the style of the -documentation. This tool can be invoked using the proper options by typing: - -.. code-block:: shell - - $ invoke doc8 - - -Naming Files ------------- - -All source files in the documentation must: - -- have all lower case file names -- if the name has multiple words, separate them with an underscore -- end in '.rst' - - -Indenting ---------- - -In reStructuredText all indenting is significant. Use 2 spaces per indenting -level. - - -Wrapping --------- - -Hard wrap all text so that line lengths are no greater than 79 characters. It -makes everything easier when editing documentation, and has no impact on -reading documentation because we render to html. - - -Titles and Headings -------------------- - -reStructuredText allows flexibility in how headings are defined. You only have -to worry about the hierarchy of headings within a single file. Sphinx magically -handles the intra-file hierarchy on it's own. This magic means that no matter -how you style titles and headings in the various files that comprise the -documentation, Sphinx will render properly structured output. To ensure we have -a similar consistency when viewing the source files, we use the following -conventions for titles and headings: - -1. When creating a heading for a section, do not use the overline and underline -syntax. Use the underline syntax only:: - - Document Title - ============== - -2. The string of adornment characters on the line following the heading should -be the same length as the title. - -3. The title of a document should use the '=' adornment character on the next -line and only one heading of this level should appear in each file. - -4. Sections within a document should have their titles adorned with the '-' -character:: - - Section Title - ------------- - -5. Subsections within a section should have their titles adorned with the '~' -character:: - - Subsection Title - ~~~~~~~~~~~~~~~~ - -6. Use two blank lines before every title unless it's the first heading in the -file. Use one blank line after every heading. - -7. If your document needs more than three levels of sections, break it into -separate documents. - - -Inline Code ------------ - -This documentation declares ``python`` as the default Sphinx domain. Python -code or interactive Python sessions can be presented by either: - -- finishing the preceding paragraph with a ``::`` and indenting the code -- use the ``.. code-block::`` directive - -If you want to show non-Python code, like shell commands, then use ``.. -code-block: shell``. - - -External Hyperlinks -------------------- - -If you want to use an external hyperlink target, define the target at the top -of the page or the top of the section, not the bottom. The target definition -should always appear before it is referenced. - - -Links To Other Documentation Pages and Sections ------------------------------------------------ - -We use the Sphinx `autosectionlabel -`_ -extension. This allows you to reference any header in any document by:: - - See :ref:`features/argument_processing:Help Messages` - -or:: - - See :ref:`custom title` - -Which render like - -See :ref:`features/argument_processing:Help Messages` - -and - -See :ref:`custom title` - - -API Documentation ------------------ - -The API documentation is mostly pulled from docstrings in the source code using -the Sphinx `autodoc -`_ -extension. However, Sphinx has issues generating documentation for instance -attributes (see `cmd2 issue 821 -`_ for the full discussion). We -have chosen to not use code as the source of instance attribute documentation. -Instead, it is added manually to the documentation files in ``cmd2/docs/api``. -See ``cmd2/docs/api/cmd.rst`` to see how to add documentation for an attribute. - -For module data members and class attributes, the ``autodoc`` extension allows -documentation in a comment with special formatting (using a #: to start the -comment instead of just #), or in a docstring after the definition. This -project has standardized on the docstring after the definition approach. Do not -use the specially formatted comment approach. - -When using the Sphix ``autoclass`` directive, it must be preceded by two blank -lines like so: - -.. code-block:: rst - - Classes for storing the history of previously entered commands. - - - .. autoclass:: cmd2.history.History - :members: - - - .. autoclass:: cmd2.history.HistoryItem - :members: - - -Links to API Reference ----------------------- - -To reference a method or function, use one of the following approaches: - -1. Reference the full dotted path of the method:: - - The :meth:`cmd2.Cmd.poutput` method is similar to the Python built-in - print function. - -Which renders as: The :meth:`cmd2.Cmd.poutput` method is similar to the -Python built-in print function. - -2. Reference the full dotted path to the method, but only display the method -name:: - - The :meth:`~cmd2.Cmd.poutput` method is similar to the Python built-in print function. - -Which renders as: The :meth:`~cmd2.Cmd.poutput` method is similar to the -Python built-in print function. - -Avoid either of these approaches: - -1. Reference just the class name without enough dotted path:: - - The :meth:`.Cmd.poutput` method is similar to the Python built-in print - function. - -Because ``cmd2.Cmd`` subclasses ``cmd.Cmd`` from the standard library, this -approach does not clarify which class it is referring to. - -2. Reference just a method name:: - - The :meth:`poutput` method is similar to the Python built-in print - function. - -While Sphinx may be smart enough to generate the correct output, the potential -for multiple matching references is high, which causes Sphinx to generate -warnings. The build pipeline that renders the documentation treats warnings as -fatal errors. It's best to just be specific about what you are referencing. - -See ``_ for the discussion of -how we determined this approach. - - -Referencing cmd2 ------------------ - -Whenever you reference ``cmd2`` in the documentation, enclose it in double -backticks. This indicates an inline literal in restructured text, and makes it -stand out when rendered as html. diff --git a/docs/examples/alternate_event_loops.md b/docs/examples/alternate_event_loops.md new file mode 100644 index 000000000..22e29f552 --- /dev/null +++ b/docs/examples/alternate_event_loops.md @@ -0,0 +1,57 @@ +# Alternate Event Loops + +Throughout this documentation we have focused on the **90%** use case, that is the use case we believe around **90+%** of our user base is looking for. This focuses on ease of use and the best out-of-the-box experience where developers get the most functionality for the least amount of effort. We are talking about running `cmd2` applications with the `cmdloop()` method: + + from cmd2 import Cmd + class App(Cmd): + # customized attributes and methods here + app = App() + app.cmdloop() + +However, there are some limitations to this way of using `cmd2`, mainly that `cmd2` owns the inner loop of a program. This can be unnecessarily restrictive and can prevent using libraries which depend on controlling their own event loop. + +Many Python concurrency libraries involve or require an event loop which they are in control of such as [asyncio](https://docs.python.org/3/library/asyncio.html), [gevent](http://www.gevent.org/), [Twisted](https://twistedmatrix.com), etc. + +`cmd2` applications can be executed in a fashion where `cmd2` doesn't own the main loop for the program by using code like the following: + + import cmd2 + + class Cmd2EventBased(cmd2.Cmd): + def __init__(self): + cmd2.Cmd.__init__(self) + + # ... your class code here ... + + if __name__ == '__main__': + app = Cmd2EventBased() + app.preloop() + + # Do this within whatever event loop mechanism you wish to run a single command + cmd_line_text = "help history" + app.runcmds_plus_hooks([cmd_line_text]) + + app.postloop() + +The `~cmd2.Cmd.runcmds_plus_hooks()`{.interpreted-text role="meth"} method runs multiple commands via `~cmd2.Cmd.onecmd_plus_hooks`{.interpreted-text role="meth"}. + +The `~cmd2.Cmd.onecmd_plus_hooks()`{.interpreted-text role="meth"} method will do the following to execute a single command in a normal fashion: + +1. Parse user input into a `~cmd2.Statement`{.interpreted-text role="class"} object +2. Call methods registered with `~cmd2.Cmd.register_postparsing_hook()`{.interpreted-text role="meth"} +3. Redirect output, if user asked for it and it's allowed +4. Start timer +5. Call methods registered with `~cmd2.Cmd.register_precmd_hook`{.interpreted-text role="meth"} +6. Call `~cmd2.Cmd.precmd`{.interpreted-text role="meth"} - for backwards compatibility with `cmd.Cmd` +7. Add statement to `features/history:History`{.interpreted-text role="ref"} +8. Call ``do_command`` method +9. Call methods registered with `~cmd2.Cmd.register_postcmd_hook()`{.interpreted-text role="meth"} +10. Call `~cmd2.Cmd.postcmd`{.interpreted-text role="meth"} - for backwards compatibility with `cmd.Cmd` +11. Stop timer and display the elapsed time +12. Stop redirecting output if it was redirected +13. Call methods registered with `~cmd2.Cmd.register_cmdfinalization_hook()`{.interpreted-text role="meth"} + +Running in this fashion enables the ability to integrate with an external event loop. However, how to integrate with any specific event loop is beyond the scope of this documentation. Please note that running in this fashion comes with several disadvantages, including: + +- Requires the developer to write more code +- Does not support transcript testing +- Does not allow commands at invocation via command-line arguments diff --git a/docs/examples/alternate_event_loops.rst b/docs/examples/alternate_event_loops.rst deleted file mode 100644 index b0ee7f8ec..000000000 --- a/docs/examples/alternate_event_loops.rst +++ /dev/null @@ -1,78 +0,0 @@ -Alternate Event Loops -===================== - -Throughout this documentation we have focused on the **90%** use case, that is -the use case we believe around **90+%** of our user base is looking for. This -focuses on ease of use and the best out-of-the-box experience where developers -get the most functionality for the least amount of effort. We are talking -about running ``cmd2`` applications with the ``cmdloop()`` method:: - - from cmd2 import Cmd - class App(Cmd): - # customized attributes and methods here - app = App() - app.cmdloop() - -However, there are some limitations to this way of using ``cmd2``, mainly that -``cmd2`` owns the inner loop of a program. This can be unnecessarily -restrictive and can prevent using libraries which depend on controlling their -own event loop. - -Many Python concurrency libraries involve or require an event loop which they -are in control of such as asyncio_, gevent_, Twisted_, etc. - -.. _asyncio: https://docs.python.org/3/library/asyncio.html -.. _gevent: http://www.gevent.org/ -.. _Twisted: https://twistedmatrix.com - -``cmd2`` applications can be executed in a fashion where ``cmd2`` doesn't own -the main loop for the program by using code like the following:: - - import cmd2 - - class Cmd2EventBased(cmd2.Cmd): - def __init__(self): - cmd2.Cmd.__init__(self) - - # ... your class code here ... - - if __name__ == '__main__': - app = Cmd2EventBased() - app.preloop() - - # Do this within whatever event loop mechanism you wish to run a single command - cmd_line_text = "help history" - app.runcmds_plus_hooks([cmd_line_text]) - - app.postloop() - -The :meth:`~cmd2.Cmd.runcmds_plus_hooks()` method runs multiple commands via -:meth:`~cmd2.Cmd.onecmd_plus_hooks`. - -The :meth:`~cmd2.Cmd.onecmd_plus_hooks()` method will do the following to -execute a single command in a normal fashion: - -#. Parse user input into a :class:`~cmd2.Statement` object -#. Call methods registered with :meth:`~cmd2.Cmd.register_postparsing_hook()` -#. Redirect output, if user asked for it and it's allowed -#. Start timer -#. Call methods registered with :meth:`~cmd2.Cmd.register_precmd_hook` -#. Call :meth:`~cmd2.Cmd.precmd` - for backwards compatibility with ``cmd.Cmd`` -#. Add statement to :ref:`features/history:History` -#. Call `do_command` method -#. Call methods registered with :meth:`~cmd2.Cmd.register_postcmd_hook()` -#. Call :meth:`~cmd2.Cmd.postcmd` - for backwards compatibility with - ``cmd.Cmd`` -#. Stop timer and display the elapsed time -#. Stop redirecting output if it was redirected -#. Call methods registered with - :meth:`~cmd2.Cmd.register_cmdfinalization_hook()` - -Running in this fashion enables the ability to integrate with an external event -loop. However, how to integrate with any specific event loop is beyond the -scope of this documentation. Please note that running in this fashion comes -with several disadvantages, including: - -* Requires the developer to write more code -* Does not support transcript testing -* Does not allow commands at invocation via command-line arguments diff --git a/docs/examples/first_app.md b/docs/examples/first_app.md new file mode 100644 index 000000000..c48fb90dd --- /dev/null +++ b/docs/examples/first_app.md @@ -0,0 +1,227 @@ +# First Application + +Here's a quick walkthrough of a simple application which demonstrates 8 features of `cmd2`: + +- `features/settings:Settings`{.interpreted-text role="ref"} +- `features/commands:Commands`{.interpreted-text role="ref"} +- `features/argument_processing:Argument Processing`{.interpreted-text role="ref"} +- `features/generating_output:Generating Output`{.interpreted-text role="ref"} +- `features/help:Help`{.interpreted-text role="ref"} +- `features/shortcuts_aliases_macros:Shortcuts`{.interpreted-text role="ref"} +- `features/multiline_commands:Multiline Commands`{.interpreted-text role="ref"} +- `features/history:History`{.interpreted-text role="ref"} + +If you don't want to type as we go, you can download the complete source for this example. + +## Basic Application + +First we need to create a new `cmd2` application. Create a new file `first_app.py` with the following contents: + + #!/usr/bin/env python + """A simple cmd2 application.""" + import cmd2 + + + class FirstApp(cmd2.Cmd): + """A simple cmd2 application.""" + + + if __name__ == '__main__': + import sys + c = FirstApp() + sys.exit(c.cmdloop()) + +We have a new class `FirstApp` which is a subclass of `cmd2.Cmd`{.interpreted-text role="class"}. When we tell python to run our file like this: + +``` shell +$ python first_app.py +``` + +it creates an instance of our class, and calls the `~cmd2.Cmd.cmdloop`{.interpreted-text role="meth"} method. This method accepts user input and runs commands based on that input. Because we subclassed `cmd2.Cmd`{.interpreted-text role="class"}, our new app already has a bunch of features built in. + +Congratulations, you have a working `cmd2` app. You can run it, and then type `quit` to exit. + +## Create a New Setting + +Before we create our first command, we are going to add a setting to this app. `cmd2` includes robust support for `features/settings:Settings`{.interpreted-text role="ref"}. You configure settings during object initialization, so we need to add an initializer to our class: + + def __init__(self): + super().__init__() + + # Make maxrepeats settable at runtime + self.maxrepeats = 3 + self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) + +In that initializer, the first thing to do is to make sure we initialize `cmd2`. That's what the `super().__init__()` line does. Next create an attribute to hold the setting. Finally, call the `~cmd2.Cmd.add_settable`{.interpreted-text role="meth"} method with a new instance of a `~cmd2.utils.Settable`{.interpreted-text role="meth"} class. Now if you run the script, and enter the `set` command to see the settings, like this: + +``` shell +$ python first_app.py +(Cmd) set +``` + +you will see our `maxrepeats` setting show up with it's default value of `3`. + +## Create A Command + +Now we will create our first command, called `speak` which will echo back whatever we tell it to say. We are going to use an `argument processor +`{.interpreted-text role="ref"} so the `speak` command can shout and talk piglatin. We will also use some built in methods for `generating output `{.interpreted-text role="ref"}. Add this code to `first_app.py`, so that the `speak_parser` attribute and the `do_speak()` method are part of the `CmdLineApp()` class: + + speak_parser = cmd2.Cmd2ArgumentParser() + speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + speak_parser.add_argument('words', nargs='+', help='words to say') + + @cmd2.with_argparser(speak_parser) + def do_speak(self, args): + """Repeats what you tell me to.""" + words = [] + for word in args.words: + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + words.append(word) + repetitions = args.repeat or 1 + for _ in range(min(repetitions, self.maxrepeats)): + # .poutput handles newlines, and accommodates output redirection too + self.poutput(' '.join(words)) + +Up at the top of the script, you'll also need to add: + + import argparse + +There's a bit to unpack here, so let's walk through it. We created `speak_parser`, which uses the [argparse](https://docs.python.org/3/library/argparse.html) module from the Python standard library to parse command line input from a user. There is nothing thus far that is specific to `cmd2`. + +There is also a new method called `do_speak()`. In both [cmd](https://docs.python.org/3/library/cmd.html) and `cmd2`, methods that start with `do_` become new commands, so by defining this method we have created a command called `speak`. + +Note the `~cmd2.decorators.with_argparser`{.interpreted-text role="func"} decorator on the `do_speak()` method. This decorator does 3 useful things for us: + +1. It tells `cmd2` to process all input for the `speak` command using the argparser we defined. If the user input doesn't meet the requirements defined by the argparser, then an error will be displayed for the user. +2. It alters our `do_speak` method so that instead of receiving the raw user input as a parameter, we receive the namespace from the argparser. +3. It creates a help message for us based on the argparser. + +You can see in the body of the method how we use the namespace from the argparser (passed in as the variable `args`). We build an array of words which we will output, honoring both the `--piglatin` and `--shout` options. + +At the end of the method, we use our `maxrepeats` setting as an upper limit to the number of times we will print the output. + +The last thing you'll notice is that we used the `self.poutput()` method to display our output. `poutput()` is a method provided by `cmd2`, which I strongly recommend you use anytime you want to `generate output +`{.interpreted-text role="ref"}. It provides the following benefits: + +1. Allows the user to redirect output to a text file or pipe it to a shell process +2. Gracefully handles `BrokenPipeWarning` exceptions for redirected output +3. Makes the output show up in a `transcript + `{.interpreted-text role="ref"} +4. Honors the setting to `strip embedded ansi sequences + `{.interpreted-text role="ref"} (typically used for background and foreground colors) + +Go run the script again, and try out the `speak` command. Try typing `help speak`, and you will see a lovely usage message describing the various options for the command. + +With those few lines of code, we created a `command +`{.interpreted-text role="ref"}, used an `Argument Processor +`{.interpreted-text role="ref"}, added a nice `help +message `{.interpreted-text role="ref"} for our users, and `generated some output +`{.interpreted-text role="ref"}. + +## Shortcuts + +`cmd2` has several capabilities to simplify repetitive user input: `Shortcuts, Aliases, and Macros +`{.interpreted-text role="ref"}. Let's add a shortcut to our application. Shortcuts are character strings that can be used instead of a command name. For example, `cmd2` has support for a shortcut `!` which runs the `shell` command. So instead of typing this: + +``` shell +(Cmd) shell ls -al +``` + +you can type this: + +``` shell +(Cmd) !ls -al +``` + +Let's add a shortcut for our `speak` command. Change the `__init__()` method so it looks like this: + + def __init__(self): + shortcuts = cmd2.DEFAULT_SHORTCUTS + shortcuts.update({'&': 'speak'}) + super().__init__(shortcuts=shortcuts) + + # Make maxrepeats settable at runtime + self.maxrepeats = 3 + self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) + +Shortcuts are passed to the `cmd2` initializer, and if you want the built-in shortcuts of `cmd2` you have to pass them. These shortcuts are defined as a dictionary, with the key being the shortcut, and the value containing the command. When using the default shortcuts and also adding your own, it's a good idea to use the `.update()` method to modify the dictionary. This way if you add a shortcut that happens to already be in the default set, yours will override, and you won't get any errors at runtime. + +Run your app again, and type: + +``` shell +(Cmd) shortcuts +``` + +to see the list of all of the shortcuts, including the one for speak that we just created. + +## Multiline Commands + +Some use cases benefit from the ability to have commands that span more than one line. For example, you might want the ability for your user to type in a SQL command, which can often span lines and which are terminated with a semicolon. Let's add a `multiline command +`{.interpreted-text role="ref"} to our application. First we'll create a new command called `orate`. This code shows both the definition of our `speak` command, and the `orate` command: + + @cmd2.with_argparser(speak_parser) + def do_speak(self, args): + """Repeats what you tell me to.""" + words = [] + for word in args.words: + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + words.append(word) + repetitions = args.repeat or 1 + for _ in range(min(repetitions, self.maxrepeats)): + # .poutput handles newlines, and accommodates output redirection too + self.poutput(' '.join(words)) + + # orate is a synonym for speak which takes multiline input + do_orate = do_speak + +With the new command created, we need to tell `cmd2` to treat that command as a multi-line command. Modify the super initialization line to look like this: + + super().__init__(multiline_commands=['orate'], shortcuts=shortcuts) + +Now when you run the example, you can type something like this: + +``` shell +(Cmd) orate O for a Muse of fire, that would ascend +> The brightest heaven of invention, +> A kingdom for a stage, princes to act +> And monarchs to behold the swelling scene! ; +``` + +Notice the prompt changes to indicate that input is still ongoing. `cmd2` will continue prompting for input until it sees an unquoted semicolon (the default multi-line command termination character). + +## History + +`cmd2` tracks the history of the commands that users enter. As a developer, you don't need to do anything to enable this functionality, you get it for free. If you want the history of commands to persist between invocations of your application, you'll need to do a little work. The `features/history:History`{.interpreted-text role="ref"} page has all the details. + +Users can access command history using two methods: + +- the [readline](https://docs.python.org/3/library/readline.html) library which provides a python interface to the [GNU readline library](https://en.wikipedia.org/wiki/GNU_Readline) +- the `history` command which is built-in to `cmd2` + +From the prompt in a `cmd2`-based application, you can press `Control-p` to move to the previously entered command, and `Control-n` to move to the next command. You can also search through the command history using `Control-r`. The [GNU Readline User Manual](http://man7.org/linux/man-pages/man3/readline.3.html) has all the details, including all the available commands, and instructions for customizing the key bindings. + +The `history` command allows a user to view the command history, and select commands from history by number, range, string search, or regular expression. With the selected commands, users can: + +- re-run the commands +- edit the selected commands in a text editor, and run them after the text editor exits +- save the commands to a file +- run the commands, saving both the commands and their output to a file + +Learn more about the `history` command by typing `history -h` at any `cmd2` input prompt, or by exploring `Command History For Users +`{.interpreted-text role="ref"}. + +## Conclusion + +You've just created a simple, but functional command line application. With minimal work on your part, the application leverages many robust features of `cmd2`. To learn more you can: + +- Dive into all of the `../features/index`{.interpreted-text role="doc"} that `cmd2` provides +- Look at more `../examples/index`{.interpreted-text role="doc"} +- Browse the `../api/index`{.interpreted-text role="doc"} diff --git a/docs/examples/first_app.rst b/docs/examples/first_app.rst deleted file mode 100644 index d90f96d86..000000000 --- a/docs/examples/first_app.rst +++ /dev/null @@ -1,323 +0,0 @@ -First Application -================= - -.. _cmd: https://docs.python.org/3/library/cmd.html - -Here's a quick walkthrough of a simple application which demonstrates 8 -features of ``cmd2``: - -* :ref:`features/settings:Settings` -* :ref:`features/commands:Commands` -* :ref:`features/argument_processing:Argument Processing` -* :ref:`features/generating_output:Generating Output` -* :ref:`features/help:Help` -* :ref:`features/shortcuts_aliases_macros:Shortcuts` -* :ref:`features/multiline_commands:Multiline Commands` -* :ref:`features/history:History` - -If you don't want to type as we go, you can download the complete source for -this example. - - -Basic Application ------------------ - -First we need to create a new ``cmd2`` application. Create a new file -``first_app.py`` with the following contents:: - - #!/usr/bin/env python - """A simple cmd2 application.""" - import cmd2 - - - class FirstApp(cmd2.Cmd): - """A simple cmd2 application.""" - - - if __name__ == '__main__': - import sys - c = FirstApp() - sys.exit(c.cmdloop()) - -We have a new class ``FirstApp`` which is a subclass of -:class:`cmd2.Cmd`. When we tell python to run our file like this: - -.. code-block:: shell - - $ python first_app.py - -it creates an instance of our class, and calls the :meth:`~cmd2.Cmd.cmdloop` -method. This method accepts user input and runs commands based on that input. -Because we subclassed :class:`cmd2.Cmd`, our new app already has a bunch of -features built in. - -Congratulations, you have a working ``cmd2`` app. You can run it, and then type -``quit`` to exit. - - -Create a New Setting --------------------- - -Before we create our first command, we are going to add a setting to this app. -``cmd2`` includes robust support for :ref:`features/settings:Settings`. You -configure settings during object initialization, so we need to add an -initializer to our class:: - - def __init__(self): - super().__init__() - - # Make maxrepeats settable at runtime - self.maxrepeats = 3 - self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) - -In that initializer, the first thing to do is to make sure we initialize -``cmd2``. That's what the ``super().__init__()`` line does. Next create an -attribute to hold the setting. Finally, call the :meth:`~cmd2.Cmd.add_settable` -method with a new instance of a :meth:`~cmd2.utils.Settable` class. Now if you -run the script, and enter the ``set`` command to see the settings, like this: - -.. code-block:: shell - - $ python first_app.py - (Cmd) set - -you will see our ``maxrepeats`` setting show up with it's default value of -``3``. - - -Create A Command ----------------- - -Now we will create our first command, called ``speak`` which will echo back -whatever we tell it to say. We are going to use an :ref:`argument processor -` so the ``speak`` command -can shout and talk piglatin. We will also use some built in methods for -:ref:`generating output `. Add -this code to ``first_app.py``, so that the ``speak_parser`` attribute and the -``do_speak()`` method are part of the ``CmdLineApp()`` class:: - - speak_parser = cmd2.Cmd2ArgumentParser() - speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - speak_parser.add_argument('words', nargs='+', help='words to say') - - @cmd2.with_argparser(speak_parser) - def do_speak(self, args): - """Repeats what you tell me to.""" - words = [] - for word in args.words: - if args.piglatin: - word = '%s%say' % (word[1:], word[0]) - if args.shout: - word = word.upper() - words.append(word) - repetitions = args.repeat or 1 - for _ in range(min(repetitions, self.maxrepeats)): - # .poutput handles newlines, and accommodates output redirection too - self.poutput(' '.join(words)) - -Up at the top of the script, you'll also need to add:: - - import argparse - -There's a bit to unpack here, so let's walk through it. We created -``speak_parser``, which uses the `argparse -`_ module from the Python -standard library to parse command line input from a user. There is nothing thus -far that is specific to ``cmd2``. - -There is also a new method called ``do_speak()``. In both cmd_ and ``cmd2``, -methods that start with ``do_`` become new commands, so by defining this method -we have created a command called ``speak``. - -Note the :func:`~cmd2.decorators.with_argparser` decorator on the -``do_speak()`` method. This decorator does 3 useful things for us: - -1. It tells ``cmd2`` to process all input for the ``speak`` command using the - argparser we defined. If the user input doesn't meet the requirements - defined by the argparser, then an error will be displayed for the user. -2. It alters our ``do_speak`` method so that instead of receiving the raw user - input as a parameter, we receive the namespace from the argparser. -3. It creates a help message for us based on the argparser. - -You can see in the body of the method how we use the namespace from the -argparser (passed in as the variable ``args``). We build an array of words -which we will output, honoring both the ``--piglatin`` and ``--shout`` options. - -At the end of the method, we use our ``maxrepeats`` setting as an upper limit -to the number of times we will print the output. - -The last thing you'll notice is that we used the ``self.poutput()`` method to -display our output. ``poutput()`` is a method provided by ``cmd2``, which I -strongly recommend you use anytime you want to :ref:`generate output -`. It provides the following -benefits: - -1. Allows the user to redirect output to a text file or pipe it to a shell - process -2. Gracefully handles ``BrokenPipeWarning`` exceptions for redirected output -3. Makes the output show up in a :ref:`transcript - ` -4. Honors the setting to :ref:`strip embedded ansi sequences - ` (typically used for background and - foreground colors) - -Go run the script again, and try out the ``speak`` command. Try typing ``help -speak``, and you will see a lovely usage message describing the various options -for the command. - -With those few lines of code, we created a :ref:`command -`, used an :ref:`Argument Processor -`, added a nice :ref:`help -message ` for our users, and :ref:`generated some output -`. - - -Shortcuts ---------- - -``cmd2`` has several capabilities to simplify repetitive user input: -:ref:`Shortcuts, Aliases, and Macros -`. Let's add -a shortcut to our application. Shortcuts are character strings that can be used -instead of a command name. For example, ``cmd2`` has support for a shortcut -``!`` which runs the ``shell`` command. So instead of typing this: - -.. code-block:: shell - - (Cmd) shell ls -al - -you can type this: - -.. code-block:: shell - - (Cmd) !ls -al - -Let's add a shortcut for our ``speak`` command. Change the ``__init__()`` -method so it looks like this:: - - def __init__(self): - shortcuts = cmd2.DEFAULT_SHORTCUTS - shortcuts.update({'&': 'speak'}) - super().__init__(shortcuts=shortcuts) - - # Make maxrepeats settable at runtime - self.maxrepeats = 3 - self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) - -Shortcuts are passed to the ``cmd2`` initializer, and if you want the built-in -shortcuts of ``cmd2`` you have to pass them. These shortcuts are defined as a -dictionary, with the key being the shortcut, and the value containing the -command. When using the default shortcuts and also adding your own, it's a good -idea to use the ``.update()`` method to modify the dictionary. This way if you -add a shortcut that happens to already be in the default set, yours will -override, and you won't get any errors at runtime. - -Run your app again, and type: - -.. code-block:: shell - - (Cmd) shortcuts - -to see the list of all of the shortcuts, including the one for speak that we -just created. - - -Multiline Commands ------------------- - -Some use cases benefit from the ability to have commands that span more than -one line. For example, you might want the ability for your user to type in a -SQL command, which can often span lines and which are terminated with a -semicolon. Let's add a :ref:`multiline command -` to our application. First -we'll create a new command called ``orate``. This code shows both the -definition of our ``speak`` command, and the ``orate`` command:: - - @cmd2.with_argparser(speak_parser) - def do_speak(self, args): - """Repeats what you tell me to.""" - words = [] - for word in args.words: - if args.piglatin: - word = '%s%say' % (word[1:], word[0]) - if args.shout: - word = word.upper() - words.append(word) - repetitions = args.repeat or 1 - for _ in range(min(repetitions, self.maxrepeats)): - # .poutput handles newlines, and accommodates output redirection too - self.poutput(' '.join(words)) - - # orate is a synonym for speak which takes multiline input - do_orate = do_speak - -With the new command created, we need to tell ``cmd2`` to treat that command as -a multi-line command. Modify the super initialization line to look like this:: - - super().__init__(multiline_commands=['orate'], shortcuts=shortcuts) - -Now when you run the example, you can type something like this: - -.. code-block:: shell - - (Cmd) orate O for a Muse of fire, that would ascend - > The brightest heaven of invention, - > A kingdom for a stage, princes to act - > And monarchs to behold the swelling scene! ; - -Notice the prompt changes to indicate that input is still ongoing. ``cmd2`` -will continue prompting for input until it sees an unquoted semicolon (the -default multi-line command termination character). - - -History -------- - -``cmd2`` tracks the history of the commands that users enter. As a developer, -you don't need to do anything to enable this functionality, you get it for -free. If you want the history of commands to persist between invocations of -your application, you'll need to do a little work. The -:ref:`features/history:History` page has all the details. - -Users can access command history using two methods: - -- the `readline `_ library - which provides a python interface to the `GNU readline library - `_ -- the ``history`` command which is built-in to ``cmd2`` - -From the prompt in a ``cmd2``-based application, you can press ``Control-p`` to -move to the previously entered command, and ``Control-n`` to move to the next -command. You can also search through the command history using ``Control-r``. -The `GNU Readline User Manual -`_ has all the -details, including all the available commands, and instructions for customizing -the key bindings. - -The ``history`` command allows a user to view the command history, and select -commands from history by number, range, string search, or regular expression. -With the selected commands, users can: - -- re-run the commands -- edit the selected commands in a text editor, and run them after the text - editor exits -- save the commands to a file -- run the commands, saving both the commands and their output to a file - -Learn more about the ``history`` command by typing ``history -h`` at any -``cmd2`` input prompt, or by exploring :ref:`Command History For Users -`. - - -Conclusion ----------- - -You've just created a simple, but functional command line application. With -minimal work on your part, the application leverages many robust features of -``cmd2``. To learn more you can: - -- Dive into all of the :doc:`../features/index` that ``cmd2`` provides -- Look at more :doc:`../examples/index` -- Browse the :doc:`../api/index` diff --git a/docs/examples/index.md b/docs/examples/index.md new file mode 100644 index 000000000..077c63c13 --- /dev/null +++ b/docs/examples/index.md @@ -0,0 +1,8 @@ +# Examples + + + +- [First Application](first_app.md) +- [Alternate Event Loops](alternate_event_loops.md) + + diff --git a/docs/examples/index.rst b/docs/examples/index.rst deleted file mode 100644 index 94a3e06fc..000000000 --- a/docs/examples/index.rst +++ /dev/null @@ -1,8 +0,0 @@ -Examples -======== - -.. toctree:: - :maxdepth: 1 - - first_app - alternate_event_loops diff --git a/docs/features/argument_processing.md b/docs/features/argument_processing.md new file mode 100644 index 000000000..7c7073308 --- /dev/null +++ b/docs/features/argument_processing.md @@ -0,0 +1,303 @@ +# Argument Processing + +`cmd2` makes it easy to add sophisticated argument processing to your commands using the [argparse](https://docs.python.org/3/library/argparse.html) python module. `cmd2` handles the following for you: + +1. Parsing input and quoted strings like the Unix shell +1. Parse the resulting argument list using an instance of `argparse.ArgumentParser` that you provide +1. Passes the resulting `argparse.Namespace` object to your command function. The `Namespace` includes the `Statement` object that was created when parsing the command line. It can be retrieved by calling `cmd2_statement.get()` on the `Namespace`. +1. Adds the usage message from the argument parser to your command. +1. Checks if the `-h/--help` option is present, and if so, display the help message for the command + +These features are all provided by the `@with_argparser` decorator which is importable from `cmd2`. + +See the either the [argprint](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) or [decorator](https://github.com/python-cmd2/cmd2/blob/master/examples/decorator_example.py) example to learn more about how to use the various `cmd2` argument processing decorators in your `cmd2` applications. + +`cmd2` provides the following [decorators](../api/decorators.md) for assisting with parsing arguments passed to commands: + +- `cmd2.decorators.with_argparser` +- `cmd2.decorators.with_argument_list` + +All of these decorators accept an optional **preserve_quotes** argument which defaults to `False`. Setting this argument to `True` is useful for cases where you are passing the arguments to another command which might have its own argument parsing. + +## Argument Parsing + +For each command in the `cmd2` subclass which requires argument parsing, create a unique instance of `argparse.ArgumentParser()` which can parse the input appropriately for the command. Then decorate the command method with the `@with_argparser` decorator, passing the argument parser as the first parameter to the decorator. This changes the second argument to the command method, which will contain the results of `ArgumentParser.parse_args()`. + +Here's what it looks like: + +```py +from cmd2 import Cmd2ArgumentParser, with_argparser + +argparser = Cmd2ArgumentParser() +argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') +argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') +argparser.add_argument('-r', '--repeat', type=int, help='output [n] times') +argparser.add_argument('word', nargs='?', help='word to say') + +@with_argparser(argparser) +def do_speak(self, opts) + """Repeats what you tell me to.""" + arg = opts.word + if opts.piglatin: + arg = '%s%say' % (arg[1:], arg[0]) + if opts.shout: + arg = arg.upper() + repetitions = opts.repeat or 1 + for i in range(min(repetitions, self.maxrepeats)): + self.poutput(arg) +``` + +!!! note + + The `@with_argparser` decorator sets the `prog` variable in the argument parser based on the name of the method it is decorating. This will override anything you specify in `prog` variable when creating the argument parser. + +## Help Messages + +By default, `cmd2` uses the docstring of the command method when a user asks for help on the command. When you use the `@with_argparser` decorator, the docstring for the `do_*` method is used to set the description for the `argparse.ArgumentParser`. + +With this code: + +```py +from cmd2 import Cmd2ArgumentParser, with_argparser + +argparser = Cmd2ArgumentParser() +argparser.add_argument('tag', help='tag') +argparser.add_argument('content', nargs='+', help='content to surround with tag') +@with_argparser(argparser) +def do_tag(self, args): + """create a html tag""" + self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) + self.stdout.write('\n') +``` + +the `help tag` command displays: + +```text +usage: tag [-h] tag content [content ...] + +create a html tag + +positional arguments: + tag tag + content content to surround with tag + +optional arguments: + -h, --help show this help message and exit +``` + +If you would prefer you can set the `description` while instantiating the `argparse.ArgumentParser` and leave the docstring on your method empty: + +```py +from cmd2 import Cmd2ArgumentParser, with_argparser + +argparser = Cmd2ArgumentParser(description='create an html tag') +argparser.add_argument('tag', help='tag') +argparser.add_argument('content', nargs='+', help='content to surround with tag') +@with_argparser(argparser) +def do_tag(self, args): + self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) + self.stdout.write('\n') +``` + +Now when the user enters `help tag` they see: + +```text +usage: tag [-h] tag content [content ...] + +create an html tag + +positional arguments: + tag tag + content content to surround with tag + +optional arguments: + -h, --help show this help message and exit +``` + +To add additional text to the end of the generated help message, use the `epilog` variable: + +```py +from cmd2 import Cmd2ArgumentParser, with_argparser + +argparser = Cmd2ArgumentParser(description='create an html tag', + epilog='This command cannot generate tags with no content, like
.') +argparser.add_argument('tag', help='tag') +argparser.add_argument('content', nargs='+', help='content to surround with tag') +@with_argparser(argparser) +def do_tag(self, args): + self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) + self.stdout.write('\n') +``` + +Which yields: + +```text +usage: tag [-h] tag content [content ...] + +create an html tag + +positional arguments: + tag tag + content content to surround with tag + +optional arguments: + -h, --help show this help message and exit + +This command cannot generate tags with no content, like
+``` + +!!! warning + + If a command **foo** is decorated with one of cmd2's argparse decorators, then **help_foo** will not be invoked when `help foo` is called. The [argparse](https://docs.python.org/3/library/argparse.html) module provides a rich API which can be used to tweak every aspect of the displayed help and we encourage `cmd2` developers to utilize that. + +## Argument List + +The default behavior of `cmd2` is to pass the user input directly to your `do_*` methods as a string. The object passed to your method is actually a `Statement` object, which has additional attributes that may be helpful, including `arg_list` and `argv`: + +```py +class CmdLineApp(cmd2.Cmd): + """ Example cmd2 application. """ + + def do_say(self, statement): + # statement contains a string + self.poutput(statement) + + def do_speak(self, statement): + # statement also has a list of arguments + # quoted arguments remain quoted + for arg in statement.arg_list: + self.poutput(arg) + + def do_articulate(self, statement): + # statement.argv contains the command + # and the arguments, which have had quotes + # stripped + for arg in statement.argv: + self.poutput(arg) +``` + +If you don't want to access the additional attributes on the string passed to you`do_*` method you can still have `cmd2` apply shell parsing rules to the user input and pass you a list of arguments instead of a string. Apply the `@with_argument_list` decorator to those methods that should receive an argument list instead of a string: + +```py +from cmd2 import with_argument_list + +class CmdLineApp(cmd2.Cmd): + """ Example cmd2 application. """ + + def do_say(self, cmdline): + # cmdline contains a string + pass + + @with_argument_list + def do_speak(self, arglist): + # arglist contains a list of arguments + pass +``` + +## Unknown Positional Arguments + +If you want all unknown arguments to be passed to your command as a list of strings, then decorate the command method with the `@with_argparser(..., with_unknown_args=True)` decorator. + +Here's what it looks like: + +```py +from cmd2 import Cmd2ArgumentParser, with_argparser + +dir_parser = Cmd2ArgumentParser() +dir_parser.add_argument('-l', '--long', action='store_true', + help="display in long format with one item per line") + +@with_argparser(dir_parser, with_unknown_args=True) +def do_dir(self, args, unknown): + """List contents of current directory.""" + # No arguments for this command + if unknown: + self.perror("dir does not take any positional arguments:") + self.do_help('dir') + self.last_result = 'Bad arguments' + return + + # Get the contents as a list + contents = os.listdir(self.cwd) + + ... +``` + +## Using A Custom Namespace + +In some cases, it may be necessary to write custom `argparse` code that is dependent on state data of your application. To support this ability while still allowing use of the decorators, `@with_argparser` has an optional argument called `ns_provider`. + +`ns_provider` is a Callable that accepts a `cmd2.Cmd` object as an argument and returns an `argparse.Namespace`: + +```py +Callable[[cmd2.Cmd], argparse.Namespace] +``` + +For example: + +```py +def settings_ns_provider(self) -> argparse.Namespace: + """Populate an argparse Namespace with current settings""" + ns = argparse.Namespace() + ns.app_settings = self.settings + return ns +``` + +To use this function with the argparse decorators, do the following: + +```py +@with_argparser(my_parser, ns_provider=settings_ns_provider) +``` + +The Namespace is passed by the decorators to the `argparse` parsing functions which gives your custom code access to the state data it needs for its parsing logic. + +## Subcommands + +Subcommands are supported for commands using the `@with_argparser` decorator. The syntax is based on argparse sub-parsers. + +You may add multiple layers of subcommands for your command. `cmd2` will automatically traverse and tab complete subcommands for all commands using argparse. + +See the [subcommands](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) example to learn more about how to use subcommands in your `cmd2` application. + +## Argparse Extensions + +`cmd2` augments the standard `argparse.nargs` with range tuple capability: + +- `nargs=(5,)` - accept 5 or more items +- `nargs=(8, 12)` - accept 8 to 12 items + +`cmd2` also provides the `cmd2.argparse_custom.Cmd2ArgumentParser` class which inherits from `argparse.ArgumentParser` and improves error and help output. + +## Decorator Order + +If you are using custom decorators in combination with `@cmd2.with_argparser`, then the order of your custom decorator(s) relative to the `cmd2` decorator matters when it comes to runtime behavior and `argparse` errors. There is nothing `cmd2`-specific here, this is just a side-effect of how decorators work in Python. To learn more about how decorators work, see [decorator_primer](https://realpython.com/primer-on-python-decorators). + +If you want your custom decorator's runtime behavior to occur in the case of an `argparse` error, then that decorator needs to go **after** the `argparse` one, e.g.: + +```py +@cmd2.with_argparser(foo_parser) +@my_decorator +def do_foo(self, args: argparse.Namespace) -> None: + """foo docs""" + pass +``` + +However, if you do NOT want the custom decorator runtime behavior to occur even in the case of an `argparse` error, then that decorator needs to go **before** the `arpgarse` one, e.g.: + +```py +@my_decorator +@cmd2.with_argparser(bar_parser) +def do_bar(self, args: argparse.Namespace) -> None: + """bar docs""" + pass +``` + +The [help_categories](https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py) example demonstrates both above cases in a concrete fashion. + +## Reserved Argument Names + +`cmd2` argparse decorators add the following attributes to argparse Namespaces. To avoid naming collisions, do not use any of the names for your argparse arguments. + +- `cmd2_statement` - `cmd2.Cmd2AttributeWrapper` object containing `cmd2.Statement` object that was created when parsing the command line. +- `cmd2_handler` - `cmd2.Cmd2AttributeWrapper` object containing a subcommand handler function or `None` if one was not set. +- `__subcmd_handler__` - used by cmd2 to identify the handler for a subcommand created with `@cmd2.as_subcommand_to` decorator. diff --git a/docs/features/argument_processing.rst b/docs/features/argument_processing.rst deleted file mode 100644 index d60904659..000000000 --- a/docs/features/argument_processing.rst +++ /dev/null @@ -1,377 +0,0 @@ -Argument Processing -=================== - -``cmd2`` makes it easy to add sophisticated argument processing to your -commands using the `argparse -`_ python module. ``cmd2`` -handles the following for you: - -1. Parsing input and quoted strings like the Unix shell - -2. Parse the resulting argument list using an instance of - ``argparse.ArgumentParser`` that you provide - -3. Passes the resulting ``argparse.Namespace`` object to your command function. - The ``Namespace`` includes the ``Statement`` object that was created when - parsing the command line. It can be retrieved by calling - ``cmd2_statement.get()`` on the ``Namespace``. - -4. Adds the usage message from the argument parser to your command. - -5. Checks if the ``-h/--help`` option is present, and if so, display the help - message for the command - -These features are all provided by the ``@with_argparser`` decorator which is -importable from ``cmd2``. - -See the either the argprint_ or decorator_ example to learn more about how to -use the various ``cmd2`` argument processing decorators in your ``cmd2`` -applications. - -.. _argprint: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py -.. _decorator: https://github.com/python-cmd2/cmd2/blob/master/examples/decorator_example.py - -``cmd2`` provides the following decorators for assisting with parsing arguments -passed to commands: - -* :func:`cmd2.decorators.with_argparser` -* :func:`cmd2.decorators.with_argument_list` - -All of these decorators accept an optional **preserve_quotes** argument which -defaults to ``False``. Setting this argument to ``True`` is useful for cases -where you are passing the arguments to another command which might have its own -argument parsing. - - -Argument Parsing ----------------- - -For each command in the ``cmd2`` subclass which requires argument parsing, -create a unique instance of ``argparse.ArgumentParser()`` which can parse the -input appropriately for the command. Then decorate the command method with the -``@with_argparser`` decorator, passing the argument parser as the first -parameter to the decorator. This changes the second argument to the command -method, which will contain the results of ``ArgumentParser.parse_args()``. - -Here's what it looks like:: - - from cmd2 import Cmd2ArgumentParser, with_argparser - - argparser = Cmd2ArgumentParser() - argparser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - argparser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - argparser.add_argument('-r', '--repeat', type=int, help='output [n] times') - argparser.add_argument('word', nargs='?', help='word to say') - - @with_argparser(argparser) - def do_speak(self, opts) - """Repeats what you tell me to.""" - arg = opts.word - if opts.piglatin: - arg = '%s%say' % (arg[1:], arg[0]) - if opts.shout: - arg = arg.upper() - repetitions = opts.repeat or 1 - for i in range(min(repetitions, self.maxrepeats)): - self.poutput(arg) - -.. note:: - - The ``@with_argparser`` decorator sets the ``prog`` variable in the argument - parser based on the name of the method it is decorating. This will override - anything you specify in ``prog`` variable when creating the argument parser. - - -Help Messages -------------- - -By default, ``cmd2`` uses the docstring of the command method when a user asks -for help on the command. When you use the ``@with_argparser`` decorator, the -docstring for the ``do_*`` method is used to set the description for the -``argparse.ArgumentParser``. - -With this code:: - - from cmd2 import Cmd2ArgumentParser, with_argparser - - argparser = Cmd2ArgumentParser() - argparser.add_argument('tag', help='tag') - argparser.add_argument('content', nargs='+', help='content to surround with tag') - @with_argparser(argparser) - def do_tag(self, args): - """create a html tag""" - self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) - self.stdout.write('\n') - -the ``help tag`` command displays: - -.. code-block:: text - - usage: tag [-h] tag content [content ...] - - create a html tag - - positional arguments: - tag tag - content content to surround with tag - - optional arguments: - -h, --help show this help message and exit - - -If you would prefer you can set the ``description`` while instantiating the -``argparse.ArgumentParser`` and leave the docstring on your method empty:: - - from cmd2 import Cmd2ArgumentParser, with_argparser - - argparser = Cmd2ArgumentParser(description='create an html tag') - argparser.add_argument('tag', help='tag') - argparser.add_argument('content', nargs='+', help='content to surround with tag') - @with_argparser(argparser) - def do_tag(self, args): - self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) - self.stdout.write('\n') - -Now when the user enters ``help tag`` they see: - -.. code-block:: text - - usage: tag [-h] tag content [content ...] - - create an html tag - - positional arguments: - tag tag - content content to surround with tag - - optional arguments: - -h, --help show this help message and exit - - -To add additional text to the end of the generated help message, use the ``epilog`` variable:: - - from cmd2 import Cmd2ArgumentParser, with_argparser - - argparser = Cmd2ArgumentParser(description='create an html tag', - epilog='This command cannot generate tags with no content, like
.') - argparser.add_argument('tag', help='tag') - argparser.add_argument('content', nargs='+', help='content to surround with tag') - @with_argparser(argparser) - def do_tag(self, args): - self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) - self.stdout.write('\n') - -Which yields: - -.. code-block:: text - - usage: tag [-h] tag content [content ...] - - create an html tag - - positional arguments: - tag tag - content content to surround with tag - - optional arguments: - -h, --help show this help message and exit - - This command cannot generate tags with no content, like
- -.. warning:: - - If a command **foo** is decorated with one of cmd2's argparse decorators, - then **help_foo** will not be invoked when ``help foo`` is called. The - argparse_ module provides a rich API which can be used to tweak every - aspect of the displayed help and we encourage ``cmd2`` developers to - utilize that. - -.. _argparse: https://docs.python.org/3/library/argparse.html - - -Argument List -------------- - -The default behavior of ``cmd2`` is to pass the user input directly to your -``do_*`` methods as a string. The object passed to your method is actually a -``Statement`` object, which has additional attributes that may be helpful, -including ``arg_list`` and ``argv``:: - - class CmdLineApp(cmd2.Cmd): - """ Example cmd2 application. """ - - def do_say(self, statement): - # statement contains a string - self.poutput(statement) - - def do_speak(self, statement): - # statement also has a list of arguments - # quoted arguments remain quoted - for arg in statement.arg_list: - self.poutput(arg) - - def do_articulate(self, statement): - # statement.argv contains the command - # and the arguments, which have had quotes - # stripped - for arg in statement.argv: - self.poutput(arg) - - -If you don't want to access the additional attributes on the string passed to -you``do_*`` method you can still have ``cmd2`` apply shell parsing rules to the -user input and pass you a list of arguments instead of a string. Apply the -``@with_argument_list`` decorator to those methods that should receive an -argument list instead of a string:: - - from cmd2 import with_argument_list - - class CmdLineApp(cmd2.Cmd): - """ Example cmd2 application. """ - - def do_say(self, cmdline): - # cmdline contains a string - pass - - @with_argument_list - def do_speak(self, arglist): - # arglist contains a list of arguments - pass - - -Unknown Positional Arguments ----------------------------- - -If you want all unknown arguments to be passed to your command as a list of -strings, then decorate the command method with the -``@with_argparser(..., with_unknown_args=True)`` decorator. - -Here's what it looks like:: - - from cmd2 import Cmd2ArgumentParser, with_argparser - - dir_parser = Cmd2ArgumentParser() - dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") - - @with_argparser(dir_parser, with_unknown_args=True) - def do_dir(self, args, unknown): - """List contents of current directory.""" - # No arguments for this command - if unknown: - self.perror("dir does not take any positional arguments:") - self.do_help('dir') - self.last_result = 'Bad arguments' - return - - # Get the contents as a list - contents = os.listdir(self.cwd) - - ... - -Using A Custom Namespace ------------------------- - -In some cases, it may be necessary to write custom ``argparse`` code that is -dependent on state data of your application. To support this ability while -still allowing use of the decorators, ``@with_argparser`` has an optional -argument called ``ns_provider``. - -``ns_provider`` is a Callable that accepts a ``cmd2.Cmd`` object as an argument -and returns an ``argparse.Namespace``:: - - Callable[[cmd2.Cmd], argparse.Namespace] - -For example:: - - def settings_ns_provider(self) -> argparse.Namespace: - """Populate an argparse Namespace with current settings""" - ns = argparse.Namespace() - ns.app_settings = self.settings - return ns - -To use this function with the argparse decorators, do the following:: - - @with_argparser(my_parser, ns_provider=settings_ns_provider) - -The Namespace is passed by the decorators to the ``argparse`` parsing functions -which gives your custom code access to the state data it needs for its parsing -logic. - -Subcommands ------------- - -Subcommands are supported for commands using the ``@with_argparser`` decorator. -The syntax is based on argparse sub-parsers. - -You may add multiple layers of subcommands for your command. ``cmd2`` will -automatically traverse and tab complete subcommands for all commands using -argparse. - -See the subcommands_ example to learn more about how to -use subcommands in your ``cmd2`` application. - -.. _subcommands: https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py - - -Argparse Extensions -------------------- - -``cmd2`` augments the standard ``argparse.nargs`` with range tuple capability: - -- ``nargs=(5,)`` - accept 5 or more items -- ``nargs=(8, 12)`` - accept 8 to 12 items - -``cmd2`` also provides the :class:`cmd2.argparse_custom.Cmd2ArgumentParser` -class which inherits from ``argparse.ArgumentParser`` and improves error and -help output. - - -Decorator Order ---------------- - -If you are using custom decorators in combination with -``@cmd2.with_argparser``, then the -order of your custom decorator(s) relative to the ``cmd2`` decorator matters -when it comes to runtime behavior and ``argparse`` errors. There is nothing -``cmd2``-specific here, this is just a side-effect of how decorators work in -Python. To learn more about how decorators work, see decorator_primer_. - -If you want your custom decorator's runtime behavior to occur in the case of -an ``argparse`` error, then that decorator needs to go **after** the -``argparse`` one, e.g.:: - - @cmd2.with_argparser(foo_parser) - @my_decorator - def do_foo(self, args: argparse.Namespace) -> None: - """foo docs""" - pass - -However, if you do NOT want the custom decorator runtime behavior to occur -even in the case of an `argparse` error, then that decorator needs to go -**before** the ``arpgarse`` one, e.g.:: - - @my_decorator - @cmd2.with_argparser(bar_parser) - def do_bar(self, args: argparse.Namespace) -> None: - """bar docs""" - pass - -The help_categories_ example demonstrates both above cases in a concrete -fashion. - -.. _decorator_primer: https://realpython.com/primer-on-python-decorators -.. _help_categories: https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py - - -Reserved Argument Names ------------------------ -``cmd2`` argparse decorators add the following attributes to argparse -Namespaces. To avoid naming collisions, do not use any of the names for your -argparse arguments. - -- ``cmd2_statement`` - ``cmd2.Cmd2AttributeWrapper`` object containing - ``cmd2.Statement`` object that was created when parsing the command line. -- ``cmd2_handler`` - ``cmd2.Cmd2AttributeWrapper`` object containing - a subcommand handler function or ``None`` if one was not set. -- ``__subcmd_handler__`` - used by cmd2 to identify the handler for a - subcommand created with ``@cmd2.as_subcommand_to`` decorator. diff --git a/docs/features/builtin_commands.md b/docs/features/builtin_commands.md new file mode 100644 index 000000000..9e9861423 --- /dev/null +++ b/docs/features/builtin_commands.md @@ -0,0 +1,111 @@ +# Builtin Commands + +Applications which subclass `cmd2.Cmd` inherit a number of commands which may be useful to your users. Developers can [Remove Builtin Commands](#remove-builtin-commands) if they do not want them to be part of the application. + +## List of Builtin Commands + +### alias + +This command manages aliases via subcommands `create`, `delete`, and `list`. See [Aliases](shortcuts_aliases_macros.md#aliases) for more information. + +### edit + +This command launches an editor program and instructs it to open the given file name. Here's an example: + +```sh +(Cmd) edit ~/.ssh/config +``` + +The program to be launched is determined by the value of the [editor](settings.md#editor) setting. + +### help + +This command lists available commands or provides detailed help for a specific command. When called with the `-v/--verbose` argument, it shows a brief description of each command. See [Help](help.md) for more information. + +### history + +This command allows you to view, run, edit, save, or clear previously entered commands from the history. See [History](history.md) for more information. + +### ipy + +This optional opt-in command enters an interactive IPython shell. See [IPython (optional)](./embedded_python_shells.md#ipython-optional) for more information. + +### macro + +This command manages macros via subcommands `create`, `delete`, and `list`. A macro is similar to an alias, but it can contain argument placeholders. See [Macros](./shortcuts_aliases_macros.md#macros) for more information. + +### py + +This command invokes a Python command or shell. See [Embedded Python Shells](./embedded_python_shells.md) for more information. + +### quit + +This command exits the `cmd2` application. + +### run_pyscript {: #feature-builtin-commands-run-pyscript } + +This command runs a Python script file inside the `cmd2` application. See [Python Scripts](./scripting.md#scripting-python-scripts) for more information. + +### run_script + +This command runs commands in a script file that is encoded as either ASCII or UTF-8 text. See [Command Scripts](./scripting.md#command-scripts) for more information. + +### \_relative_run_script + +This command is hidden from the help that's visible to end users. It runs a script like [run_script](#run_script) but does so using a path relative to the script that is currently executing. This is useful when you have scripts that run other scripts. See `features/scripting:Running +Command Scripts`{.interpreted-text role="ref"} for more information. + +### set + +A list of all user-settable parameters, with brief comments, is viewable from within a running application: + +```text +(Cmd) set +Name Value Description +==================================================================================================================== +allow_style Terminal Allow ANSI text style sequences in output (valid values: + Always, Never, Terminal) +always_show_hint False Display tab completion hint even when completion suggestions + print +debug True Show full traceback on exception +echo False Echo command issued into output +editor vi Program used by 'edit' +feedback_to_output False Include nonessentials in '|', '>' results +max_completion_items 50 Maximum number of CompletionItems to display during tab + completion +quiet False Don't print nonessential feedback +scripts_add_to_history True Scripts and pyscripts add commands to history +timing False Report execution times +``` + +Any of these user-settable parameters can be set while running your app with the `set` command like so: + +```text +(Cmd) set allow_style Never +``` + +See [Settings](./settings.md) for more information. + +### shell + +Execute a command as if at the operating system shell prompt: + +```text +(Cmd) shell pwd -P +/usr/local/bin +``` + +### shortcuts + +This command lists available shortcuts. See [Shortcuts](./shortcuts_aliases_macros.md#shortcuts) for more information. + +## Remove Builtin Commands + +Developers may not want to offer the commands builtin to `cmd2.Cmd`{.interpreted-text role="class"} to users of their application. To remove a command you must delete the method implementing that command from the `cmd2.Cmd`{.interpreted-text role="class"} object at runtime. For example, if you wanted to remove the `features/builtin_commands:shell`{.interpreted-text role="ref"} command from your application: + +```py +class NoShellApp(cmd2.Cmd): + """A simple cmd2 application.""" + + delattr(cmd2.Cmd, 'do_shell') +``` diff --git a/docs/features/builtin_commands.rst b/docs/features/builtin_commands.rst deleted file mode 100644 index 4925fc3d7..000000000 --- a/docs/features/builtin_commands.rst +++ /dev/null @@ -1,161 +0,0 @@ -Builtin Commands -================ - -Applications which subclass :class:`cmd2.Cmd` inherit a number of commands -which may be useful to your users. Developers can -:ref:`features/builtin_commands:Remove Builtin Commands` if they do not want -them to be part of the application. - -List of Builtin Commands ------------------------- - -alias -~~~~~ - -This command manages aliases via subcommands ``create``, ``delete``, and -``list``. See :ref:`features/shortcuts_aliases_macros:Aliases` for more -information. - -edit -~~~~ - -This command launches an editor program and instructs it to open the given file -name. Here's an example: - -.. code-block:: text - - (Cmd) edit ~/.ssh/config - -The program to be launched is determined by the value of the -:ref:`features/settings:editor` setting. - -help -~~~~ - -This command lists available commands or provides detailed help for a specific -command. When called with the ``-v/--verbose`` argument, it shows a brief -description of each command. See :ref:`features/help:Help` for more -information. - -history -~~~~~~~ - -This command allows you to view, run, edit, save, or clear previously entered -commands from the history. See :ref:`features/history:History` for more -information. - -ipy -~~~ - -This optional opt-in command enters an interactive IPython shell. See -:ref:`features/embedded_python_shells:IPython (optional)` for more information. - -macro -~~~~~ - -This command manages macros via subcommands ``create``, ``delete``, and -``list``. A macro is similar to an alias, but it can contain argument -placeholders. See :ref:`features/shortcuts_aliases_macros:Macros` for more -information. - -py -~~ - -This command invokes a Python command or shell. See -:ref:`features/embedded_python_shells:Embedded Python Shells` for more -information. - -quit -~~~~ - -This command exits the ``cmd2`` application. - -.. _feature-builtin-commands-run-pyscript: - -run_pyscript -~~~~~~~~~~~~ - -This command runs a Python script file inside the ``cmd2`` application. -See :ref:`features/scripting:Python Scripts` for more information. - -run_script -~~~~~~~~~~ - -This command runs commands in a script file that is encoded as either ASCII -or UTF-8 text. See :ref:`features/scripting:Command Scripts` for more -information. - -_relative_run_script -~~~~~~~~~~~~~~~~~~~~ - -This command is hidden from the help that's visible to end users. It runs a -script like :ref:`features/builtin_commands:run_script` but does so using a -path relative to the script that is currently executing. This is useful when -you have scripts that run other scripts. See :ref:`features/scripting:Running -Command Scripts` for more information. - -set -~~~ - -A list of all user-settable parameters, with brief comments, is viewable from -within a running application: - -.. code-block:: text - - (Cmd) set - Name Value Description - ==================================================================================================================== - allow_style Terminal Allow ANSI text style sequences in output (valid values: - Always, Never, Terminal) - always_show_hint False Display tab completion hint even when completion suggestions - print - debug True Show full traceback on exception - echo False Echo command issued into output - editor vi Program used by 'edit' - feedback_to_output False Include nonessentials in '|', '>' results - max_completion_items 50 Maximum number of CompletionItems to display during tab - completion - quiet False Don't print nonessential feedback - scripts_add_to_history True Scripts and pyscripts add commands to history - timing False Report execution times - - -Any of these user-settable parameters can be set while running your app with -the ``set`` command like so: - -.. code-block:: text - - (Cmd) set allow_style Never - -See :ref:`features/settings:Settings` for more information. - -shell -~~~~~ - -Execute a command as if at the operating system shell prompt: - -.. code-block:: text - - (Cmd) shell pwd -P - /usr/local/bin - -shortcuts -~~~~~~~~~ - -This command lists available shortcuts. See -:ref:`features/shortcuts_aliases_macros:Shortcuts` for more information. - - -Remove Builtin Commands ------------------------ - -Developers may not want to offer the commands builtin to :class:`cmd2.Cmd` -to users of their application. To remove a command you must delete the method -implementing that command from the :class:`cmd2.Cmd` object at runtime. -For example, if you wanted to remove the :ref:`features/builtin_commands:shell` -command from your application:: - - class NoShellApp(cmd2.Cmd): - """A simple cmd2 application.""" - - delattr(cmd2.Cmd, 'do_shell') diff --git a/docs/features/clipboard.md b/docs/features/clipboard.md new file mode 100644 index 000000000..fcb46dbbf --- /dev/null +++ b/docs/features/clipboard.md @@ -0,0 +1,27 @@ +# Clipboard Integration + +Nearly every operating system has some notion of a short-term storage area which can be accessed by any program. Usually this is called the clipboard, but sometimes people refer to it as the paste buffer. + +`cmd2` integrates with the operating system clipboard using the [pyperclip](https://github.com/asweigart/pyperclip) module. Command output can be sent to the clipboard by ending the command with a greater than symbol: + +```text +mycommand args > +``` + +Think of it as though you are redirecting output to an unnamed, ephemeral place, you know, like the clipboard. You can also append output to the current contents of the clipboard by ending the command with two greater than symbols: + +```text +mycommand arg1 arg2 >> +``` + +## Developers + +You can control whether the above user features of adding output to the operating system clipboard are allowed for the user by setting the `~cmd2.Cmd.allow_clipboard`{.interpreted-text role="attr"} attribute. The default value is `True`. Set it to `False` and the above functionality will generate an error message instead of adding the output to the clipboard. `~cmd2.Cmd.allow_clipboard`{.interpreted-text role="attr"} can be set upon initialization, and you can change it at any time from within your code. + +If you would like your `cmd2` based application to be able to use the clipboard in additional or alternative ways, you can use the following methods (which work uniformly on Windows, macOS, and Linux). + +::: cmd2.clipboard + handler: python + options: + show_root_heading: false + show_source: false \ No newline at end of file diff --git a/docs/features/clipboard.rst b/docs/features/clipboard.rst deleted file mode 100644 index a4b9cdf21..000000000 --- a/docs/features/clipboard.rst +++ /dev/null @@ -1,41 +0,0 @@ -Clipboard Integration -===================== - -Nearly every operating system has some notion of a short-term storage area -which can be accessed by any program. Usually this is called the clipboard, but -sometimes people refer to it as the paste buffer. - -``cmd2`` integrates with the operating system clipboard using the `pyperclip -`_ module. Command output can be sent -to the clipboard by ending the command with a greater than symbol: - -.. code-block:: text - - mycommand args > - -Think of it as though you are redirecting output to an unnamed, ephemeral -place, you know, like the clipboard. You can also append output to the current -contents of the clipboard by ending the command with two greater than symbols: - -.. code-block:: text - - mycommand arg1 arg2 >> - - -Developers ----------- - -You can control whether the above user features of adding output to the -operating system clipboard are allowed for the user by setting the -:attr:`~cmd2.Cmd.allow_clipboard` attribute. The default value is ``True``. -Set it to ``False`` and the above functionality will generate an error -message instead of adding the output to the clipboard. -:attr:`~cmd2.Cmd.allow_clipboard` can be set upon initialization, and you can -change it at any time from within your code. - -If you would like your ``cmd2`` based application to be able to use the -clipboard in additional or alternative ways, you can use the following methods -(which work uniformly on Windows, macOS, and Linux). - -.. automodule:: cmd2.clipboard - :members: diff --git a/docs/features/commands.md b/docs/features/commands.md new file mode 100644 index 000000000..5a1249fd9 --- /dev/null +++ b/docs/features/commands.md @@ -0,0 +1,155 @@ +# Commands + +`cmd2` is designed to make it easy for you to create new commands. These commands form the backbone of your application. If you started writing your application using [cmd](https://docs.python.org/3/library/cmd.html), all the commands you have built will work when you move to `cmd2`. However, there are many more capabilities available in `cmd2` which you can take advantage of to add more robust features to your commands, and which makes your commands easier to write. Before we get to all the good stuff, let's briefly discuss how to create a new command in your application. + +## Basic Commands + +The simplest `cmd2` application looks like this: + +```py +#!/usr/bin/env python +"""A simple cmd2 application.""" +import cmd2 + + +class App(cmd2.Cmd): + """A simple cmd2 application.""" + + +if __name__ == '__main__': + import sys + c = App() + sys.exit(c.cmdloop()) +``` + +This application subclasses `cmd2.Cmd` but has no code of it's own, so all functionality (and there's quite a bit) is inherited. Lets create a simple command in this application called `echo` which outputs any arguments given to it. Add this method to the class: + +```py +def do_echo(self, line): + self.poutput(line) +``` + +When you type input into the `cmd2` prompt, the first space delimited word is treated as the command name. `cmd2` looks for a method called `do_commandname`. If it exists, it calls the method, passing the rest of the user input as the first argument. If it doesn't exist `cmd2` prints an error message. As a result of this behavior, the only thing you have to do to create a new command is to define a new method in the class with the appropriate name. This is exactly how you would create a command using the [cmd](https://docs.python.org/3/library/cmd.html) module which is part of the python standard library. + +!!! note + + See [Generating Output](./generating_output.md) if you are unfamiliar with the `poutput()` method. + +## Statements + +A command is passed one argument: a string which contains all the rest of the user input. However, in `cmd2` this string is actually a `Statement` object, which is a subclass of `str` to retain backwards compatibility. + +`cmd2` has a much more sophsticated parsing engine than what's included in the [cmd](https://docs.python.org/3/library/cmd.html) module. This parsing handles: + +- quoted arguments +- output redirection and piping +- multi-line commands +- shortcut, macro, and alias expansion + +In addition to parsing all of these elements from the user input, `cmd2` also has code to make all of these items work; it's almost transparent to you and to the commands you write in your own application. However, by passing your command the `Statement` object instead of just a plain string, you can get visibility into what `cmd2` has done with the user input before your command got it. You can also avoid writing a bunch of parsing code, because `cmd2` gives you access to what it has already parsed. + +A `Statement` object is a subclass of `str` that contains the following attributes: + +command + +: Name of the command called. You already know this because of the method `cmd2` called, but it can sometimes be nice to have it in a string, i.e. if you want your error messages to contain the command name. + +args + +: A string containing the arguments to the command with output redirection or piping to shell commands removed. It turns out that the "string" value of the `Statement` object has all the output redirection and piping clauses removed as well. Quotes remain in the string. + +command[and_args]{#and_args} + +: A string of just the command and the arguments, with output redirection or piping to shell commands removed. + +argv + +: A list of arguments a-la `sys.argv`, including the command as `argv[0]` and the subsequent arguments as additional items in the list. Quotes around arguments will be stripped as will any output redirection or piping portions of the command. + +raw + +: Full input exactly as typed by the user. + +terminator + +: Character used to end a multiline command. You can configure multiple termination characters, and this attribute will tell you which one the user typed. + +For many simple commands, like the `echo` command above, you can ignore the `Statement` object and all of it's attributes and just use the passed value as a string. You might choose to use the `argv` attribute to do more sophisticated argument processing. Before you go too far down that path, you should check out the [Argument Processing](./argument_processing.md) functionality included with `cmd2`. + +## Return Values + +Most commands should return nothing (either by omitting a `return` statement, or by `return None`. This indicates that your command is finished (with or without errors), and that `cmd2` should prompt the user for more input. + +If you return `True` from a command method, that indicates to `cmd2` that it should stop prompting for user input and cleanly exit. `cmd2` already includes a `quit` command, but if you wanted to make another one called `finish` you could: + +```py +def do_finish(self, line): + """Exit the application""" + return True +``` + +## Exit Codes + +`cmd2` has basic infrastructure to support sh/ksh/csh/bash type exit codes. The `cmd2.Cmd` object sets an `exit_code` attribute to zero when it is instantiated. The value of this attribute is returned from the `cmdloop()` call. Therefore, if you don't do anything with this attribute in your code, `cmdloop()` will (almost) always return zero. There are a few built-in `cmd2` commands which set `exit_code` to `1` if an error occurs. + +You can use this capability to easily return your own values to the operating system shell: + +```py +#!/usr/bin/env python +"""A simple cmd2 application.""" +import cmd2 + + +class App(cmd2.Cmd): + """A simple cmd2 application.""" + +def do_bail(self, line): + """Exit the application""" + self.perror("fatal error, exiting") + self.exit_code = 2 + return true + +if __name__ == '__main__': + import sys + c = App() + sys.exit(c.cmdloop()) +``` + +If the app was run from the `bash` operating system shell, then you would see the following interaction: + +```sh +(Cmd) bail +fatal error, exiting +$ echo $? +2 +``` + +Raising `SystemExit(code)` or calling `sys.exit(code)` in a command or hook function also sets `self.exit_code` and stops the program. + +## Exception Handling + +You may choose to catch and handle any exceptions which occur in a command method. If the command method raises an exception, `cmd2` will catch it and display it for you. The [debug setting](./settings.md#debug) controls how the exception is displayed. If `debug` is `false`, which is the default, `cmd2` will display the exception name and message. If `debug` is `true`, `cmd2` will display a traceback, and then display the exception name and message. + +There are a few exceptions which commands can raise that do not print as described above: + +- `cmd2.exceptions.SkipPostcommandHooks` - all postcommand hooks are skipped and no exception prints +- `cmd2.exceptions.Cmd2ArgparseError` - behaves like `SkipPostcommandHooks` +- `SystemExit` - `stop` will be set to `True` in an attempt to stop the command loop +- `KeyboardInterrupt` - raised if running in a text script and `stop` isn't already True to stop the script + +All other `BaseExceptions` are not caught by `cmd2` and will be raised + +## Disabling or Hiding Commands + +See [Disabling Commands](./disable_commands.md) for details of how to: + +- remove commands included in `cmd2` +- hide commands from the help menu +- disable and re-enable commands at runtime + +## Modular Commands and Loading/Unloading Commands + +See [Modular Commands](./modular_commands.md) for details of how to: + +- Define commands in separate CommandSet modules +- Load or unload commands at runtime diff --git a/docs/features/commands.rst b/docs/features/commands.rst deleted file mode 100644 index 5dd5a163b..000000000 --- a/docs/features/commands.rst +++ /dev/null @@ -1,224 +0,0 @@ -Commands -======== - -.. _cmd: https://docs.python.org/3/library/cmd.html - -``cmd2`` is designed to make it easy for you to create new commands. These -commands form the backbone of your application. If you started writing your -application using cmd_, all the commands you have built will work when you move -to ``cmd2``. However, there are many more capabilities available in ``cmd2`` -which you can take advantage of to add more robust features to your commands, -and which makes your commands easier to write. Before we get to all the good -stuff, let's briefly discuss how to create a new command in your application. - - -Basic Commands --------------- - -The simplest ``cmd2`` application looks like this:: - - #!/usr/bin/env python - """A simple cmd2 application.""" - import cmd2 - - - class App(cmd2.Cmd): - """A simple cmd2 application.""" - - - if __name__ == '__main__': - import sys - c = App() - sys.exit(c.cmdloop()) - -This application subclasses ``cmd2.Cmd`` but has no code of it's own, so all -functionality (and there's quite a bit) is inherited. Lets create a simple -command in this application called ``echo`` which outputs any arguments given -to it. Add this method to the class:: - - def do_echo(self, line): - self.poutput(line) - -When you type input into the ``cmd2`` prompt, the first space delimited word is -treated as the command name. ``cmd2`` looks for a method called -``do_commandname``. If it exists, it calls the method, passing the rest of the -user input as the first argument. If it doesn't exist ``cmd2`` prints an error -message. As a result of this behavior, the only thing you have to do to create -a new command is to define a new method in the class with the appropriate name. -This is exactly how you would create a command using the cmd_ module which is -part of the python standard library. - -.. note:: - - See :ref:`features/generating_output:Generating Output` if you are - unfamiliar with the ``poutput()`` method. - - -Statements ----------- - -A command is passed one argument: a string which contains all the rest of the -user input. However, in ``cmd2`` this string is actually a ``Statement`` -object, which is a subclass of ``str`` to retain backwards compatibility. - -``cmd2`` has a much more sophsticated parsing engine than what's included in -the cmd_ module. This parsing handles: - -- quoted arguments -- output redirection and piping -- multi-line commands -- shortcut, macro, and alias expansion - -In addition to parsing all of these elements from the user input, ``cmd2`` also -has code to make all of these items work; it's almost transparent to you and to -the commands you write in your own application. However, by passing your -command the ``Statement`` object instead of just a plain string, you can get -visibility into what ``cmd2`` has done with the user input before your command -got it. You can also avoid writing a bunch of parsing code, because ``cmd2`` -gives you access to what it has already parsed. - -A ``Statement`` object is a subclass of ``str`` that contains the following -attributes: - -command - Name of the command called. You already know this because of the method - ``cmd2`` called, but it can sometimes be nice to have it in a string, i.e. - if you want your error messages to contain the command name. - -args - A string containing the arguments to the command with output redirection or - piping to shell commands removed. It turns out that the "string" value of - the ``Statement`` object has all the output redirection and piping clauses - removed as well. Quotes remain in the string. - -command_and_args - A string of just the command and the arguments, with output redirection or - piping to shell commands removed. - -argv - A list of arguments a-la ``sys.argv``, including the command as ``argv[0]`` - and the subsequent arguments as additional items in the list. Quotes around - arguments will be stripped as will any output redirection or piping - portions of the command. - -raw - Full input exactly as typed by the user. - -terminator - Character used to end a multiline command. You can configure multiple - termination characters, and this attribute will tell you which one the user - typed. - -For many simple commands, like the ``echo`` command above, you can ignore the -``Statement`` object and all of it's attributes and just use the passed value -as a string. You might choose to use the ``argv`` attribute to do more -sophisticated argument processing. Before you go too far down that path, you -should check out the :ref:`features/argument_processing:Argument Processing` -functionality included with ``cmd2``. - - -Return Values -------------- - -Most commands should return nothing (either by omitting a ``return`` statement, -or by ``return None``. This indicates that your command is finished (with or -without errors), and that ``cmd2`` should prompt the user for more input. - -If you return ``True`` from a command method, that indicates to ``cmd2`` that -it should stop prompting for user input and cleanly exit. ``cmd2`` already -includes a ``quit`` command, but if you wanted to make another one called -``finish`` you could:: - - def do_finish(self, line): - """Exit the application""" - return True - - -Exit Codes ----------- - -``cmd2`` has basic infrastructure to support sh/ksh/csh/bash type exit codes. -The ``cmd2.Cmd`` object sets an ``exit_code`` attribute to zero when it is -instantiated. The value of this attribute is returned from the ``cmdloop()`` -call. Therefore, if you don't do anything with this attribute in your code, -``cmdloop()`` will (almost) always return zero. There are a few built-in -``cmd2`` commands which set ``exit_code`` to ``1`` if an error occurs. - -You can use this capability to easily return your own values to the operating -system shell:: - - #!/usr/bin/env python - """A simple cmd2 application.""" - import cmd2 - - - class App(cmd2.Cmd): - """A simple cmd2 application.""" - - def do_bail(self, line): - """Exit the application""" - self.perror("fatal error, exiting") - self.exit_code = 2 - return true - - if __name__ == '__main__': - import sys - c = App() - sys.exit(c.cmdloop()) - -If the app was run from the `bash` operating system shell, then you would see -the following interaction:: - - (Cmd) bail - fatal error, exiting - $ echo $? - 2 - - -Raising ``SystemExit(code)`` or calling ``sys.exit(code)`` in a command -or hook function also sets ``self.exit_code`` and stops the program. - -Exception Handling ------------------- - -You may choose to catch and handle any exceptions which occur in -a command method. If the command method raises an exception, ``cmd2`` will -catch it and display it for you. The `debug` :ref:`setting -` controls how the exception is displayed. If -`debug` is `false`, which is the default, ``cmd2`` will display the exception -name and message. If `debug` is `true`, ``cmd2`` will display a traceback, and -then display the exception name and message. - -There are a few exceptions which commands can raise that do not print as -described above: - -- :attr:`cmd2.exceptions.SkipPostcommandHooks` - all postcommand hooks are - skipped and no exception prints -- :attr:`cmd2.exceptions.Cmd2ArgparseError` - behaves like - ``SkipPostcommandHooks`` -- ``SystemExit`` - ``stop`` will be set to ``True`` in an attempt to stop the - command loop -- ``KeyboardInterrupt`` - raised if running in a text script and ``stop`` isn't - already True to stop the script - -All other ``BaseExceptions`` are not caught by ``cmd2`` and will be raised - -Disabling or Hiding Commands ----------------------------- - -See :ref:`features/disable_commands:Disabling Commands` for details of how -to: - -- remove commands included in ``cmd2`` -- hide commands from the help menu -- disable and re-enable commands at runtime - - -Modular Commands and Loading/Unloading Commands ------------------------------------------------ - -See :ref:`features/modular_commands:Modular Commands` for details of how -to: - -- Define commands in separate CommandSet modules -- Load or unload commands at runtime diff --git a/docs/features/completion.md b/docs/features/completion.md new file mode 100644 index 000000000..9951d9673 --- /dev/null +++ b/docs/features/completion.md @@ -0,0 +1,85 @@ +# Completion + +`cmd2.Cmd` adds tab completion of file system paths for all built-in commands where it makes sense, including: + +- [edit](./builtin_commands.md#edit) +- [run_pyscript](./builtin_commands.md#feature-builtin-commands-run-pyscript) +- [run_script](./builtin_commands.md#run_script) +- [shell](./builtin_commands.md#shell) + +`cmd2.Cmd` also adds tab completion of shell commands to the [shell](./builtin_commands.md#shell) command. + +It is easy to add identical file system path completion to your own custom commands. Suppose you have defined a custom command `foo` by implementing the `do_foo` method. To enable path completion for the `foo` command, then add a line of code similar to the following to your class which inherits from `cmd2.Cmd`: + +```py +complete_foo = cmd2.Cmd.path_complete +``` + +This will effectively define the `complete_foo` readline completer method in your class and make it utilize the same path completion logic as the built-in commands. + +The built-in logic allows for a few more advanced path completion capabilities, such as cases where you only want to match directories. Suppose you have a custom command `bar` implemented by the `do_bar` method. You can enable path completion of directories only for this command by adding a line of code similar to the following to your class which inherits from `cmd2.Cmd`: + +```py +# Make sure you have an "import functools" somewhere at the top +complete_bar = functools.partialmethod(cmd2.Cmd.path_complete, path_filter=os.path.isdir) +``` + +## Included Tab Completion Functions + +`cmd2` provides the following tab completion functions + +- `cmd2.Cmd.basic_complete` - helper method for tab completion against a list + +- `cmd2.Cmd.path_complete` - helper method provides flexible tab completion of file system paths + + > - See the [paged_output](https://github.com/python-cmd2/cmd2/blob/master/examples/paged_output.py) example for a simple use case + > - See the [python_scripting](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py) example for a more full-featured use case + +- `cmd2.Cmd.delimiter_complete` - helper method for tab completion against a list but each match is split on a delimiter + + > - See the [basic_completion](https://github.com/python-cmd2/cmd2/blob/master/examples/basic_completion.py) example for a demonstration of how to use this feature + +- `cmd2.Cmd.flag_based_complete` - helper method for tab completion based on a particular flag preceding the token being completed + +- `cmd2.Cmd.index_based_complete` - helper method for tab completion based on a fixed position in the input string + + > - See the [basic_completion](https://github.com/python-cmd2/cmd2/blob/master/examples/basic_completion.py) example for a demonstration of how to use these features + > - `flag_based_complete()` and `index_based_complete()` are basic methods and should only be used if you are not familiar with argparse. The recommended approach for tab completing positional tokens and flags is to use [argparse-based](#argparse-based) completion. + +## Raising Exceptions During Completion + +There are times when an error occurs while tab completing and a message needs to be reported to the user. These include the following example cases: + +- Reading a database to retrieve a tab completion data set failed +- A previous command line argument that determines the data set being completed is invalid +- Tab completion hints + +`cmd2` provides the `cmd2.exceptions.CompletionError` exception class for this capability. If an error occurs in which it is more desirable to display a message than a stack trace, then raise a `CompletionError`. By default, the message displays in red like an error. However, `CompletionError` has a member called `apply_style`. Set this False if the error style should not be applied. For instance, `ArgparseCompleter` sets it to False when displaying completion hints. + +## Tab Completion Using argparse Decorators {: #argparse-based } + +When using one the argparse-based [cmd2.decorators](../api/decorators.md), `cmd2` provides automatic tab completion of flag names. + +Tab completion of argument values can be configured by using one of three parameters to `argparse.ArgumentParser.add_argument` + +- `choices` +- `choices_provider` +- `completer` + +See the [arg_decorators](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_decorators.py) or [colors](https://github.com/python-cmd2/cmd2/blob/master/examples/colors.py) example for a demonstration of how to use the `choices` parameter. See the [argparse_completion](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_completion.py) example for a demonstration of how to use the `choices_provider` parameter. See the [arg_decorators](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_decorators.py) or [argparse_completion](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_completion.py) example for a demonstration of how to use the `completer` parameter. + +When tab completing flags or argument values for a `cmd2` command using one of these decorators, `cmd2` keeps track of state so that once a flag has already previously been provided, it won't attempt to tab complete it again. When no completion results exists, a hint for the current argument will be displayed to help the user. + +## CompletionItem For Providing Extra Context + +When tab completing things like a unique ID from a database, it can often be beneficial to provide the user with some extra context about the item being completed, such as a description. To facilitate this, `cmd2` defines the `cmd2.argparse_custom.CompletionItem` class which can be returned from any of the 3 completion parameters: `choices`, `choices_provider`, and `completer`. + +See the [argparse_completion](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_completion.py) example or the implementation of the built-in [set](./builtin_commands.md#set) command for demonstration of how this is used. + +## Custom Completion with `read_input()` + +`cmd2` provides `cmd2.Cmd.read_input` as an alternative to Python's `input()` function. `read_input` supports configurable tab completion and up-arrow history at the prompt. See [read_input](https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py) example for a demonstration. + +## For More Information + +See [cmd2's argparse_custom API](../api/argparse_custom.md) for a more detailed discussion of argparse completion. diff --git a/docs/features/completion.rst b/docs/features/completion.rst deleted file mode 100644 index 3894a4eb9..000000000 --- a/docs/features/completion.rst +++ /dev/null @@ -1,151 +0,0 @@ -Completion -========== - -:class:`cmd2.Cmd` adds tab completion of file system paths for all built-in -commands where it makes sense, including: - -- :ref:`features/builtin_commands:edit` -- :ref:`features/builtin_commands:run_pyscript` -- :ref:`features/builtin_commands:run_script` -- :ref:`features/builtin_commands:shell` - -:class:`cmd2.Cmd` also adds tab completion of shell commands to the -:ref:`features/builtin_commands:shell` command. - -It is easy to add identical file system path completion to your own custom -commands. Suppose you have defined a custom command ``foo`` by implementing -the ``do_foo`` method. To enable path completion for the ``foo`` command, then -add a line of code similar to the following to your class which inherits from -:class:`cmd2.Cmd`:: - - complete_foo = cmd2.Cmd.path_complete - -This will effectively define the ``complete_foo`` readline completer method in -your class and make it utilize the same path completion logic as the built-in -commands. - -The built-in logic allows for a few more advanced path completion capabilities, -such as cases where you only want to match directories. Suppose you have a -custom command ``bar`` implemented by the ``do_bar`` method. You can enable -path completion of directories only for this command by adding a line of code -similar to the following to your class which inherits from :class:`cmd2.Cmd`:: - - # Make sure you have an "import functools" somewhere at the top - complete_bar = functools.partialmethod(cmd2.Cmd.path_complete, path_filter=os.path.isdir) - - -Included Tab Completion Functions ---------------------------------- -``cmd2`` provides the following tab completion functions - -- :attr:`cmd2.Cmd.basic_complete` - helper method for tab completion against - a list -- :attr:`cmd2.Cmd.path_complete` - helper method provides flexible tab - completion of file system paths - - - See the paged_output_ example for a simple use case - - See the python_scripting_ example for a more full-featured use case - -- :attr:`cmd2.Cmd.delimiter_complete` - helper method for tab completion - against a list but each match is split on a delimiter - - - See the basic_completion_ example for a demonstration of how to use - this feature - -- :attr:`cmd2.Cmd.flag_based_complete` - helper method for tab completion based - on a particular flag preceding the token being completed -- :attr:`cmd2.Cmd.index_based_complete` - helper method for tab completion - based on a fixed position in the input string - - - See the basic_completion_ example for a demonstration of how to use these - features - - ``flag_based_complete()`` and ``index_based_complete()`` are basic - methods and should only be used if you are not familiar with argparse. - The recommended approach for tab completing positional tokens and flags - is to use argparse-based_ completion. - -.. _paged_output: https://github.com/python-cmd2/cmd2/blob/master/examples/paged_output.py -.. _python_scripting: https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py -.. _basic_completion: https://github.com/python-cmd2/cmd2/blob/master/examples/basic_completion.py - - -Raising Exceptions During Completion ------------------------------------- -There are times when an error occurs while tab completing and a message needs -to be reported to the user. These include the following example cases: - -- Reading a database to retrieve a tab completion data set failed -- A previous command line argument that determines the data set being completed - is invalid -- Tab completion hints - -``cmd2`` provides the :class:`cmd2.exceptions.CompletionError` exception class for -this capability. If an error occurs in which it is more desirable to display -a message than a stack trace, then raise a ``CompletionError``. By default, the -message displays in red like an error. However, ``CompletionError`` has a -member called ``apply_style``. Set this False if the error style should not be -applied. For instance, ``ArgparseCompleter`` sets it to False when displaying -completion hints. - -.. _argparse-based: - - -Tab Completion Using argparse Decorators ----------------------------------------- - -When using one the argparse-based :ref:`api/decorators:cmd2.decorators`, -``cmd2`` provides automatic tab completion of flag names. - -Tab completion of argument values can be configured by using one of three -parameters to :meth:`argparse.ArgumentParser.add_argument` - -- ``choices`` -- ``choices_provider`` -- ``completer`` - -See the arg_decorators_ or colors_ example for a demonstration of how to -use the ``choices`` parameter. See the argparse_completion_ example for a -demonstration of how to use the ``choices_provider`` parameter. See the -arg_decorators_ or argparse_completion_ example for a demonstration -of how to use the ``completer`` parameter. - -When tab completing flags or argument values for a ``cmd2`` command using -one of these decorators, ``cmd2`` keeps track of state so that once a flag has -already previously been provided, it won't attempt to tab complete it again. -When no completion results exists, a hint for the current argument will be -displayed to help the user. - -.. _arg_decorators: https://github.com/python-cmd2/cmd2/blob/master/examples/arg_decorators.py -.. _colors: https://github.com/python-cmd2/cmd2/blob/master/examples/colors.py -.. _argparse_completion: https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_completion.py - - -CompletionItem For Providing Extra Context ------------------------------------------- - -When tab completing things like a unique ID from a database, it can often be -beneficial to provide the user with some extra context about the item being -completed, such as a description. To facilitate this, ``cmd2`` defines the -:class:`cmd2.argparse_custom.CompletionItem` class which can be returned from -any of the 3 completion parameters: ``choices``, ``choices_provider``, and -``completer``. - -See the argparse_completion_ example or the implementation of the built-in -:meth:`~cmd2.Cmd.do_set` command for demonstration of how this is used. - - -Custom Completion with ``read_input()`` --------------------------------------------------- - -``cmd2`` provides :attr:`cmd2.Cmd.read_input` as an alternative to Python's -``input()`` function. ``read_input`` supports configurable tab completion and -up-arrow history at the prompt. See read_input_ example for a demonstration. - -.. _read_input: https://github.com/python-cmd2/cmd2/blob/master/examples/read_input.py - - -For More Information --------------------- - -See :mod:`cmd2.argparse_custom` for a more detailed discussion of argparse -completion. diff --git a/docs/features/disable_commands.md b/docs/features/disable_commands.md new file mode 100644 index 000000000..9c6d96d9a --- /dev/null +++ b/docs/features/disable_commands.md @@ -0,0 +1,74 @@ +# Disabling Commands + +`cmd2` allows a developer to: + +- remove commands included in `cmd2` +- prevent commands from appearing in the help menu (hide commands) +- disable and re-enable commands at runtime + +## Remove A Command + +When a command has been removed, the command method has been deleted from the object. The command doesn't show up in help, and it can't be executed. This approach is appropriate if you never want a built-in command to be part of your application. Delete the command method in your initialization code: + + class RemoveBuiltinCommand(cmd2.Cmd): + """An app which removes a built-in command from cmd2""" + + def __init__(self): + super().__init__() + # To remove built-in commands entirely, delete + # the "do_*" function from the cmd2.Cmd class + del cmd2.Cmd.do_edit + +## Hide A Command + +When a command is hidden, it won't show up in the help menu, but if the user knows it's there and types the command, it will be executed. You hide a command by adding it to the `hidden_commands` list: + + class HiddenCommands(cmd2.Cmd): + ""An app which demonstrates how to hide a command""" + def __init__(self): + super().__init__() + self.hidden_commands.append('py') + +As shown above, you would typically do this as part of initializing your application. If you decide you want to unhide a command later in the execution of your application, you can by doing: + + self.hidden_commands = [cmd for cmd in self.hidden_commands if cmd != 'py'] + +You might be thinking that the list comprehension is overkill and you'd rather do something like: + + self.hidden_commands.remove('py') + +You may be right, but `remove()` will raise a `ValueError` if `py` isn't in the list, and it will only remove the first one if it's in the list multiple times. + +## Disable A Command + +One way to disable a command is to add code to the command method which determines whether the command should be executed or not. If the command should not be executed, your code can print an appropriate error message and return. + +`cmd2` also provides another way to accomplish the same thing. Here's a simple app which disables the `open` command if the door is locked: + + class DisabledCommands(cmd2.Cmd): + """An application which disables and enables commands""" + + def do_lock(self, line): + self.disable_command('open', "you can't open the door because it is locked") + self.poutput('the door is locked') + + def do_unlock(self, line): + self.enable_command('open') + self.poutput('the door is unlocked') + + def do_open(self, line): + """open the door""" + self.poutput('opening the door') + +This method has the added benefit of removing disabled commands from the help menu. But, this method only works if you know in advance that the command should be disabled, and if the conditions for re-enabling it are likewise known in advance. + +## Disable A Category of Commands + +You can group or categorize commands as shown in `features/help:Categorizing Commands`{.interpreted-text role="ref"}. If you do so, you can disable and enable all the commands in a category with a single method call. Say you have created a category of commands called "Server Information". You can disable all commands in that category: + + not_connected_msg = 'You must be connected to use this command' + self.disable_category('Server Information', not_connected_msg) + +Similarly, you can re-enable all the commands in a category: + + self.enable_category('Server Information') diff --git a/docs/features/disable_commands.rst b/docs/features/disable_commands.rst deleted file mode 100644 index 397fa88df..000000000 --- a/docs/features/disable_commands.rst +++ /dev/null @@ -1,103 +0,0 @@ -Disabling Commands -================== - -``cmd2`` allows a developer to: - -- remove commands included in ``cmd2`` -- prevent commands from appearing in the help menu (hide commands) -- disable and re-enable commands at runtime - - -Remove A Command ----------------- - -When a command has been removed, the command method has been deleted from the -object. The command doesn't show up in help, and it can't be executed. This -approach is appropriate if you never want a built-in command to be part of your -application. Delete the command method in your initialization code:: - - class RemoveBuiltinCommand(cmd2.Cmd): - """An app which removes a built-in command from cmd2""" - - def __init__(self): - super().__init__() - # To remove built-in commands entirely, delete - # the "do_*" function from the cmd2.Cmd class - del cmd2.Cmd.do_edit - - -Hide A Command --------------- - -When a command is hidden, it won't show up in the help menu, but if -the user knows it's there and types the command, it will be executed. -You hide a command by adding it to the ``hidden_commands`` list:: - - class HiddenCommands(cmd2.Cmd): - ""An app which demonstrates how to hide a command""" - def __init__(self): - super().__init__() - self.hidden_commands.append('py') - -As shown above, you would typically do this as part of initializing your -application. If you decide you want to unhide a command later in the execution -of your application, you can by doing:: - - self.hidden_commands = [cmd for cmd in self.hidden_commands if cmd != 'py'] - -You might be thinking that the list comprehension is overkill and you'd rather -do something like:: - - self.hidden_commands.remove('py') - -You may be right, but ``remove()`` will raise a ``ValueError`` if ``py`` -isn't in the list, and it will only remove the first one if it's in the list -multiple times. - - -Disable A Command ------------------ - -One way to disable a command is to add code to the command method which -determines whether the command should be executed or not. If the command should -not be executed, your code can print an appropriate error message and return. - -``cmd2`` also provides another way to accomplish the same thing. Here's a -simple app which disables the ``open`` command if the door is locked:: - - class DisabledCommands(cmd2.Cmd): - """An application which disables and enables commands""" - - def do_lock(self, line): - self.disable_command('open', "you can't open the door because it is locked") - self.poutput('the door is locked') - - def do_unlock(self, line): - self.enable_command('open') - self.poutput('the door is unlocked') - - def do_open(self, line): - """open the door""" - self.poutput('opening the door') - -This method has the added benefit of removing disabled commands from the help -menu. But, this method only works if you know in advance that the command -should be disabled, and if the conditions for re-enabling it are likewise known -in advance. - - -Disable A Category of Commands ------------------------------- - -You can group or categorize commands as shown in -:ref:`features/help:Categorizing Commands`. If you do so, you can disable and -enable all the commands in a category with a single method call. Say you have -created a category of commands called "Server Information". You can disable -all commands in that category:: - - not_connected_msg = 'You must be connected to use this command' - self.disable_category('Server Information', not_connected_msg) - -Similarly, you can re-enable all the commands in a category:: - - self.enable_category('Server Information') diff --git a/docs/features/embedded_python_shells.md b/docs/features/embedded_python_shells.md new file mode 100644 index 000000000..205648cb0 --- /dev/null +++ b/docs/features/embedded_python_shells.md @@ -0,0 +1,55 @@ +# Embedded Python Shells + +## Python (optional) + +If the `cmd2.Cmd` class is instantiated with `include_py=True`, then the optional `py` command will be present and run an interactive Python shell: + + from cmd2 import Cmd + class App(Cmd): + def __init__(self): + Cmd.__init__(self, include_py=True) + +The Python shell can run CLI commands from you application using the object named in `self.pyscript_name` (defaults to `app`). This wrapper provides access to execute commands in your `cmd2` application while maintaining isolation from the full ``Cmd`` instance. For example, any application command can be run with `app("command ...")`. + +You may optionally enable full access to to your application by setting `self.self_in_py` to `True`. Enabling this flag adds `self` to the python session, which is a reference to your `cmd2` application. This can be useful for debugging your application. + +Any local or global variable created within the Python session will not persist in the CLI's environment. + +Anything in `self.py_locals` is always available in the Python environment. + +All of these parameters are also available to Python scripts which run in your application via the `run_pyscript` command: + +- supports tab completion of file system paths +- has the ability to pass command-line arguments to the scripts invoked + +This command provides a more complicated and more powerful scripting capability than that provided by the simple text file scripts. Python scripts can include conditional control flow logic. See the **python_scripting.py** `cmd2` application and the **script_conditional.py** script in the `examples` source code directory for an example of how to achieve this in your own applications. See `features/scripting:Scripting`{.interpreted-text role="ref"} for an explanation of both scripting methods in **cmd2** applications. + +A simple example of using `run_pyscript` is shown below along with the [arg_printer](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py) script: + + (Cmd) run_pyscript examples/scripts/arg_printer.py foo bar baz + Running Python script 'arg_printer.py' which was called with 3 arguments + arg 1: 'foo' + arg 2: 'bar' + arg 3: 'baz' + +## IPython (optional) + +**If** [IPython](http://ipython.readthedocs.io) is installed on the system **and** the `cmd2.Cmd` class is instantiated with `include_ipy=True`, then the optional `ipy` command will run an interactive IPython shell: + + from cmd2 import Cmd + class App(Cmd): + def __init__(self): + Cmd.__init__(self, include_ipy=True) + +The `ipy` command enters an interactive [IPython](http://ipython.readthedocs.io) session. Similar to an interactive Python session, this shell can access your application instance via `self` if `self.self_in_py` is `True` and any changes to your application made via `self` will persist. However, any local or global variable created within the `ipy` shell will not persist in the CLI's environment + +Also, as in the interactive Python session, the `ipy` shell has access to the contents of `self.py_locals` and can call back into the application using the `app` object (or your custom name). + +[IPython](http://ipython.readthedocs.io) provides many advantages, including: + +> - Comprehensive object introspection +> - Get help on objects with `?` +> - Extensible tab completion, with support by default for completion of python variables and keywords +> - Good built-in [ipdb](https://pypi.org/project/ipdb/) debugger + +The object introspection and tab completion make IPython particularly efficient for debugging as well as for interactive experimentation and data analysis. diff --git a/docs/features/embedded_python_shells.rst b/docs/features/embedded_python_shells.rst deleted file mode 100644 index fc04e0202..000000000 --- a/docs/features/embedded_python_shells.rst +++ /dev/null @@ -1,93 +0,0 @@ -Embedded Python Shells -====================== - -Python (optional) ------------------- -If the ``cmd2.Cmd`` class is instantiated with ``include_py=True``, then the -optional ``py`` command will be present and run an interactive Python shell:: - - from cmd2 import Cmd - class App(Cmd): - def __init__(self): - Cmd.__init__(self, include_py=True) - -The Python shell can run CLI commands from you application using the object -named in ``self.pyscript_name`` (defaults to ``app``). This wrapper provides -access to execute commands in your ``cmd2`` application while maintaining -isolation from the full `Cmd` instance. For example, any application command -can be run with ``app("command ...")``. - -You may optionally enable full access to to your application by setting -``self.self_in_py`` to ``True``. Enabling this flag adds ``self`` to the -python session, which is a reference to your ``cmd2`` application. This can be -useful for debugging your application. - -Any local or global variable created within the Python session will not persist -in the CLI's environment. - -Anything in ``self.py_locals`` is always available in the Python environment. - -All of these parameters are also available to Python scripts which run in your -application via the ``run_pyscript`` command: - -- supports tab completion of file system paths -- has the ability to pass command-line arguments to the scripts invoked - -This command provides a more complicated and more powerful scripting capability -than that provided by the simple text file scripts. Python scripts can include -conditional control flow logic. See the **python_scripting.py** ``cmd2`` -application and the **script_conditional.py** script in the ``examples`` source -code directory for an example of how to achieve this in your own applications. -See :ref:`features/scripting:Scripting` for an explanation of both scripting -methods in **cmd2** applications. - -A simple example of using ``run_pyscript`` is shown below along with the -arg_printer_ script:: - - (Cmd) run_pyscript examples/scripts/arg_printer.py foo bar baz - Running Python script 'arg_printer.py' which was called with 3 arguments - arg 1: 'foo' - arg 2: 'bar' - arg 3: 'baz' - -.. _arg_printer: - https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py - - -IPython (optional) ------------------- - -**If** IPython_ is installed on the system **and** the ``cmd2.Cmd`` class is -instantiated with ``include_ipy=True``, then the optional ``ipy`` command will -run an interactive IPython shell:: - - from cmd2 import Cmd - class App(Cmd): - def __init__(self): - Cmd.__init__(self, include_ipy=True) - -The ``ipy`` command enters an interactive IPython_ session. Similar to an -interactive Python session, this shell can access your application instance via -``self`` if ``self.self_in_py`` is ``True`` and any changes to your application -made via ``self`` will persist. However, any local or global variable created -within the ``ipy`` shell will not persist in the CLI's environment - -Also, as in the interactive Python session, the ``ipy`` shell has access to the -contents of ``self.py_locals`` and can call back into the application using the -``app`` object (or your custom name). - -IPython_ provides many advantages, including: - - * Comprehensive object introspection - * Get help on objects with ``?`` - * Extensible tab completion, with support by default for completion of - python variables and keywords - * Good built-in ipdb_ debugger - -The object introspection and tab completion make IPython particularly efficient -for debugging as well as for interactive experimentation and data analysis. - -.. _IPython: http://ipython.readthedocs.io -.. _ipdb: https://pypi.org/project/ipdb/ - - diff --git a/docs/features/generating_output.md b/docs/features/generating_output.md new file mode 100644 index 000000000..b1d5bfb0d --- /dev/null +++ b/docs/features/generating_output.md @@ -0,0 +1,79 @@ +# Generating Output + +A standard `cmd` application can produce output by using either of these methods: + + print("Greetings, Professor Falken.", file=self.stdout) + self.stdout.write("Shall we play a game?\n") + +While you could send output directly to `sys.stdout`, `cmd2.Cmd`{.interpreted-text role="mod"} can be initialized with a `stdin` and `stdout` variables, which it stores as `self.stdin` and `self.stdout`. By using these variables every time you produce output, you can trivially change where all the output goes by changing how you initialize your class. + +`cmd2.Cmd`{.interpreted-text role="mod"} extends this approach in a number of convenient ways. See `features/redirection:Output Redirection And Pipes`{.interpreted-text role="ref"} for information on how users can change where the output of a command is sent. In order for those features to work, the output you generate must be sent to `self.stdout`. You can use the methods described above, and everything will work fine. `cmd2.Cmd`{.interpreted-text role="mod"} also includes a number of output related methods which you may use to enhance the output your application produces. + +## Ordinary Output + +The `~cmd2.Cmd.poutput`{.interpreted-text role="meth"} method is similar to the Python [built-in print function](https://docs.python.org/3/library/functions.html#print). `~cmd2.Cmd.poutput`{.interpreted-text role="meth"} adds two conveniences: + +> 1\. Since users can pipe output to a shell command, it catches `BrokenPipeError` and outputs the contents of `self.broken_pipe_warning` to `stderr`. `self.broken_pipe_warning` defaults to an empty string so this method will just swallow the exception. If you want to show an error message, put it in `self.broken_pipe_warning` when you initialize `~cmd2.Cmd`{.interpreted-text role="mod"}. +> +> 2\. It examines and honors the `features/settings:allow_style`{.interpreted-text role="ref"} setting. See `features/generating_output:Colored Output`{.interpreted-text role="ref"} below for more details. + +Here's a simple command that shows this method in action: + + def do_echo(self, args): + """A simple command showing how poutput() works""" + self.poutput(args) + +## Error Messages + +When an error occurs in your program, you can display it on `sys.stderr` by calling the `~.cmd2.Cmd.perror`{.interpreted-text role="meth"} method. By default this method applies `cmd2.ansi.style_error`{.interpreted-text role="meth"} to the output. + +## Warning Messages + +`~.cmd2.Cmd.pwarning`{.interpreted-text role="meth"} is just like `~.cmd2.Cmd.perror`{.interpreted-text role="meth"} but applies `cmd2.ansi.style_warning`{.interpreted-text role="meth"} to the output. + +## Feedback + +You may have the need to display information to the user which is not intended to be part of the generated output. This could be debugging information or status information about the progress of long running commands. It's not output, it's not error messages, it's feedback. If you use the `features/settings:Timing`{.interpreted-text role="ref"} setting, the output of how long it took the command to run will be output as feedback. You can use the `~.cmd2.Cmd.pfeedback`{.interpreted-text role="meth"} method to produce this type of output, and several `features/settings:Settings`{.interpreted-text role="ref"} control how it is handled. + +If the `features/settings:quiet`{.interpreted-text role="ref"} setting is `True`, then calling `~.cmd2.Cmd.pfeedback`{.interpreted-text role="meth"} produces no output. If `features/settings:quiet`{.interpreted-text role="ref"} is `False`, the `features/settings:feedback_to_output`{.interpreted-text role="ref"} setting is consulted to determine whether to send the output to `stdout` or `stderr`. + +## Exceptions + +If your app catches an exception and you would like to display the exception to the user, the `~.cmd2.Cmd.pexcept`{.interpreted-text role="meth"} method can help. The default behavior is to just display the message contained within the exception. However, if the `features/settings:debug`{.interpreted-text role="ref"} setting is `True`, then the entire stack trace will be displayed. + +## Paging Output + +If you know you are going to generate a lot of output, you may want to display it in a way that the user can scroll forwards and backwards through it. If you pass all of the output to be displayed in a single call to `~.cmd2.Cmd.ppaged`{.interpreted-text role="meth"}, it will be piped to an operating system appropriate shell command to page the output. On Windows, the output is piped to `more`; on Unix-like operating systems like MacOS and Linux, it is piped to `less`. + +## Colored Output + +You can add your own [ANSI escape sequences](https://en.wikipedia.org/wiki/ANSI_escape_code#Colors) to your output which tell the terminal to change the foreground and background colors. + +`cmd2` provides a number of convenience functions and classes for adding color and other styles to text. These are all documented in `cmd2.ansi`{.interpreted-text role="mod"}. + +After adding the desired escape sequences to your output, you should use one of these methods to present the output to the user: + +- `.cmd2.Cmd.poutput`{.interpreted-text role="meth"} +- `.cmd2.Cmd.perror`{.interpreted-text role="meth"} +- `.cmd2.Cmd.pwarning`{.interpreted-text role="meth"} +- `.cmd2.Cmd.pexcept`{.interpreted-text role="meth"} +- `.cmd2.Cmd.pfeedback`{.interpreted-text role="meth"} +- `.cmd2.Cmd.ppaged`{.interpreted-text role="meth"} + +These methods all honor the `features/settings:allow_style`{.interpreted-text role="ref"} setting, which users can modify to control whether these escape codes are passed through to the terminal or not. + +## Aligning Text + +If you would like to generate output which is left, center, or right aligned within a specified width or the terminal width, the following functions can help: + +- `cmd2.utils.align_left`{.interpreted-text role="meth"} +- `cmd2.utils.align_center`{.interpreted-text role="meth"} +- `cmd2.utils.align_right`{.interpreted-text role="meth"} + +These functions differ from Python's string justifying functions in that they support characters with display widths greater than 1. Additionally, ANSI style sequences are safely ignored and do not count toward the display width. This means colored text is supported. If text has line breaks, then each line is aligned independently. + +## Columnar Output + +When generating output in multiple columns, you often need to calculate the width of each item so you can pad it appropriately with spaces. However, there are categories of Unicode characters that occupy 2 cells, and other that occupy 0. To further complicate matters, you might have included ANSI escape sequences in the output to generate colors on the terminal. + +The `cmd2.ansi.style_aware_wcswidth`{.interpreted-text role="meth"} function solves both of these problems. Pass it a string, and regardless of which Unicode characters and ANSI text style escape sequences it contains, it will tell you how many characters on the screen that string will consume when printed. diff --git a/docs/features/generating_output.rst b/docs/features/generating_output.rst deleted file mode 100644 index 7a6f047d3..000000000 --- a/docs/features/generating_output.rst +++ /dev/null @@ -1,160 +0,0 @@ -Generating Output -================= - -A standard ``cmd`` application can produce output by using either of these -methods:: - - print("Greetings, Professor Falken.", file=self.stdout) - self.stdout.write("Shall we play a game?\n") - -While you could send output directly to ``sys.stdout``, :mod:`cmd2.Cmd` -can be initialized with a ``stdin`` and ``stdout`` variables, which it stores -as ``self.stdin`` and ``self.stdout``. By using these variables every time you -produce output, you can trivially change where all the output goes by changing -how you initialize your class. - -:mod:`cmd2.Cmd` extends this approach in a number of convenient ways. See -:ref:`features/redirection:Output Redirection And Pipes` for information on how -users can change where the output of a command is sent. In order for those -features to work, the output you generate must be sent to ``self.stdout``. You -can use the methods described above, and everything will work fine. -:mod:`cmd2.Cmd` also includes a number of output related methods which you -may use to enhance the output your application produces. - - -Ordinary Output ---------------- - -The :meth:`~cmd2.Cmd.poutput` method is similar to the Python -`built-in print function `_. :meth:`~cmd2.Cmd.poutput` adds two -conveniences: - - 1. Since users can pipe output to a shell command, it catches - ``BrokenPipeError`` and outputs the contents of - ``self.broken_pipe_warning`` to ``stderr``. ``self.broken_pipe_warning`` - defaults to an empty string so this method will just swallow the exception. - If you want to show an error message, put it in - ``self.broken_pipe_warning`` when you initialize :mod:`~cmd2.Cmd`. - - 2. It examines and honors the :ref:`features/settings:allow_style` setting. - See :ref:`features/generating_output:Colored Output` below for more details. - -Here's a simple command that shows this method in action:: - - def do_echo(self, args): - """A simple command showing how poutput() works""" - self.poutput(args) - - -Error Messages --------------- - -When an error occurs in your program, you can display it on ``sys.stderr`` by -calling the :meth:`~.cmd2.Cmd.perror` method. By default this method applies -:meth:`cmd2.ansi.style_error` to the output. - - -Warning Messages ----------------- - -:meth:`~.cmd2.Cmd.pwarning` is just like :meth:`~.cmd2.Cmd.perror` but applies -:meth:`cmd2.ansi.style_warning` to the output. - - -Feedback --------- - -You may have the need to display information to the user which is not intended -to be part of the generated output. This could be debugging information or -status information about the progress of long running commands. It's not -output, it's not error messages, it's feedback. If you use the -:ref:`features/settings:Timing` setting, the output of how long it took the -command to run will be output as feedback. You can use the -:meth:`~.cmd2.Cmd.pfeedback` method to produce this type of output, and -several :ref:`features/settings:Settings` control how it is handled. - -If the :ref:`features/settings:quiet` setting is ``True``, then calling -:meth:`~.cmd2.Cmd.pfeedback` produces no output. If -:ref:`features/settings:quiet` is ``False``, the -:ref:`features/settings:feedback_to_output` setting is consulted to determine -whether to send the output to ``stdout`` or ``stderr``. - - -Exceptions ----------- - -If your app catches an exception and you would like to display the exception to -the user, the :meth:`~.cmd2.Cmd.pexcept` method can help. The default behavior -is to just display the message contained within the exception. However, if the -:ref:`features/settings:debug` setting is ``True``, then the entire stack trace -will be displayed. - - -Paging Output -------------- - -If you know you are going to generate a lot of output, you may want to display -it in a way that the user can scroll forwards and backwards through it. If you -pass all of the output to be displayed in a single call to -:meth:`~.cmd2.Cmd.ppaged`, it will be piped to an operating system appropriate -shell command to page the output. On Windows, the output is piped to ``more``; -on Unix-like operating systems like MacOS and Linux, it is piped to ``less``. - - -Colored Output --------------- - -You can add your own `ANSI escape sequences -`_ to your output which -tell the terminal to change the foreground and background colors. - -``cmd2`` provides a number of convenience functions and classes for adding -color and other styles to text. These are all documented in :mod:`cmd2.ansi`. - -After adding the desired escape sequences to your output, you should use one of -these methods to present the output to the user: - -- :meth:`.cmd2.Cmd.poutput` -- :meth:`.cmd2.Cmd.perror` -- :meth:`.cmd2.Cmd.pwarning` -- :meth:`.cmd2.Cmd.pexcept` -- :meth:`.cmd2.Cmd.pfeedback` -- :meth:`.cmd2.Cmd.ppaged` - -These methods all honor the :ref:`features/settings:allow_style` setting, which -users can modify to control whether these escape codes are passed through to -the terminal or not. - - -Aligning Text --------------- - -If you would like to generate output which is left, center, or right aligned -within a specified width or the terminal width, the following functions can -help: - -- :meth:`cmd2.utils.align_left` -- :meth:`cmd2.utils.align_center` -- :meth:`cmd2.utils.align_right` - -These functions differ from Python's string justifying functions in that they -support characters with display widths greater than 1. Additionally, ANSI style -sequences are safely ignored and do not count toward the display width. This -means colored text is supported. If text has line breaks, then each line is -aligned independently. - - - -Columnar Output ---------------- - -When generating output in multiple columns, you often need to calculate the -width of each item so you can pad it appropriately with spaces. However, there -are categories of Unicode characters that occupy 2 cells, and other that occupy -0. To further complicate matters, you might have included ANSI escape sequences -in the output to generate colors on the terminal. - -The :meth:`cmd2.ansi.style_aware_wcswidth` function solves both of these -problems. Pass it a string, and regardless of which Unicode characters and ANSI -text style escape sequences it contains, it will tell you how many characters -on the screen that string will consume when printed. diff --git a/docs/features/help.md b/docs/features/help.md new file mode 100644 index 000000000..52f8e5980 --- /dev/null +++ b/docs/features/help.md @@ -0,0 +1,163 @@ +# Help + +From our experience, end users rarely read documentation no matter how high-quality or useful that documentation might be. So it is important that you provide good built-in help within your application. Fortunately, `cmd2` makes this easy. + +## Getting Help + +`cmd2` makes it easy for end users of `cmd2` applications to get help via the built-in `help` command. The `help` command by itself displays a list of the commands available: + +``` text +(Cmd) help + +Documented commands (use 'help -v' for verbose/'help ' for details): +=========================================================================== +alias help ipy py run_pyscript set shortcuts +edit history macro quit run_script shell +``` + +The `help` command can also be used to provide detailed help for a specific command: + +``` text +(Cmd) help quit +Usage: quit [-h] + +Exit this application + +optional arguments: + -h, --help show this help message and exit +``` + +## Providing Help + +`cmd2` makes it easy for developers of `cmd2` applications to provide this help. By default, the help for a command is the docstring for the `do_*` method defining the command - e.g. for a command **foo**, that command is implemented by defining the `do_foo` method and the docstring for that method is the help. + +For commands which use one of the `argparse` decorators to parse arguments, help is provided by `argparse`. See `features/argument_processing:Help Messages`{.interpreted-text role="ref"} for more information. + +Occasionally there might be an unusual circumstance where providing static help text isn't good enough and you want to provide dynamic information in the help text for a command. To meet this need, if a `help_foo` method is defined to match the `do_foo` method, then that method will be used to provide the help for command **foo**. This dynamic help is only supported for commands which do not use an `argparse` decorator because didn't want different output for `help cmd` than for `cmd -h`. + +## Categorizing Commands + +By default, the `help` command displays: + + Documented commands (use 'help -v' for verbose/'help ' for details): + =========================================================================== + alias help ipy py run_pyscript set shortcuts + edit history macro quit run_script shell + +If you have a large number of commands, you can optionally group your commands into categories. Here's the output from the example `help_categories.py`: + + Documented commands (use 'help -v' for verbose/'help ' for details): + + Application Management + ====================== + deploy findleakers redeploy sessions stop + expire list restart start undeploy + + Command Management + ================== + disable_commands enable_commands + + Connecting + ========== + connect which + + Server Information + ================== + resources serverinfo sslconnectorciphers status thread_dump vminfo + + Other + ===== + alias edit history py run_pyscript set shortcuts + config help macro quit run_script shell version + +There are 2 methods of specifying command categories, using the `@with_category` decorator or with the `categorize()` function. Once a single command category is detected, the help output switches to a categorized mode of display. All commands with an explicit category defined default to the category ``Other``. + +Using the `@with_category` decorator: + + @with_category(CMD_CAT_CONNECTING) + def do_which(self, _): + """Which command""" + self.poutput('Which') + +Using the `categorize()` function: + +> You can call with a single function: +> +> def do_connect(self, _): +> """Connect command""" +> self.poutput('Connect') +> +> # Tag the above command functions under the category Connecting +> categorize(do_connect, CMD_CAT_CONNECTING) +> +> Or with an Iterable container of functions: +> +> def do_undeploy(self, _): +> """Undeploy command""" +> self.poutput('Undeploy') +> +> def do_stop(self, _): +> """Stop command""" +> self.poutput('Stop') +> +> def do_findleakers(self, _): +> """Find Leakers command""" +> self.poutput('Find Leakers') +> +> # Tag the above command functions under the category Application Management +> categorize((do_undeploy, +> do_stop, +> do_findleakers), CMD_CAT_APP_MGMT) + +The `help` command also has a verbose option (`help -v` or `help --verbose`) that combines the help categories with per-command Help Messages: + + Documented commands (use 'help -v' for verbose/'help ' for details): + + Application Management + ================================================================================ + deploy Deploy command + expire Expire command + findleakers Find Leakers command + list List command + redeploy Redeploy command + restart usage: restart [-h] {now,later,sometime,whenever} + sessions Sessions command + start Start command + stop Stop command + undeploy Undeploy command + + Connecting + ================================================================================ + connect Connect command + which Which command + + Server Information + ================================================================================ + resources Resources command + serverinfo Server Info command + sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains + multiple lines of help information for the user. Each line of help in a + contiguous set of lines will be printed and aligned in the verbose output + provided with 'help --verbose' + status Status command + thread_dump Thread Dump command + vminfo VM Info command + + Other + ================================================================================ + alias Manage aliases + config Config command + edit Run a text editor and optionally open a file with it + help List available commands or provide detailed help for a specific command + history View, run, edit, save, or clear previously entered commands + macro Manage macros + py Invoke Python command or shell + quit Exits this application + run_pyscript Runs a python script file inside the console + run_script Runs commands in script file that is encoded as either ASCII or UTF-8 text + set Set a settable parameter or show current settings of parameters + shell Execute a command as if at the OS prompt + shortcuts List available shortcuts + version Version command + +When called with the `-v` flag for verbose help, the one-line description for each command is provided by the first line of the docstring for that command's associated `do_*` method. diff --git a/docs/features/help.rst b/docs/features/help.rst deleted file mode 100644 index b98e4164d..000000000 --- a/docs/features/help.rst +++ /dev/null @@ -1,193 +0,0 @@ -Help -==== - -From our experience, end users rarely read documentation no matter how high- -quality or useful that documentation might be. So it is important that you -provide good built-in help within your application. Fortunately, ``cmd2`` -makes this easy. - -Getting Help ------------- - -``cmd2`` makes it easy for end users of ``cmd2`` applications to get help via -the built-in ``help`` command. The ``help`` command by itself displays a list -of the commands available: - -.. code-block:: text - - (Cmd) help - - Documented commands (use 'help -v' for verbose/'help ' for details): - =========================================================================== - alias help ipy py run_pyscript set shortcuts - edit history macro quit run_script shell - -The ``help`` command can also be used to provide detailed help for a specific -command: - -.. code-block:: text - - (Cmd) help quit - Usage: quit [-h] - - Exit this application - - optional arguments: - -h, --help show this help message and exit - -Providing Help --------------- - -``cmd2`` makes it easy for developers of ``cmd2`` applications to provide this -help. By default, the help for a command is the docstring for the ``do_*`` -method defining the command - e.g. for a command **foo**, that command is -implemented by defining the ``do_foo`` method and the docstring for that method -is the help. - -For commands which use one of the ``argparse`` decorators to parse arguments, -help is provided by ``argparse``. See -:ref:`features/argument_processing:Help Messages` for more information. - -Occasionally there might be an unusual circumstance where providing static help -text isn't good enough and you want to provide dynamic information in the help -text for a command. To meet this need, if a ``help_foo`` method is defined to -match the ``do_foo`` method, then that method will be used to provide the help -for command **foo**. This dynamic help is only supported for commands which -do not use an ``argparse`` decorator because didn't want different output for -``help cmd`` than for ``cmd -h``. - -Categorizing Commands ---------------------- - -By default, the ``help`` command displays:: - - Documented commands (use 'help -v' for verbose/'help ' for details): - =========================================================================== - alias help ipy py run_pyscript set shortcuts - edit history macro quit run_script shell - -If you have a large number of commands, you can optionally group your commands -into categories. Here's the output from the example ``help_categories.py``:: - - Documented commands (use 'help -v' for verbose/'help ' for details): - - Application Management - ====================== - deploy findleakers redeploy sessions stop - expire list restart start undeploy - - Command Management - ================== - disable_commands enable_commands - - Connecting - ========== - connect which - - Server Information - ================== - resources serverinfo sslconnectorciphers status thread_dump vminfo - - Other - ===== - alias edit history py run_pyscript set shortcuts - config help macro quit run_script shell version - -There are 2 methods of specifying command categories, using the -``@with_category`` decorator or with the ``categorize()`` function. Once a -single command category is detected, the help output switches to a categorized -mode of display. All commands with an explicit category defined default to the -category `Other`. - -Using the ``@with_category`` decorator:: - - @with_category(CMD_CAT_CONNECTING) - def do_which(self, _): - """Which command""" - self.poutput('Which') - -Using the ``categorize()`` function: - - You can call with a single function:: - - def do_connect(self, _): - """Connect command""" - self.poutput('Connect') - - # Tag the above command functions under the category Connecting - categorize(do_connect, CMD_CAT_CONNECTING) - - Or with an Iterable container of functions:: - - def do_undeploy(self, _): - """Undeploy command""" - self.poutput('Undeploy') - - def do_stop(self, _): - """Stop command""" - self.poutput('Stop') - - def do_findleakers(self, _): - """Find Leakers command""" - self.poutput('Find Leakers') - - # Tag the above command functions under the category Application Management - categorize((do_undeploy, - do_stop, - do_findleakers), CMD_CAT_APP_MGMT) - -The ``help`` command also has a verbose option (``help -v`` or ``help ---verbose``) that combines the help categories with per-command Help Messages:: - - Documented commands (use 'help -v' for verbose/'help ' for details): - - Application Management - ================================================================================ - deploy Deploy command - expire Expire command - findleakers Find Leakers command - list List command - redeploy Redeploy command - restart usage: restart [-h] {now,later,sometime,whenever} - sessions Sessions command - start Start command - stop Stop command - undeploy Undeploy command - - Connecting - ================================================================================ - connect Connect command - which Which command - - Server Information - ================================================================================ - resources Resources command - serverinfo Server Info command - sslconnectorciphers SSL Connector Ciphers command is an example of a command that contains - multiple lines of help information for the user. Each line of help in a - contiguous set of lines will be printed and aligned in the verbose output - provided with 'help --verbose' - status Status command - thread_dump Thread Dump command - vminfo VM Info command - - Other - ================================================================================ - alias Manage aliases - config Config command - edit Run a text editor and optionally open a file with it - help List available commands or provide detailed help for a specific command - history View, run, edit, save, or clear previously entered commands - macro Manage macros - py Invoke Python command or shell - quit Exits this application - run_pyscript Runs a python script file inside the console - run_script Runs commands in script file that is encoded as either ASCII or UTF-8 text - set Set a settable parameter or show current settings of parameters - shell Execute a command as if at the OS prompt - shortcuts List available shortcuts - version Version command - -When called with the ``-v`` flag for verbose help, the one-line description for -each command is provided by the first line of the docstring for that command's -associated ``do_*`` method. diff --git a/docs/features/history.md b/docs/features/history.md new file mode 100644 index 000000000..34ba9b13b --- /dev/null +++ b/docs/features/history.md @@ -0,0 +1,168 @@ +# History + +## For Developers + +The `cmd` module from the Python standard library includes `readline` history. + +`cmd2.Cmd`{.interpreted-text role="class"} offers the same `readline` capabilities, but also maintains its own data structures for the history of all commands entered by the user. When the class is initialized, it creates an instance of the `cmd2.history.History`{.interpreted-text role="class"} class (which is a subclass of `list`) as `cmd2.Cmd.history`{.interpreted-text role="data"}. + +Each time a command is executed (this gets complex, see `features/hooks:Command Processing Loop`{.interpreted-text role="ref"} for exactly when) the parsed `cmd2.Statement`{.interpreted-text role="class"} is appended to `cmd2.Cmd.history`{.interpreted-text role="data"}. + +`cmd2` adds the option of making this history persistent via optional arguments to `cmd2.Cmd.__init__`{.interpreted-text role="meth"}. If you pass a filename in the `persistent_history_file` argument, the contents of `cmd2.Cmd.history`{.interpreted-text role="data"} will be written as compressed JSON to that history file. We chose this format instead of plain text to preserve the complete `cmd2.Statement`{.interpreted-text role="class"} object for each command. + +!!! note + + `readline` saves everything you type, whether it is a valid command or not. `cmd2` only saves input to internal history if the command parses successfully and is a valid command. This design choice was intentional, because the contents of history can be saved to a file as a script, or can be re-run. Not saving invalid input reduces unintentional errors when doing so. + + However, this design choice causes an inconsistency between the `readline` history and the `cmd2` history when you enter an invalid command: it is saved to the `readline` history, but not to the `cmd2` history. + +The `cmd2.Cmd.history`{.interpreted-text role="data"} attribute, the `cmd2.history.History`{.interpreted-text role="class"} class, and the `cmd2.history.HistoryItem`{.interpreted-text role="class"} class are all part of the public API for `cmd2.Cmd`{.interpreted-text role="class"}. You could use these classes to implement write your own `history` command (see below for documentation on how the included `history` command works). + +## For Users + +You can use the up and down arrow keys to move through the history of previously entered commands. + +If the `readline` module is installed, you can press `Control-p` to move to the previously entered command, and `Control-n` to move to the next command. You can also search through the command history using `Control-r`. + +Eric Johnson hosts a nice [readline cheat sheet](http://readline.kablamo.org/emacs.html), or you can dig into the [GNU Readline User Manual](http://man7.org/linux/man-pages/man3/readline.3.html) for all the details, including instructions for customizing the key bindings. + +`cmd2` makes a third type of history access available with the `history` command. Each time the user enters a command, `cmd2` saves the input. The `history` command lets you do interesting things with that saved input. The examples to follow all assume that you have entered the following commands: + + (Cmd) alias create one !echo one + Alias 'one' created + (Cmd) alias create two !echo two + Alias 'two' created + (Cmd) alias create three !echo three + Alias 'three' created + (Cmd) alias create four !echo four + Alias 'four' created + +In it's simplest form, the `history` command displays previously entered commands. With no additional arguments, it displays all previously entered commands: + + (Cmd) history + 1 alias create one !echo one + 2 alias create two !echo two + 3 alias create three !echo three + 4 alias create four !echo four + +If you give a positive integer as an argument, then it only displays the specified command: + + (Cmd) history 4 + 4 alias create four !echo four + +If you give a negative integer _N_ as an argument, then it display the _Nth_ last command. For example, if you give `-1` it will display the last command you entered. If you give `-2` it will display the next to last command you entered, and so forth: + + (Cmd) history -2 + 3 alias create three !echo three + +You can use a similar mechanism to display a range of commands. Simply give two command numbers separated by `..` or `:`, and you will see all commands between, and including, those two numbers: + + (Cmd) history 1:3 + 1 alias create one !echo one + 2 alias create two !echo two + 3 alias create three !echo three + +If you omit the first number, it will start at the beginning. If you omit the last number, it will continue to the end: + + (Cmd) history :2 + 1 alias create one !echo one + 2 alias create two !echo two + (Cmd) history 2: + 2 alias create two !echo two + 3 alias create three !echo three + 4 alias create four !echo four + +If you want to display the last three commands entered: + + (Cmd) history -- -3: + 2 alias create two !echo two + 3 alias create three !echo three + 4 alias create four !echo four + +Notice the double dashes. These are required because the history command uses `argparse` to parse the command line arguments. As described in the [argparse documentation](https://docs.python.org/3/library/argparse.html) , `-3:` is an option, not an argument: + +> If you have positional arguments that must begin with - and don't look like negative numbers, you can insert the pseudo-argument '--' which tells parse[args]{#args}() that everything after that is a positional argument: + +There is no zeroth command, so don't ask for it. If you are a python programmer, you've probably noticed this looks a lot like the slice syntax for lists and arrays. It is, with the exception that the first history command is 1, where the first element in a python array is 0. + +Besides selecting previous commands by number, you can also search for them. You can use a simple string search: + + (Cmd) history two + 2 alias create two !echo two + +Or a regular expression search by enclosing your regex in slashes: + + (Cmd) history '/te\ +th/' + 3 alias create three !echo three + +If your regular expression contains any characters that `argparse` finds interesting, like dash or plus, you also need to enclose your regular expression in quotation marks. + +This all sounds great, but doesn't it seem like a bit of overkill to have all these ways to select commands if all we can do is display them? Turns out, displaying history commands is just the beginning. The history command can perform many other actions: + +- running previously entered commands +- saving previously entered commands to a text file +- opening previously entered commands in your favorite text editor +- running previously entered commands, saving the commands and their output to a text file +- clearing the history of entered commands + +Each of these actions is invoked using a command line option. The `-r` or `--run` option runs one or more previously entered commands. To run command number 1: + + (Cmd) history --run 1 + +To rerun the last two commands (there's that double dash again to make argparse stop looking for options): + + (Cmd) history -r -- -2: + +Say you want to re-run some previously entered commands, but you would really like to make a few changes to them before doing so. When you use the `-e` or `--edit` option, `history` will write the selected commands out to a text file, and open that file with a text editor. You make whatever changes, additions, or deletions, you want. When you leave the text editor, all the commands in the file are executed. To edit and then re-run commands 2-4 you would: + + (Cmd) history --edit 2:4 + +If you want to save the commands to a text file, but not edit and re-run them, use the `-o` or `--output-file` option. This is a great way to create `Scripts `{.interpreted-text role="ref"}, which can be executed using the `run_script` command. To save the first 5 commands entered in this session to a text file: + + (Cmd) history :5 -o history.txt + +The `history` command can also save both the commands and their output to a text file. This is called a transcript. See `features/transcripts:Transcripts`{.interpreted-text role="ref"} for more information on how transcripts work, and what you can use them for. To create a transcript use the `-t` or `--transcription` option: + + (Cmd) history 2:3 --transcript transcript.txt + +The `--transcript` option implies `--run`: the commands must be re-run in order to capture their output to the transcript file. + +The last action the history command can perform is to clear the command history using `-c` or `--clear`: + + (Cmd) history -c + +In addition to these five actions, the `history` command also has some options to control how the output is formatted. With no arguments, the `history` command displays the command number before each command. This is great when displaying history to the screen because it gives you an easy reference to identify previously entered commands. However, when creating a script or a transcript, the command numbers would prevent the script from loading properly. The `-s` or `--script` option instructs the `history` command to suppress the line numbers. This option is automatically set by the `--output_file`, `--transcript`, and `--edit` options. If you want to output the history commands with line numbers to a file, you can do it with output redirection: + + (Cmd) history 1:4 > history.txt + +You might use `-s` or `--script` on it's own if you want to display history commands to the screen without line numbers, so you can copy them to the clipboard: + + (Cmd) history -s 1:3 + +`cmd2` supports both aliases and macros, which allow you to substitute a short, more convenient input string with a longer replacement string. Say we create an alias like this, and then use it: + + (Cmd) alias create ls shell ls -aF + Alias 'ls' created + (Cmd) ls -d h* + history.txt htmlcov/ + +By default, the `history` command shows exactly what we typed: + + (Cmd) history + 1 alias create ls shell ls -aF + 2 ls -d h* + +There are two ways to modify that display so you can see what aliases and macros were expanded to. The first is to use `-x` or `--expanded`. These options show the expanded command instead of the entered command: + + (Cmd) history -x + 1 alias create ls shell ls -aF + 2 shell ls -aF -d h* + +If you want to see both the entered command and the expanded command, use the `-v` or `--verbose` option: + + (Cmd) history -v + 1 alias create ls shell ls -aF + 2 ls -d h* + 2x shell ls -aF -d h* + +If the entered command had no expansion, it is displayed as usual. However, if there is some change as the result of expanding macros and aliases, then the entered command is displayed with the number, and the expanded command is displayed with the number followed by an `x`. diff --git a/docs/features/history.rst b/docs/features/history.rst deleted file mode 100644 index 056e02a0b..000000000 --- a/docs/features/history.rst +++ /dev/null @@ -1,269 +0,0 @@ -History -======= - -For Developers --------------- - -The ``cmd`` module from the Python standard library includes ``readline`` -history. - -:class:`cmd2.Cmd` offers the same ``readline`` capabilities, but also maintains -its own data structures for the history of all commands entered by the user. -When the class is initialized, it creates an instance of the -:class:`cmd2.history.History` class (which is a subclass of ``list``) as -:data:`cmd2.Cmd.history`. - -Each time a command is executed (this gets -complex, see :ref:`features/hooks:Command Processing Loop` for exactly when) -the parsed :class:`cmd2.Statement` is appended to :data:`cmd2.Cmd.history`. - -``cmd2`` adds the option of making this history persistent via optional -arguments to :meth:`cmd2.Cmd.__init__`. If you pass a filename in the -``persistent_history_file`` argument, the contents of :data:`cmd2.Cmd.history` -will be written as compressed JSON to that history file. We chose this format -instead of plain text to preserve the complete :class:`cmd2.Statement` object -for each command. - -.. note:: - - ``readline`` saves everything you type, whether it is a valid command or - not. ``cmd2`` only saves input to internal history if the command parses - successfully and is a valid command. This design choice was intentional, - because the contents of history can be saved to a file as a script, or can - be re-run. Not saving invalid input reduces unintentional errors when doing - so. - - However, this design choice causes an inconsistency between the - ``readline`` history and the ``cmd2`` history when you enter an invalid - command: it is saved to the ``readline`` history, but not to the ``cmd2`` - history. - -The :data:`cmd2.Cmd.history` attribute, the :class:`cmd2.history.History` -class, and the :class:`cmd2.history.HistoryItem` class are all part of the -public API for :class:`cmd2.Cmd`. You could use these classes to implement -write your own ``history`` command (see below for documentation on how the -included ``history`` command works). - - -For Users ---------- - -You can use the up and down arrow keys to move through the history of -previously entered commands. - -If the ``readline`` module is installed, you can press ``Control-p`` to move to -the previously entered command, and ``Control-n`` to move to the next command. -You can also search through the command history using ``Control-r``. - -Eric Johnson hosts a nice `readline cheat sheet -`_, or you can dig into the `GNU -Readline User Manual -`_ for all the -details, including instructions for customizing the key bindings. - -``cmd2`` makes a third type of history access available with the ``history`` -command. Each time the user enters a command, ``cmd2`` saves the input. The -``history`` command lets you do interesting things with that saved input. The -examples to follow all assume that you have entered the following commands:: - - (Cmd) alias create one !echo one - Alias 'one' created - (Cmd) alias create two !echo two - Alias 'two' created - (Cmd) alias create three !echo three - Alias 'three' created - (Cmd) alias create four !echo four - Alias 'four' created - -In it's simplest form, the ``history`` command displays previously entered -commands. With no additional arguments, it displays all previously entered -commands:: - - (Cmd) history - 1 alias create one !echo one - 2 alias create two !echo two - 3 alias create three !echo three - 4 alias create four !echo four - -If you give a positive integer as an argument, then it only displays the -specified command:: - - (Cmd) history 4 - 4 alias create four !echo four - -If you give a negative integer *N* as an argument, then it display the *Nth* -last command. For example, if you give ``-1`` it will display the last command -you entered. If you give ``-2`` it will display the next to last command you -entered, and so forth:: - - (Cmd) history -2 - 3 alias create three !echo three - -You can use a similar mechanism to display a range of commands. Simply give two -command numbers separated by ``..`` or ``:``, and you will see all commands -between, and including, those two numbers:: - - (Cmd) history 1:3 - 1 alias create one !echo one - 2 alias create two !echo two - 3 alias create three !echo three - -If you omit the first number, it will start at the beginning. If you omit the -last number, it will continue to the end:: - - (Cmd) history :2 - 1 alias create one !echo one - 2 alias create two !echo two - (Cmd) history 2: - 2 alias create two !echo two - 3 alias create three !echo three - 4 alias create four !echo four - -If you want to display the last three commands entered:: - - (Cmd) history -- -3: - 2 alias create two !echo two - 3 alias create three !echo three - 4 alias create four !echo four - -Notice the double dashes. These are required because the history command uses -``argparse`` to parse the command line arguments. As described in the `argparse -documentation `_ , ``-3:`` is -an option, not an argument: - - If you have positional arguments that must begin with - and don’t look - like negative numbers, you can insert the pseudo-argument '--' which tells - parse_args() that everything after that is a positional argument: - -There is no zeroth command, so don't ask for it. If you are a python -programmer, you've probably noticed this looks a lot like the slice syntax for -lists and arrays. It is, with the exception that the first history command is -1, where the first element in a python array is 0. - -Besides selecting previous commands by number, you can also search for them. -You can use a simple string search:: - - (Cmd) history two - 2 alias create two !echo two - -Or a regular expression search by enclosing your regex in slashes:: - - (Cmd) history '/te\ +th/' - 3 alias create three !echo three - -If your regular expression contains any characters that ``argparse`` finds -interesting, like dash or plus, you also need to enclose your regular -expression in quotation marks. - -This all sounds great, but doesn't it seem like a bit of overkill to have all -these ways to select commands if all we can do is display them? Turns out, -displaying history commands is just the beginning. The history command can -perform many other actions: - -- running previously entered commands -- saving previously entered commands to a text file -- opening previously entered commands in your favorite text editor -- running previously entered commands, saving the commands and their output - to a text file -- clearing the history of entered commands - -Each of these actions is invoked using a command line option. The ``-r`` or -``--run`` option runs one or more previously entered commands. To run command -number 1:: - - (Cmd) history --run 1 - -To rerun the last two commands (there's that double dash again to make argparse -stop looking for options):: - - (Cmd) history -r -- -2: - -Say you want to re-run some previously entered commands, but you would really -like to make a few changes to them before doing so. When you use the ``-e`` or -``--edit`` option, ``history`` will write the selected commands out to a text -file, and open that file with a text editor. You make whatever changes, -additions, or deletions, you want. When you leave the text editor, all the -commands in the file are executed. To edit and then re-run commands 2-4 you -would:: - - (Cmd) history --edit 2:4 - -If you want to save the commands to a text file, but not edit and re-run them, -use the ``-o`` or ``--output-file`` option. This is a great way to create -:ref:`Scripts `, which can be executed using the -``run_script`` command. To save the first 5 commands entered in this session to -a text file:: - - (Cmd) history :5 -o history.txt - -The ``history`` command can also save both the commands and their output to a -text file. This is called a transcript. See -:ref:`features/transcripts:Transcripts` for more information on how transcripts -work, and what you can use them for. To create a transcript use the ``-t`` or -``--transcription`` option:: - - (Cmd) history 2:3 --transcript transcript.txt - -The ``--transcript`` option implies ``--run``: the commands must be re-run in -order to capture their output to the transcript file. - -The last action the history command can perform is to clear the command history -using ``-c`` or ``--clear``:: - - (Cmd) history -c - -In addition to these five actions, the ``history`` command also has some -options to control how the output is formatted. With no arguments, the -``history`` command displays the command number before each command. This is -great when displaying history to the screen because it gives you an easy -reference to identify previously entered commands. However, when creating a -script or a transcript, the command numbers would prevent the script from -loading properly. The ``-s`` or ``--script`` option instructs the ``history`` -command to suppress the line numbers. This option is automatically set by the -``--output_file``, ``--transcript``, and ``--edit`` options. If you want to -output the history commands with line numbers to a file, you can do it with -output redirection:: - - (Cmd) history 1:4 > history.txt - -You might use ``-s`` or ``--script`` on it's own if you want to display history -commands to the screen without line numbers, so you can copy them to the -clipboard:: - - (Cmd) history -s 1:3 - -``cmd2`` supports both aliases and macros, which allow you to substitute a -short, more convenient input string with a longer replacement string. Say we -create an alias like this, and then use it:: - - (Cmd) alias create ls shell ls -aF - Alias 'ls' created - (Cmd) ls -d h* - history.txt htmlcov/ - -By default, the ``history`` command shows exactly what we typed:: - - (Cmd) history - 1 alias create ls shell ls -aF - 2 ls -d h* - -There are two ways to modify that display so you can see what aliases and -macros were expanded to. The first is to use ``-x`` or ``--expanded``. These -options show the expanded command instead of the entered command:: - - (Cmd) history -x - 1 alias create ls shell ls -aF - 2 shell ls -aF -d h* - -If you want to see both the entered command and the expanded command, use the -``-v`` or ``--verbose`` option:: - - (Cmd) history -v - 1 alias create ls shell ls -aF - 2 ls -d h* - 2x shell ls -aF -d h* - -If the entered command had no expansion, it is displayed as usual. However, if -there is some change as the result of expanding macros and aliases, then the -entered command is displayed with the number, and the expanded command is -displayed with the number followed by an ``x``. diff --git a/docs/features/hooks.md b/docs/features/hooks.md new file mode 100644 index 000000000..45fa0b3a5 --- /dev/null +++ b/docs/features/hooks.md @@ -0,0 +1,211 @@ +# Hooks + +The typical way of starting a `cmd2` application is as follows: + + import cmd2 + class App(cmd2.Cmd): + # customized attributes and methods here + + if __name__ == '__main__': + app = App() + app.cmdloop() + +There are several pre-existing methods and attributes which you can tweak to control the overall behavior of your application before, during, and after the command processing loop. + +## Application Lifecycle Hooks + +You can run a script on initialization by passing the script filename in the `startup_script` parameter of `cmd2.Cmd.__init__`{.interpreted-text role="meth"}. + +You can also register methods to be called at the beginning of the command loop: + + class App(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_preloop_hook(self.myhookmethod) + + def myhookmethod(self) -> None: + self.poutput("before the loop begins") + +To retain backwards compatibility with `cmd.Cmd`, after all registered preloop hooks have been called, the `~cmd2.Cmd.preloop`{.interpreted-text role="meth"} method is called. + +A similar approach allows you to register functions to be called after the command loop has finished: + + class App(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_postloop_hook(self.myhookmethod) + + def myhookmethod(self) -> None: + self.poutput("after the loop ends") + +To retain backwards compatibility with `cmd.Cmd`, after all registered postloop hooks have been called, the `~cmd2.Cmd.postloop`{.interpreted-text role="meth"} method is called. + +Preloop and postloop hook methods are not passed any parameters and any return value is ignored. + +The approach of registering hooks instead of overriding methods allows multiple hooks to be called before the command loop begins or ends. Plugin authors should review `features/plugins:Hooks`{.interpreted-text role="ref"} for best practices writing hooks. + +## Application Lifecycle Attributes + +There are numerous attributes on `cmd2.Cmd`{.interpreted-text role="class"} which affect application behavior upon entering or during the command loop: + +- `~cmd2.Cmd.intro`{.interpreted-text role="data"} - if provided this serves as the intro banner printed once at start of application, after `~cmd2.Cmd.preloop`{.interpreted-text role="meth"} is called. +- `~cmd2.Cmd.prompt`{.interpreted-text role="data"} - see `features/prompt:Prompt`{.interpreted-text role="ref"} for more information. +- `~cmd2.Cmd.continuation_prompt`{.interpreted-text role="data"} - The prompt issued to solicit input for the 2nd and subsequent lines of a `multiline command `{.interpreted-text role="ref"} +- `~cmd2.Cmd.echo`{.interpreted-text role="data"} - if `True` write the prompt and the command into the output stream. + +In addition, several arguments to `cmd2.Cmd.__init__`{.interpreted-text role="meth"} also affect the command loop behavior: + +- `allow_cli_args` - allows commands to be specified on the operating system command line which are executed before the command processing loop begins. +- `transcript_files` - see `features/transcripts:Transcripts`{.interpreted-text role="ref"} for more information +- `startup_script` - run a script on initialization. See `features/scripting:Scripting`{.interpreted-text role="ref"} for more information. + +## Command Processing Loop + +When you call `cmd2.Cmd.cmdloop`{.interpreted-text role="meth"}, the following sequence of events are repeated until the application exits: + +1. Output the prompt +2. Accept user input +3. Parse user input into a `~cmd2.Statement`{.interpreted-text role="class"} object +4. Call methods registered with `~cmd2.Cmd.register_postparsing_hook()`{.interpreted-text role="meth"} +5. Redirect output, if user asked for it and it's allowed +6. Start timer +7. Call methods registered with `~cmd2.Cmd.register_precmd_hook`{.interpreted-text role="meth"} +8. Call `~cmd2.Cmd.precmd`{.interpreted-text role="meth"} - for backwards compatibility with `cmd.Cmd` +9. Add statement to `features/history:History`{.interpreted-text role="ref"} +10. Call ``do_command`` method +11. Call methods registered with `~cmd2.Cmd.register_postcmd_hook()`{.interpreted-text role="meth"} +12. Call `~cmd2.Cmd.postcmd`{.interpreted-text role="meth"} - for backwards compatibility with `cmd.Cmd` +13. Stop timer and display the elapsed time +14. Stop redirecting output if it was redirected +15. Call methods registered with `~cmd2.Cmd.register_cmdfinalization_hook()`{.interpreted-text role="meth"} + +By registering hook methods, steps 4, 8, 12, and 16 allow you to run code during, and control the flow of the command processing loop. Be aware that plugins also utilize these hooks, so there may be code running that is not part of your application. Methods registered for a hook are called in the order they were registered. You can register a function more than once, and it will be called each time it was registered. + +Postparsing, precommand, and postcommand hook methods share some common ways to influence the command processing loop. + +If a hook raises an exception: + +- no more hooks (except command finalization hooks) of any kind will be called +- if the command has not yet been executed, it will not be executed +- the exception message will be displayed for the user. + +Specific types of hook methods have additional options as described below. + +## Postparsing Hooks + +Postparsing hooks are called after the user input has been parsed but before execution of the command. These hooks can be used to: + +- modify the user input +- run code before every command executes +- cancel execution of the current command +- exit the application + +When postparsing hooks are called, output has not been redirected, nor has the timer for command execution been started. + +To define and register a postparsing hook, do the following: + + class App(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_postparsing_hook(self.myhookmethod) + + def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: + # the statement object created from the user input + # is available as params.statement + return params + +`~cmd2.Cmd.register_postparsing_hook`{.interpreted-text role="meth"} checks the method signature of the passed callable, and raises a `TypeError` if it has the wrong number of parameters. It will also raise a `TypeError` if the passed parameter and return value are not annotated as `PostparsingData`. + +The hook method will be passed one parameter, a `~cmd2.plugin.PostparsingData`{.interpreted-text role="class"} object which we will refer to as `params`. `params` contains two attributes. `params.statement` is a `~cmd2.Statement`{.interpreted-text role="class"} object which describes the parsed user input. There are many useful attributes in the `~cmd2.Statement`{.interpreted-text role="class"} object, including `.raw` which contains exactly what the user typed. `params.stop` is set to `False` by default. + +The hook method must return a `cmd2.plugin.PostparsingData`{.interpreted-text role="class"} object, and it is very convenient to just return the object passed into the hook method. The hook method may modify the attributes of the object to influence the behavior of the application. If `params.stop` is set to true, a fatal failure is triggered prior to execution of the command, and the application exits. + +To modify the user input, you create a new `~cmd2.Statement`{.interpreted-text role="class"} object and return it in `params.statement`. Don't try and directly modify the contents of a `~cmd2.Statement`{.interpreted-text role="class"} object, there be dragons. Instead, use the various attributes in a `~cmd2.Statement`{.interpreted-text role="class"} object to construct a new string, and then parse that string to create a new `~cmd2.Statement`{.interpreted-text role="class"} object. + +`cmd2.Cmd`{.interpreted-text role="class"} uses an instance of `~cmd2.parsing.StatementParser`{.interpreted-text role="class"} to parse user input. This instance has been configured with the proper command terminators, multiline commands, and other parsing related settings. This instance is available as the `~cmd2.Cmd.statement_parser`{.interpreted-text role="data"} attribute. Here's a simple example which shows the proper technique: + + def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: + if not '|' in params.statement.raw: + newinput = params.statement.raw + ' | less' + params.statement = self.statement_parser.parse(newinput) + return params + +If a postparsing hook returns a `~cmd2.plugin.PostparsingData`{.interpreted-text role="class"} object with the `~cmd2.plugin.PostparsingData.stop`{.interpreted-text role="data"} attribute set to `True`: + +- no more hooks of any kind (except `features/hooks:Command Finalization Hooks`{.interpreted-text role="ref"}) will be called +- the command will not be executed +- no error message will be displayed to the user +- the application will exit + +## Precommand Hooks + +Precommand hooks can modify the user input, but cannot request the application terminate. If your hook needs to be able to exit the application, you should implement it as a postparsing hook. + +Once output is redirected and the timer started, all the hooks registered with `~cmd2.Cmd.register_precmd_hook`{.interpreted-text role="meth"} are called. Here's how to do it: + + class App(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_precmd_hook(self.myhookmethod) + + def myhookmethod(self, data: cmd2.plugin.PrecommandData) -> cmd2.plugin.PrecommandData: + # the statement object created from the user input + # is available as data.statement + return data + +`~cmd2.Cmd.register_precmd_hook`{.interpreted-text role="meth"} checks the method signature of the passed callable, and raises a `TypeError` if it has the wrong number of parameters. It will also raise a `TypeError` if the parameters and return value are not annotated as `PrecommandData`. + +You may choose to modify the user input by creating a new `~cmd2.Statement`{.interpreted-text role="class"} with different properties (see above). If you do so, assign your new `~cmd2.Statement`{.interpreted-text role="class"} object to `data.statement`. + +The precommand hook must return a `~cmd2.plugin.PrecommandData`{.interpreted-text role="class"} object. You don't have to create this object from scratch, you can just return the one passed into the hook. + +After all registered precommand hooks have been called, `~cmd2.Cmd.precmd`{.interpreted-text role="meth"} will be called. To retain full backward compatibility with `cmd.Cmd`, this method is passed a `~cmd2.Statement`{.interpreted-text role="class"}, not a `~cmd2.plugin.PrecommandData`{.interpreted-text role="class"} object. + +## Postcommand Hooks + +Once the command method has returned (i.e. the `do_command(self, statement) method` has been called and returns, all postcommand hooks are called. If output was redirected by the user, it is still redirected, and the command timer is still running. + +Here's how to define and register a postcommand hook: + + class App(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_postcmd_hook(self.myhookmethod) + + def myhookmethod(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.PostcommandData: + return data + +Your hook will be passed a `~cmd2.plugin.PostcommandData`{.interpreted-text role="class"} object, which has a `~cmd2.plugin.PostcommandData.statement`{.interpreted-text role="data"} attribute that describes the command which was executed. If your postcommand hook method gets called, you are guaranteed that the command method was called, and that it didn't raise an exception. + +If any postcommand hook raises an exception, the exception will be displayed to the user, and no further postcommand hook methods will be called. Command finalization hooks, if any, will be called. + +After all registered postcommand hooks have been called, `self.postcmd` will be called to retain full backward compatibility with `cmd.Cmd`. + +If any postcommand hook (registered or `self.postcmd`) returns a `~cmd2.plugin.PostcommandData`{.interpreted-text role="class"} object with the stop attribute set to `True`, subsequent postcommand hooks will still be called, as will the command finalization hooks, but once those hooks have all been called, the application will terminate. Likewise, if :`self.postcmd` returns `True`, the command finalization hooks will be called before the application terminates. + +Any postcommand hook can change the value of the `stop` attribute before returning it, and the modified value will be passed to the next postcommand hook. The value returned by the final postcommand hook will be passed to the command finalization hooks, which may further modify the value. If your hook blindly returns `False`, a prior hook's request to exit the application will not be honored. It's best to return the value you were passed unless you have a compelling reason to do otherwise. + +To purposefully and silently skip postcommand hooks, commands can raise any of of the following exceptions. + +- `cmd2.exceptions.SkipPostcommandHooks`{.interpreted-text role="attr"} +- `cmd2.exceptions.Cmd2ArgparseError`{.interpreted-text role="attr"} + +## Command Finalization Hooks + +Command finalization hooks are called even if one of the other types of hooks or the command method raise an exception. Here's how to create and register a command finalization hook: + + class App(cmd2.Cmd): + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.register_cmdfinalization_hook(self.myhookmethod) + + def myhookmethod(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData: + return data + +Command Finalization hooks must check whether the `~cmd2.plugin.CommandFinalizationData.statement`{.interpreted-text role="data"} attribute of the passed `~cmd2.plugin.CommandFinalizationData`{.interpreted-text role="class"} object contains a value. There are certain circumstances where these hooks may be called before the user input has been parsed, so you can't always rely on having a `~cmd2.plugin.CommandFinalizationData.statement`{.interpreted-text role="data"}. + +If any prior postparsing or precommand hook has requested the application to terminate, the value of the `~cmd2.plugin.CommandFinalizationData.stop`{.interpreted-text role="data"} attribute passed to the first command finalization hook will be `True`. Any command finalization hook can change the value of the `~cmd2.plugin.CommandFinalizationData.stop`{.interpreted-text role="data"} attribute before returning it, and the modified value will be passed to the next command finalization hook. The value returned by the final command finalization hook will determine whether the application terminates or not. + +This approach to command finalization hooks can be powerful, but it can also cause problems. If your hook blindly returns `False`, a prior hook's request to exit the application will not be honored. It's best to return the value you were passed unless you have a compelling reason to do otherwise. + +If any command finalization hook raises an exception, no more command finalization hooks will be called. If the last hook to return a value returned `True`, then the exception will be rendered, and the application will terminate. diff --git a/docs/features/hooks.rst b/docs/features/hooks.rst deleted file mode 100644 index 1ae2f3e4a..000000000 --- a/docs/features/hooks.rst +++ /dev/null @@ -1,340 +0,0 @@ -Hooks -===== - -The typical way of starting a ``cmd2`` application is as follows:: - - import cmd2 - class App(cmd2.Cmd): - # customized attributes and methods here - - if __name__ == '__main__': - app = App() - app.cmdloop() - -There are several pre-existing methods and attributes which you can tweak to -control the overall behavior of your application before, during, and after the -command processing loop. - - -Application Lifecycle Hooks ---------------------------- - -You can run a script on initialization by passing the script filename in the -``startup_script`` parameter of :meth:`cmd2.Cmd.__init__`. - -You can also register methods to be called at the beginning of the command -loop:: - - class App(cmd2.Cmd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_preloop_hook(self.myhookmethod) - - def myhookmethod(self) -> None: - self.poutput("before the loop begins") - -To retain backwards compatibility with ``cmd.Cmd``, after all registered -preloop hooks have been called, the :meth:`~cmd2.Cmd.preloop` method is -called. - -A similar approach allows you to register functions to be called after the -command loop has finished:: - - class App(cmd2.Cmd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_postloop_hook(self.myhookmethod) - - def myhookmethod(self) -> None: - self.poutput("after the loop ends") - -To retain backwards compatibility with ``cmd.Cmd``, after all registered -postloop hooks have been called, the :meth:`~cmd2.Cmd.postloop` method is -called. - -Preloop and postloop hook methods are not passed any parameters and any return -value is ignored. - -The approach of registering hooks instead of overriding methods allows multiple -hooks to be called before the command loop begins or ends. Plugin authors -should review :ref:`features/plugins:Hooks` for best practices writing hooks. - - -Application Lifecycle Attributes --------------------------------- - -There are numerous attributes on :class:`cmd2.Cmd` which affect application -behavior upon entering or during the command loop: - -- :data:`~cmd2.Cmd.intro` - if provided this serves as the intro banner printed - once at start of application, after :meth:`~cmd2.Cmd.preloop` is called. -- :data:`~cmd2.Cmd.prompt` - see :ref:`features/prompt:Prompt` for more - information. -- :data:`~cmd2.Cmd.continuation_prompt` - The prompt issued to solicit input - for the 2nd and subsequent lines of a - :ref:`multiline command ` -- :data:`~cmd2.Cmd.echo` - if ``True`` write the prompt and the command into - the output stream. - -In addition, several arguments to :meth:`cmd2.Cmd.__init__` also affect -the command loop behavior: - -- ``allow_cli_args`` - allows commands to be specified on the operating system - command line which are executed before the command processing loop begins. -- ``transcript_files`` - see :ref:`features/transcripts:Transcripts` for more - information -- ``startup_script`` - run a script on initialization. See - :ref:`features/scripting:Scripting` for more information. - - -Command Processing Loop ------------------------ - -When you call :meth:`cmd2.Cmd.cmdloop`, the following sequence of events are -repeated until the application exits: - -#. Output the prompt -#. Accept user input -#. Parse user input into a :class:`~cmd2.Statement` object -#. Call methods registered with :meth:`~cmd2.Cmd.register_postparsing_hook()` -#. Redirect output, if user asked for it and it's allowed -#. Start timer -#. Call methods registered with :meth:`~cmd2.Cmd.register_precmd_hook` -#. Call :meth:`~cmd2.Cmd.precmd` - for backwards compatibility with ``cmd.Cmd`` -#. Add statement to :ref:`features/history:History` -#. Call `do_command` method -#. Call methods registered with :meth:`~cmd2.Cmd.register_postcmd_hook()` -#. Call :meth:`~cmd2.Cmd.postcmd` - for backwards compatibility with - ``cmd.Cmd`` -#. Stop timer and display the elapsed time -#. Stop redirecting output if it was redirected -#. Call methods registered with - :meth:`~cmd2.Cmd.register_cmdfinalization_hook()` - -By registering hook methods, steps 4, 8, 12, and 16 allow you to run code -during, and control the flow of the command processing loop. Be aware that -plugins also utilize these hooks, so there may be code running that is not part -of your application. Methods registered for a hook are called in the order they -were registered. You can register a function more than once, and it will be -called each time it was registered. - -Postparsing, precommand, and postcommand hook methods share some common ways to -influence the command processing loop. - -If a hook raises an exception: - -- no more hooks (except command finalization hooks) of any kind will be called -- if the command has not yet been executed, it will not be executed -- the exception message will be displayed for the user. - -Specific types of hook methods have additional options as described below. - - -Postparsing Hooks ------------------ - -Postparsing hooks are called after the user input has been parsed but before -execution of the command. These hooks can be used to: - -- modify the user input -- run code before every command executes -- cancel execution of the current command -- exit the application - -When postparsing hooks are called, output has not been redirected, nor has the -timer for command execution been started. - -To define and register a postparsing hook, do the following:: - - class App(cmd2.Cmd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_postparsing_hook(self.myhookmethod) - - def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - # the statement object created from the user input - # is available as params.statement - return params - -:meth:`~cmd2.Cmd.register_postparsing_hook` checks the method signature of the -passed callable, and raises a ``TypeError`` if it has the wrong number of -parameters. It will also raise a ``TypeError`` if the passed parameter and -return value are not annotated as ``PostparsingData``. - -The hook method will be passed one parameter, a -:class:`~cmd2.plugin.PostparsingData` object which we will refer to as -``params``. ``params`` contains two attributes. ``params.statement`` is a -:class:`~cmd2.Statement` object which describes the parsed user input. -There are many useful attributes in the :class:`~cmd2.Statement` -object, including ``.raw`` which contains exactly what the user typed. -``params.stop`` is set to ``False`` by default. - -The hook method must return a :class:`cmd2.plugin.PostparsingData` object, and -it is very convenient to just return the object passed into the hook method. -The hook method may modify the attributes of the object to influence the -behavior of the application. If ``params.stop`` is set to true, a fatal failure -is triggered prior to execution of the command, and the application exits. - -To modify the user input, you create a new :class:`~cmd2.Statement` object and -return it in ``params.statement``. Don't try and directly modify the contents -of a :class:`~cmd2.Statement` object, there be dragons. Instead, use the -various attributes in a :class:`~cmd2.Statement` object to construct a new -string, and then parse that string to create a new :class:`~cmd2.Statement` -object. - -:class:`cmd2.Cmd` uses an instance of :class:`~cmd2.parsing.StatementParser` to -parse user input. This instance has been configured with the proper command -terminators, multiline commands, and other parsing related settings. This -instance is available as the :data:`~cmd2.Cmd.statement_parser` attribute. -Here's a simple example which shows the proper technique:: - - def myhookmethod(self, params: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - if not '|' in params.statement.raw: - newinput = params.statement.raw + ' | less' - params.statement = self.statement_parser.parse(newinput) - return params - -If a postparsing hook returns a :class:`~cmd2.plugin.PostparsingData` object -with the :data:`~cmd2.plugin.PostparsingData.stop` attribute set to ``True``: - -- no more hooks of any kind (except - :ref:`features/hooks:Command Finalization Hooks`) will be called -- the command will not be executed -- no error message will be displayed to the user -- the application will exit - - -Precommand Hooks ----------------- - -Precommand hooks can modify the user input, but cannot request the application -terminate. If your hook needs to be able to exit the application, you should -implement it as a postparsing hook. - -Once output is redirected and the timer started, all the hooks registered with -:meth:`~cmd2.Cmd.register_precmd_hook` are called. Here's how to do it:: - - class App(cmd2.Cmd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_precmd_hook(self.myhookmethod) - - def myhookmethod(self, data: cmd2.plugin.PrecommandData) -> cmd2.plugin.PrecommandData: - # the statement object created from the user input - # is available as data.statement - return data - -:meth:`~cmd2.Cmd.register_precmd_hook` checks the method signature of the -passed callable, and raises a ``TypeError`` if it has the wrong number of -parameters. It will also raise a ``TypeError`` if the parameters and return -value are not annotated as ``PrecommandData``. - -You may choose to modify the user input by creating a new -:class:`~cmd2.Statement` with different properties (see above). If you do so, -assign your new :class:`~cmd2.Statement` object to ``data.statement``. - -The precommand hook must return a :class:`~cmd2.plugin.PrecommandData` object. -You don't have to create this object from scratch, you can just return the one -passed into the hook. - -After all registered precommand hooks have been called, -:meth:`~cmd2.Cmd.precmd` will be called. To retain full backward compatibility -with ``cmd.Cmd``, this method is passed a :class:`~cmd2.Statement`, not a -:class:`~cmd2.plugin.PrecommandData` object. - - -Postcommand Hooks ------------------ - -Once the command method has returned (i.e. the ``do_command(self, statement) -method`` has been called and returns, all postcommand hooks are called. If -output was redirected by the user, it is still redirected, and the command -timer is still running. - -Here's how to define and register a postcommand hook:: - - class App(cmd2.Cmd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_postcmd_hook(self.myhookmethod) - - def myhookmethod(self, data: cmd2.plugin.PostcommandData) -> cmd2.plugin.PostcommandData: - return data - -Your hook will be passed a :class:`~cmd2.plugin.PostcommandData` object, which -has a :data:`~cmd2.plugin.PostcommandData.statement` attribute that describes -the command which was executed. If your postcommand hook method gets called, -you are guaranteed that the command method was called, and that it didn't raise -an exception. - -If any postcommand hook raises an exception, the exception will be displayed to -the user, and no further postcommand hook methods will be called. Command -finalization hooks, if any, will be called. - -After all registered postcommand hooks have been called, -``self.postcmd`` will be called to retain full backward compatibility -with ``cmd.Cmd``. - -If any postcommand hook (registered or ``self.postcmd``) returns a -:class:`~cmd2.plugin.PostcommandData` object with the stop attribute set to -``True``, subsequent postcommand hooks will still be called, as will the -command finalization hooks, but once those hooks have all been called, the -application will terminate. Likewise, if :``self.postcmd`` returns -``True``, the command finalization hooks will be called before the application -terminates. - -Any postcommand hook can change the value of the ``stop`` attribute before -returning it, and the modified value will be passed to the next postcommand -hook. The value returned by the final postcommand hook will be passed to the -command finalization hooks, which may further modify the value. If your hook -blindly returns ``False``, a prior hook's request to exit the application will -not be honored. It's best to return the value you were passed unless you have a -compelling reason to do otherwise. - -To purposefully and silently skip postcommand hooks, commands can raise any of -of the following exceptions. - -- :attr:`cmd2.exceptions.SkipPostcommandHooks` -- :attr:`cmd2.exceptions.Cmd2ArgparseError` - - -Command Finalization Hooks --------------------------- - -Command finalization hooks are called even if one of the other types of hooks -or the command method raise an exception. Here's how to create and register a -command finalization hook:: - - class App(cmd2.Cmd): - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.register_cmdfinalization_hook(self.myhookmethod) - - def myhookmethod(self, data: cmd2.plugin.CommandFinalizationData) -> cmd2.plugin.CommandFinalizationData: - return data - -Command Finalization hooks must check whether the -:data:`~cmd2.plugin.CommandFinalizationData.statement` attribute of the passed -:class:`~cmd2.plugin.CommandFinalizationData` object contains a value. There -are certain circumstances where these hooks may be called before the user input -has been parsed, so you can't always rely on having a -:data:`~cmd2.plugin.CommandFinalizationData.statement`. - -If any prior postparsing or precommand hook has requested the application to -terminate, the value of the :data:`~cmd2.plugin.CommandFinalizationData.stop` -attribute passed to the first command finalization hook will be ``True``. Any -command finalization hook can change the value of the -:data:`~cmd2.plugin.CommandFinalizationData.stop` attribute before returning -it, and the modified value will be passed to the next command finalization -hook. The value returned by the final command finalization hook will determine -whether the application terminates or not. - -This approach to command finalization hooks can be powerful, but it can also -cause problems. If your hook blindly returns ``False``, a prior hook's request -to exit the application will not be honored. It's best to return the value you -were passed unless you have a compelling reason to do otherwise. - -If any command finalization hook raises an exception, no more command -finalization hooks will be called. If the last hook to return a value returned -``True``, then the exception will be rendered, and the application will -terminate. diff --git a/docs/features/index.md b/docs/features/index.md new file mode 100644 index 000000000..13f99715b --- /dev/null +++ b/docs/features/index.md @@ -0,0 +1,32 @@ +# Features + +
+ +- [Argument Processing](argument_processing.md) +- [Builtin Commands](builtin_commands.md) +- [Clipboard Integration](clipboard.md) +- [Commands](commands.md) +- [Completion](completion.md) +- [Disabling Commands](disable_commands.md) +- [Embedded Python Shells](embedded_python_shells.md) +- [Generating Output](generating_output.md) +- [Help](help.md) +- [History](history.md) +- [Hooks](hooks.md) +- [Initialization](initialization.md) +- [Miscellaneous Features](misc.md) +- [Modular Commands](modular_commands.md) +- [Multiline Commands](multiline_commands.md) +- [Integrating with the OS](os.md) +- [Packaging a cmd2 application for distribution](packaging.md) +- [Plugins](plugins.md) +- [Prompt](prompt.md) +- [Output Redirection and Pipes](redirection.md) +- [Scripting](scripting.md) +- [Settings](settings.md) +- [Shortcuts, Aliases, and Macros](shortcuts_aliases_macros.md) +- [Startup Commands](startup_commands.md) +- [Table Creation](table_creation.md) +- [Transcripts](transcripts.md) + +
diff --git a/docs/features/index.rst b/docs/features/index.rst deleted file mode 100644 index 48590b6ad..000000000 --- a/docs/features/index.rst +++ /dev/null @@ -1,32 +0,0 @@ -Features -======== - -.. toctree:: - :maxdepth: 1 - - argument_processing - builtin_commands - clipboard - commands - completion - disable_commands - embedded_python_shells - generating_output - help - history - hooks - initialization - misc - modular_commands - multiline_commands - os - packaging - plugins - prompt - redirection - scripting - settings - shortcuts_aliases_macros - startup_commands - table_creation - transcripts diff --git a/docs/features/initialization.md b/docs/features/initialization.md new file mode 100644 index 000000000..bb1da9c9e --- /dev/null +++ b/docs/features/initialization.md @@ -0,0 +1,120 @@ +# Initialization + +Here is a basic example `cmd2` application which demonstrates many capabilities which you may wish to utilize while initializing the app: + + #!/usr/bin/env python3 + # coding=utf-8 + """A simple example cmd2 application demonstrating the following: + 1) Colorizing/stylizing output + 2) Using multiline commands + 3) Persistent history + 4) How to run an initialization script at startup + 5) How to group and categorize commands when displaying them in help + 6) Opting-in to using the ipy command to run an IPython shell + 7) Allowing access to your application in py and ipy + 8) Displaying an intro banner upon starting your application + 9) Using a custom prompt + 10) How to make custom attributes settable at runtime + """ + import cmd2 + from cmd2 import ( + Bg, + Fg, + style, + ) + + + class BasicApp(cmd2.Cmd): + CUSTOM_CATEGORY = 'My Custom Commands' + + def __init__(self): + super().__init__( + multiline_commands=['echo'], + persistent_history_file='cmd2_history.dat', + startup_script='scripts/startup.txt', + include_ipy=True, + ) + + # Prints an intro banner once upon application startup + self.intro = style('Welcome to cmd2!', fg=Fg.RED, bg=Bg.WHITE, bold=True) + + # Show this as the prompt when asking for input + self.prompt = 'myapp> ' + + # Used as prompt for multiline commands after the first line + self.continuation_prompt = '... ' + + # Allow access to your application in py and ipy via self + self.self_in_py = True + + # Set the default category name + self.default_category = 'cmd2 Built-in Commands' + + # Color to output text in with echo command + self.foreground_color = Fg.CYAN.name.lower() + + # Make echo_fg settable at runtime + fg_colors = [c.name.lower() for c in Fg] + self.add_settable( + cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command', self, choices=fg_colors) + ) + + @cmd2.with_category(CUSTOM_CATEGORY) + def do_intro(self, _): + """Display the intro banner""" + self.poutput(self.intro) + + @cmd2.with_category(CUSTOM_CATEGORY) + def do_echo(self, arg): + """Example of a multiline command""" + fg_color = Fg[self.foreground_color.upper()] + self.poutput(style(arg, fg=fg_color)) + + + if __name__ == '__main__': + app = BasicApp() + app.cmdloop() + +## Cmd class initializer + +A `cmd2.Cmd` instance or subclass instance is an interactive CLI application framework. There is no good reason to instantiate `Cmd` itself; rather, it's useful as a superclass of a class you define yourself in order to inherit `Cmd`'s methods and encapsulate action methods. + +Certain things must be initialized within the `__init__()` method of your class derived from `cmd2.Cmd`(all arguments to `__init__()` are optional): + +## Cmd instance attributes + +The `cmd2.Cmd` class provides a large number of public instance attributes which allow developers to customize a `cmd2` application further beyond the options provided by the `__init__()` method. + +### Public instance attributes + +Here are instance attributes of `cmd2.Cmd` which developers might wish override: + +- **always_show_hint**: if `True`, display tab completion hint even when completion suggestions print (Default: `False`) +- **broken_pipe_warning**: if non-empty, this string will be displayed if a broken pipe error occurs +- **continuation_prompt**: used for multiline commands on 2nd+ line of input +- **debug**: if `True`, show full stack trace on error (Default: `False`) +- **default_category**: if any command has been categorized, then all other commands that haven't been categorized will display under this section in the help output. +- **default_error**: the error that prints when a non-existent command is run +- **default_sort_key**: the default key for sorting string results. Its default value performs a case-insensitive alphabetical sort. +- **default_to_shell**: if `True`, attempt to run unrecognized commands as shell commands (Default: `False`) +- **disabled_commands**: commands that have been disabled from use. This is to support commands that are only available during specific states of the application. This dictionary's keys are the command names and its values are DisabledCommand objects. +- **doc_header**: Set the header used for the help function's listing of documented functions +- **echo**: if `True`, each command the user issues will be repeated to the screen before it is executed. This is particularly useful when running scripts. This behavior does not occur when running a command at the prompt. (Default: `False`) +- **editor**: text editor program to use with *edit* command (e.g. `vim`) +- **exclude_from_history**: commands to exclude from the *history* command +- **exit_code**: this determines the value returned by `cmdloop()` when exiting the application +- **feedback_to_output**: if `True`, send nonessential output to stdout, if `False` send them to stderr (Default: `False`) +- **help_error**: the error that prints when no help information can be found +- **hidden_commands**: commands to exclude from the help menu and tab completion +- **last_result**: stores results from the last command run to enable usage of results in a Python script or interactive console. Built-in commands don't make use of this. It is purely there for user-defined commands and convenience. +- **macros**: dictionary of macro names and their values +- **max_completion_items**: max number of CompletionItems to display during tab completion (Default: 50) +- **pager**: sets the pager command used by the `Cmd.ppaged()` method for displaying wrapped output using a pager +- **pager_chop**: sets the pager command used by the `Cmd.ppaged()` method for displaying chopped/truncated output using a pager +- **py_bridge_name**: name by which embedded Python environments and scripts refer to the `cmd2` application by in order to call commands (Default: `app`) +- **py_locals**: dictionary that defines specific variables/functions available in Python shells and scripts (provides more fine-grained control than making everything available with **self_in_py**) +- **quiet**: if `True`, then completely suppress nonessential output (Default: `False`) +- **scripts_add_to_history**: if `True`, scripts and pyscripts add commands to history (Default: `True`) +- **self_in_py**: if `True`, allow access to your application in *py* command via `self` (Default: `False`) +- **settable**: dictionary that controls which of these instance attributes are settable at runtime using the *set* command +- **timing**: if `True`, display execution time for each command (Default: `False`) diff --git a/docs/features/initialization.rst b/docs/features/initialization.rst deleted file mode 100644 index 3ee96cf9e..000000000 --- a/docs/features/initialization.rst +++ /dev/null @@ -1,166 +0,0 @@ -Initialization -============== - -Here is a basic example ``cmd2`` application which demonstrates many -capabilities which you may wish to utilize while initializing the app:: - - #!/usr/bin/env python3 - # coding=utf-8 - """A simple example cmd2 application demonstrating the following: - 1) Colorizing/stylizing output - 2) Using multiline commands - 3) Persistent history - 4) How to run an initialization script at startup - 5) How to group and categorize commands when displaying them in help - 6) Opting-in to using the ipy command to run an IPython shell - 7) Allowing access to your application in py and ipy - 8) Displaying an intro banner upon starting your application - 9) Using a custom prompt - 10) How to make custom attributes settable at runtime - """ - import cmd2 - from cmd2 import ( - Bg, - Fg, - style, - ) - - - class BasicApp(cmd2.Cmd): - CUSTOM_CATEGORY = 'My Custom Commands' - - def __init__(self): - super().__init__( - multiline_commands=['echo'], - persistent_history_file='cmd2_history.dat', - startup_script='scripts/startup.txt', - include_ipy=True, - ) - - # Prints an intro banner once upon application startup - self.intro = style('Welcome to cmd2!', fg=Fg.RED, bg=Bg.WHITE, bold=True) - - # Show this as the prompt when asking for input - self.prompt = 'myapp> ' - - # Used as prompt for multiline commands after the first line - self.continuation_prompt = '... ' - - # Allow access to your application in py and ipy via self - self.self_in_py = True - - # Set the default category name - self.default_category = 'cmd2 Built-in Commands' - - # Color to output text in with echo command - self.foreground_color = Fg.CYAN.name.lower() - - # Make echo_fg settable at runtime - fg_colors = [c.name.lower() for c in Fg] - self.add_settable( - cmd2.Settable('foreground_color', str, 'Foreground color to use with echo command', self, choices=fg_colors) - ) - - @cmd2.with_category(CUSTOM_CATEGORY) - def do_intro(self, _): - """Display the intro banner""" - self.poutput(self.intro) - - @cmd2.with_category(CUSTOM_CATEGORY) - def do_echo(self, arg): - """Example of a multiline command""" - fg_color = Fg[self.foreground_color.upper()] - self.poutput(style(arg, fg=fg_color)) - - - if __name__ == '__main__': - app = BasicApp() - app.cmdloop() - - -Cmd class initializer ---------------------- - -A ``cmd2.Cmd`` instance or subclass instance is an interactive CLI application -framework. There is no good reason to instantiate ``Cmd`` itself; rather, it’s -useful as a superclass of a class you define yourself in order to inherit -``Cmd``’s methods and encapsulate action methods. - -Certain things must be initialized within the ``__init__()`` method of your -class derived from ``cmd2.Cmd``(all arguments to ``__init__()`` are optional): - -.. automethod:: cmd2.Cmd.__init__ - :noindex: - -Cmd instance attributes ------------------------ - -The ``cmd2.Cmd`` class provides a large number of public instance attributes -which allow developers to customize a ``cmd2`` application further beyond the -options provided by the ``__init__()`` method. - -Public instance attributes -~~~~~~~~~~~~~~~~~~~~~~~~~~ -Here are instance attributes of ``cmd2.Cmd`` which developers might wish -override: - -- **always_show_hint**: if ``True``, display tab completion hint even when - completion suggestions print (Default: ``False``) -- **broken_pipe_warning**: if non-empty, this string will be displayed if a - broken pipe error occurs -- **continuation_prompt**: used for multiline commands on 2nd+ line of input -- **debug**: if ``True``, show full stack trace on error (Default: ``False``) -- **default_category**: if any command has been categorized, then all other - commands that haven't been categorized will display under this section in the - help output. -- **default_error**: the error that prints when a non-existent command is run -- **default_sort_key**: the default key for sorting string results. Its default - value performs a case-insensitive alphabetical sort. -- **default_to_shell**: if ``True``, attempt to run unrecognized commands as - shell commands (Default: ``False``) -- **disabled_commands**: commands that have been disabled from use. This is to - support commands that are only available during specific states of the - application. This dictionary's keys are the command names and its values are - DisabledCommand objects. -- **doc_header**: Set the header used for the help function's listing of - documented functions -- **echo**: if ``True``, each command the user issues will be repeated to the - screen before it is executed. This is particularly useful when running - scripts. This behavior does not occur when running a command at the prompt. - (Default: ``False``) -- **editor**: text editor program to use with *edit* command (e.g. ``vim``) -- **exclude_from_history**: commands to exclude from the *history* command -- **exit_code**: this determines the value returned by ``cmdloop()`` when - exiting the application -- **feedback_to_output**: if ``True``, send nonessential output to stdout, if - ``False`` send them to stderr (Default: ``False``) -- **help_error**: the error that prints when no help information can be found -- **hidden_commands**: commands to exclude from the help menu and tab - completion -- **last_result**: stores results from the last command run to enable usage - of results in a Python script or interactive console. Built-in commands don't - make use of this. It is purely there for user-defined commands and - convenience. -- **macros**: dictionary of macro names and their values -- **max_completion_items**: max number of CompletionItems to display during - tab completion (Default: 50) -- **pager**: sets the pager command used by the ``Cmd.ppaged()`` method for - displaying wrapped output using a pager -- **pager_chop**: sets the pager command used by the ``Cmd.ppaged()`` method - for displaying chopped/truncated output using a pager -- **py_bridge_name**: name by which embedded Python environments and scripts - refer to the ``cmd2`` application by in order to call commands (Default: - ``app``) -- **py_locals**: dictionary that defines specific variables/functions available - in Python shells and scripts (provides more fine-grained control than making - everything available with **self_in_py**) -- **quiet**: if ``True``, then completely suppress nonessential output (Default: - ``False``) -- **scripts_add_to_history**: if ``True``, scripts and pyscripts add commands to - history (Default: ``True``) -- **self_in_py**: if ``True``, allow access to your application in *py* - command via ``self`` (Default: ``False``) -- **settable**: dictionary that controls which of these instance attributes - are settable at runtime using the *set* command -- **timing**: if ``True``, display execution time for each command (Default: - ``False``) diff --git a/docs/features/misc.md b/docs/features/misc.md new file mode 100644 index 000000000..b30735e20 --- /dev/null +++ b/docs/features/misc.md @@ -0,0 +1,73 @@ +# Miscellaneous Features + +## Timer {: #Timer } + +Turn the timer setting on, and `cmd2` will show the wall time it takes for each command to execute. + +## Exiting + +Mention quit, and EOF handling built into `cmd2`. + +## select + +Presents numbered options to user, as bash `select`. + +`app.select` is called from within a method (not by the user directly; it is `app.select`, not `app.do_select`). + +TODO replace with mkdocstrings: + + .. automethod:: cmd2.Cmd.select + :noindex: + + def do_eat(self, arg): + sauce = self.select('sweet salty', 'Sauce? ') + result = '{food} with {sauce} sauce, yum!' + result = result.format(food=arg, sauce=sauce) + self.stdout.write(result + '\n') + + (Cmd) eat wheaties + 1. sweet + 2. salty + Sauce? 2 + wheaties with salty sauce, yum! + +## Disabling Commands + +`cmd2` supports disabling commands during runtime. This is useful if certain commands should only be available when the application is in a specific state. When a command is disabled, it will not show up in the help menu or tab complete. If a user tries to run the command, a command-specific message supplied by the developer will be printed. The following functions support this feature. + +enable[command]{#command}() + +: Enable an individual command + +enable[category]{#category}() + +: Enable an entire category of commands + +disable[command]{#command}() + +: Disable an individual command and set the message that will print when this command is run or help is called on it while disabled + +disable[category]{#category}() + +: Disable an entire category of commands and set the message that will print when anything in this category is run or help is called on it while disabled + +See the definitions of these functions for descriptions of their arguments. + +See the `do_enable_commands()` and `do_disable_commands()` functions in the [HelpCategories](https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py) example for a demonstration. + +## Default to shell + +Every `cmd2` application can execute operating-system level (shell) commands with `shell` or a `!` shortcut: + + (Cmd) shell which python + /usr/bin/python + (Cmd) !which python + /usr/bin/python + +However, if the parameter `default_to_shell` is `True`, then _every_ command will be attempted on the operating system. Only if that attempt fails (i.e., produces a nonzero return value) will the application's own `default` method be called. + + (Cmd) which python + /usr/bin/python + (Cmd) my dog has fleas + sh: my: not found + *** Unknown syntax: my dog has fleas diff --git a/docs/features/misc.rst b/docs/features/misc.rst deleted file mode 100644 index c2f14b5be..000000000 --- a/docs/features/misc.rst +++ /dev/null @@ -1,101 +0,0 @@ -Miscellaneous Features -====================== - - -Timer ------ - -Turn the timer setting on, and ``cmd2`` will show the wall time it takes for -each command to execute. - - -Exiting -------- - -Mention quit, and EOF handling built into ``cmd2``. - - -select ------- - -Presents numbered options to user, as bash ``select``. - -``app.select`` is called from within a method (not by the user directly; it is -``app.select``, not ``app.do_select``). - -.. automethod:: cmd2.Cmd.select - :noindex: - -:: - - def do_eat(self, arg): - sauce = self.select('sweet salty', 'Sauce? ') - result = '{food} with {sauce} sauce, yum!' - result = result.format(food=arg, sauce=sauce) - self.stdout.write(result + '\n') - -:: - - (Cmd) eat wheaties - 1. sweet - 2. salty - Sauce? 2 - wheaties with salty sauce, yum! - - -Disabling Commands ------------------- - -``cmd2`` supports disabling commands during runtime. This is useful if certain -commands should only be available when the application is in a specific state. -When a command is disabled, it will not show up in the help menu or tab -complete. If a user tries to run the command, a command-specific message -supplied by the developer will be printed. The following functions support this -feature. - -enable_command() - Enable an individual command - -enable_category() - Enable an entire category of commands - -disable_command() - Disable an individual command and set the message that will print when this - command is run or help is called on it while disabled - -disable_category() - Disable an entire category of commands and set the message that will print - when anything in this category is run or help is called on it while - disabled - -See the definitions of these functions for descriptions of their arguments. - -See the ``do_enable_commands()`` and ``do_disable_commands()`` functions in the -HelpCategories_ example for a demonstration. - -.. _HelpCategories: https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py - - -Default to shell ----------------- - -Every ``cmd2`` application can execute operating-system level (shell) commands -with ``shell`` or a ``!`` shortcut:: - - (Cmd) shell which python - /usr/bin/python - (Cmd) !which python - /usr/bin/python - -However, if the parameter ``default_to_shell`` is ``True``, then *every* -command will be attempted on the operating system. Only if that attempt fails -(i.e., produces a nonzero return value) will the application's own ``default`` -method be called. - -:: - - (Cmd) which python - /usr/bin/python - (Cmd) my dog has fleas - sh: my: not found - *** Unknown syntax: my dog has fleas diff --git a/docs/features/modular_commands.md b/docs/features/modular_commands.md new file mode 100644 index 000000000..d8b694c93 --- /dev/null +++ b/docs/features/modular_commands.md @@ -0,0 +1,305 @@ +# Modular Commands + +## Overview + +Cmd2 also enables developers to modularize their command definitions into `CommandSet` objects. CommandSets represent a logical grouping of commands within an cmd2 application. By default, all CommandSets will be discovered and loaded automatically when the cmd2.Cmd class is instantiated with this mixin. This also enables the developer to dynamically add/remove commands from the cmd2 application. This could be useful for loadable plugins that add additional capabilities. Additionally, it allows for object-oriented encapsulation and garbage collection of state that is specific to a CommandSet. + +### Features + +- Modular Command Sets - Commands can be broken into separate modules rather than in one god class holding all commands. +- Automatic Command Discovery - In your application, merely defining and importing a CommandSet is sufficient for cmd2 to discover and load your command. No manual registration is necessary. +- Dynamically Loadable/Unloadable Commands - Command functions and CommandSets can both be loaded and unloaded dynamically during application execution. This can enable features such as dynamically loaded modules that add additional commands. +- Events handlers - Four event handlers are provided in `CommandSet` class for custom initialization and cleanup steps. See `features/modular_commands:Event Handlers`{.interpreted-text role="ref"}. +- Subcommand Injection - Subcommands can be defined separately from the base command. This allows for a more action-centric instead of object-centric command system while still organizing your code and handlers around the objects being managed. + +See API documentation for `cmd2.command_definition.CommandSet`{.interpreted-text role="attr"} + +See [the examples]() for more details. + +## Defining Commands + +### Command Sets + +CommandSets group multiple commands together. The plugin will inspect functions within a `CommandSet` using the same rules as when they're defined in `cmd2.Cmd`. Commands must be prefixed with `do_`, help functions with `help_`, and completer functions with `complete_`. + +A new decorator `with_default_category` is provided to categorize all commands within a CommandSet in the same command category. Individual commands in a CommandSet may be override the default category by specifying a specific category with `cmd2.with_category`. + +CommandSet command methods will always expect the same parameters as when defined in a `cmd2.Cmd` sub-class, except that `self` will now refer to the `CommandSet` instead of the cmd2 instance. The cmd2 instance can be accessed through `self._cmd` that is populated when the `CommandSet` is registered. + +CommandSets will only be auto-loaded if the constructor takes no arguments. If you need to provide constructor arguments, see `features/modular_commands:Manual CommandSet Construction`{.interpreted-text role="ref"} + +``` python +import cmd2 +from cmd2 import CommandSet, with_default_category + +@with_default_category('My Category') +class AutoLoadCommandSet(CommandSet): + def __init__(self): + super().__init__() + + def do_hello(self, _: cmd2.Statement): + self._cmd.poutput('Hello') + + def do_world(self, _: cmd2.Statement): + self._cmd.poutput('World') + +class ExampleApp(cmd2.Cmd): + """ + CommandSets are automatically loaded. Nothing needs to be done. + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + def do_something(self, arg): + self.poutput('this is the something command') +``` + +### Manual CommandSet Construction + +If a CommandSet class requires parameters to be provided to the constructor, you man manually construct CommandSets and pass in the constructor to Cmd2. + +``` python +import cmd2 +from cmd2 import CommandSet, with_default_category + +@with_default_category('My Category') +class CustomInitCommandSet(CommandSet): + def __init__(self, arg1, arg2): + super().__init__() + + self._arg1 = arg1 + self._arg2 = arg2 + + def do_show_arg1(self, _: cmd2.Statement): + self._cmd.poutput(f'Arg1: {self._arg1}') + + def do_show_arg2(self, _: cmd2.Statement): + self._cmd.poutput(f'Arg2: {self._arg2}') + +class ExampleApp(cmd2.Cmd): + """ + CommandSets with constructor parameters are provided in the constructor + """ + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, **kwargs) + + def do_something(self, arg): + self.last_result = 5 + self.poutput('this is the something command') + + +def main(): + my_commands = CustomInitCommandSet(1, 2) + app = ExampleApp(command_sets=[my_commands]) + app.cmdloop() +``` + +### Dynamic Commands + +You can also dynamically load and unload commands by installing and removing CommandSets at runtime. For example, if you could support runtime loadable plugins or add/remove commands based on your state. + +You may need to disable command auto-loading if you need dynamically load commands at runtime. + +``` python +import argparse +import cmd2 +from cmd2 import CommandSet, with_argparser, with_category, with_default_category + + +@with_default_category('Fruits') +class LoadableFruits(CommandSet): + def __init__(self): + super().__init__() + + def do_apple(self, _: cmd2.Statement): + self._cmd.poutput('Apple') + + def do_banana(self, _: cmd2.Statement): + self._cmd.poutput('Banana') + + +@with_default_category('Vegetables') +class LoadableVegetables(CommandSet): + def __init__(self): + super().__init__() + + def do_arugula(self, _: cmd2.Statement): + self._cmd.poutput('Arugula') + + def do_bokchoy(self, _: cmd2.Statement): + self._cmd.poutput('Bok Choy') + + +class ExampleApp(cmd2.Cmd): + """ + CommandSets are loaded via the `load` and `unload` commands + """ + + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, auto_load_commands=False, **kwargs) + + self._fruits = LoadableFruits() + self._vegetables = LoadableVegetables() + + load_parser = cmd2.Cmd2ArgumentParser() + load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) + + @with_argparser(load_parser) + @with_category('Command Loading') + def do_load(self, ns: argparse.Namespace): + if ns.cmds == 'fruits': + try: + self.register_command_set(self._fruits) + self.poutput('Fruits loaded') + except ValueError: + self.poutput('Fruits already loaded') + + if ns.cmds == 'vegetables': + try: + self.register_command_set(self._vegetables) + self.poutput('Vegetables loaded') + except ValueError: + self.poutput('Vegetables already loaded') + + @with_argparser(load_parser) + def do_unload(self, ns: argparse.Namespace): + if ns.cmds == 'fruits': + self.unregister_command_set(self._fruits) + self.poutput('Fruits unloaded') + + if ns.cmds == 'vegetables': + self.unregister_command_set(self._vegetables) + self.poutput('Vegetables unloaded') + + +if __name__ == '__main__': + app = ExampleApp() + app.cmdloop() +``` + +## Event Handlers + +The following functions are called at different points in the `CommandSet` life cycle. + +`on_register(self, cmd) -> None` - Called by cmd2.Cmd as the first step to registering a CommandSet. The commands defined in this class have not be added to the CLI object at this point. Subclasses can override this to perform any initialization requiring access to the Cmd object (e.g. configure commands and their parsers based on CLI state data). + +`on_registered(self) -> None` - Called by cmd2.Cmd after a CommandSet is registered and all its commands have been added to the CLI. Subclasses can override this to perform custom steps related to the newly added commands (e.g. setting them to a disabled state). + +`on_unregister(self) -> None` - Called by `cmd2.Cmd` as the first step to unregistering a CommandSet. Subclasses can override this to perform any cleanup steps which require their commands being registered in the CLI. + +`on_unregistered(self) -> None` - Called by `cmd2.Cmd` after a CommandSet has been unregistered and all its commands removed from the CLI. Subclasses can override this to perform remaining cleanup steps. + +## Injecting Subcommands + +### Description + +Using the ``with_argparse`` decorator, it is possible to define subcommands for your command. This has a tendency to either drive your interface into an object-centric interface. For example, imagine you have a tool that manages your media collection and you want to manage movies or shows. An object-centric approach would push you to have base commands such as ``movies`` and ``shows`` which each have subcommands ``add``, ``edit``, ``list``, ``delete``. If you wanted to present an action-centric command set, so that ``add``, ``edit``, ``list``, and ``delete`` are the base commands, you'd have to organize your code around these similar actions rather than organizing your code around similar objects being managed. + +Subcommand injection allows you to inject subcommands into a base command to present an interface that is sensible to a user while still organizing your code in whatever structure make more logical sense to the developer. + +### Example + +This example is a variation on the Dynamic Commands example above. A ``cut`` command is introduced as a base command and each CommandSet + +``` python +import argparse +import cmd2 +from cmd2 import CommandSet, with_argparser, with_category, with_default_category + + +@with_default_category('Fruits') +class LoadableFruits(CommandSet): + def __init__(self): + super().__init__() + + def do_apple(self, _: cmd2.Statement): + self._cmd.poutput('Apple') + + banana_parser = cmd2.Cmd2ArgumentParser() + banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) + + @cmd2.as_subcommand_to('cut', 'banana', banana_parser) + def cut_banana(self, ns: argparse.Namespace): + """Cut banana""" + self._cmd.poutput('cutting banana: ' + ns.direction) + + +@with_default_category('Vegetables') +class LoadableVegetables(CommandSet): + def __init__(self): + super().__init__() + + def do_arugula(self, _: cmd2.Statement): + self._cmd.poutput('Arugula') + + bokchoy_parser = cmd2.Cmd2ArgumentParser() + bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) + + @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) + def cut_bokchoy(self, _: argparse.Namespace): + self._cmd.poutput('Bok Choy') + + +class ExampleApp(cmd2.Cmd): + """ + CommandSets are automatically loaded. Nothing needs to be done. + """ + + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, auto_load_commands=False, **kwargs) + + self._fruits = LoadableFruits() + self._vegetables = LoadableVegetables() + + load_parser = cmd2.Cmd2ArgumentParser() + load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) + + @with_argparser(load_parser) + @with_category('Command Loading') + def do_load(self, ns: argparse.Namespace): + if ns.cmds == 'fruits': + try: + self.register_command_set(self._fruits) + self.poutput('Fruits loaded') + except ValueError: + self.poutput('Fruits already loaded') + + if ns.cmds == 'vegetables': + try: + self.register_command_set(self._vegetables) + self.poutput('Vegetables loaded') + except ValueError: + self.poutput('Vegetables already loaded') + + @with_argparser(load_parser) + def do_unload(self, ns: argparse.Namespace): + if ns.cmds == 'fruits': + self.unregister_command_set(self._fruits) + self.poutput('Fruits unloaded') + + if ns.cmds == 'vegetables': + self.unregister_command_set(self._vegetables) + self.poutput('Vegetables unloaded') + + cut_parser = cmd2.Cmd2ArgumentParser() + cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut') + + @with_argparser(cut_parser) + def do_cut(self, ns: argparse.Namespace): + handler = ns.cmd2_handler.get() + if handler is not None: + # Call whatever subcommand function was selected + handler(ns) + else: + # No subcommand was provided, so call help + self.poutput('This command does nothing without sub-parsers registered') + self.do_help('cut') + + +if __name__ == '__main__': + app = ExampleApp() + app.cmdloop() +``` diff --git a/docs/features/modular_commands.rst b/docs/features/modular_commands.rst deleted file mode 100644 index b78e151c3..000000000 --- a/docs/features/modular_commands.rst +++ /dev/null @@ -1,359 +0,0 @@ -Modular Commands -================ - -Overview --------- - -Cmd2 also enables developers to modularize their command definitions into ``CommandSet`` objects. CommandSets represent -a logical grouping of commands within an cmd2 application. By default, all CommandSets will be discovered and loaded -automatically when the cmd2.Cmd class is instantiated with this mixin. This also enables the developer to -dynamically add/remove commands from the cmd2 application. This could be useful for loadable plugins that -add additional capabilities. Additionally, it allows for object-oriented encapsulation and garbage collection of state -that is specific to a CommandSet. - -Features -~~~~~~~~ - -* Modular Command Sets - Commands can be broken into separate modules rather than in one god class holding all - commands. -* Automatic Command Discovery - In your application, merely defining and importing a CommandSet is sufficient for - cmd2 to discover and load your command. No manual registration is necessary. -* Dynamically Loadable/Unloadable Commands - Command functions and CommandSets can both be loaded and unloaded - dynamically during application execution. This can enable features such as dynamically loaded modules that - add additional commands. -* Events handlers - Four event handlers are provided in ``CommandSet`` class for custom initialization - and cleanup steps. See :ref:`features/modular_commands:Event Handlers`. -* Subcommand Injection - Subcommands can be defined separately from the base command. This allows for a more - action-centric instead of object-centric command system while still organizing your code and handlers around the - objects being managed. - -See API documentation for :attr:`cmd2.command_definition.CommandSet` - -See [the examples](https://github.com/python-cmd2/cmd2/tree/master/examples/modular_commands) for more details. - - -Defining Commands ------------------ - -Command Sets -~~~~~~~~~~~~~ - -CommandSets group multiple commands together. The plugin will inspect functions within a ``CommandSet`` -using the same rules as when they're defined in ``cmd2.Cmd``. Commands must be prefixed with ``do_``, help -functions with ``help_``, and completer functions with ``complete_``. - -A new decorator ``with_default_category`` is provided to categorize all commands within a CommandSet in the -same command category. Individual commands in a CommandSet may be override the default category by specifying a -specific category with ``cmd2.with_category``. - -CommandSet command methods will always expect the same parameters as when defined in a ``cmd2.Cmd`` sub-class, -except that ``self`` will now refer to the ``CommandSet`` instead of the cmd2 instance. The cmd2 instance can -be accessed through ``self._cmd`` that is populated when the ``CommandSet`` is registered. - -CommandSets will only be auto-loaded if the constructor takes no arguments. -If you need to provide constructor arguments, see :ref:`features/modular_commands:Manual CommandSet Construction` - -.. code-block:: python - - import cmd2 - from cmd2 import CommandSet, with_default_category - - @with_default_category('My Category') - class AutoLoadCommandSet(CommandSet): - def __init__(self): - super().__init__() - - def do_hello(self, _: cmd2.Statement): - self._cmd.poutput('Hello') - - def do_world(self, _: cmd2.Statement): - self._cmd.poutput('World') - - class ExampleApp(cmd2.Cmd): - """ - CommandSets are automatically loaded. Nothing needs to be done. - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - def do_something(self, arg): - self.poutput('this is the something command') - - -Manual CommandSet Construction -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -If a CommandSet class requires parameters to be provided to the constructor, you man manually construct -CommandSets and pass in the constructor to Cmd2. - -.. code-block:: python - - import cmd2 - from cmd2 import CommandSet, with_default_category - - @with_default_category('My Category') - class CustomInitCommandSet(CommandSet): - def __init__(self, arg1, arg2): - super().__init__() - - self._arg1 = arg1 - self._arg2 = arg2 - - def do_show_arg1(self, _: cmd2.Statement): - self._cmd.poutput(f'Arg1: {self._arg1}') - - def do_show_arg2(self, _: cmd2.Statement): - self._cmd.poutput(f'Arg2: {self._arg2}') - - class ExampleApp(cmd2.Cmd): - """ - CommandSets with constructor parameters are provided in the constructor - """ - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - def do_something(self, arg): - self.last_result = 5 - self.poutput('this is the something command') - - - def main(): - my_commands = CustomInitCommandSet(1, 2) - app = ExampleApp(command_sets=[my_commands]) - app.cmdloop() - - -Dynamic Commands -~~~~~~~~~~~~~~~~ - -You can also dynamically load and unload commands by installing and removing CommandSets at runtime. For example, -if you could support runtime loadable plugins or add/remove commands based on your state. - -You may need to disable command auto-loading if you need dynamically load commands at runtime. - -.. code-block:: python - - import argparse - import cmd2 - from cmd2 import CommandSet, with_argparser, with_category, with_default_category - - - @with_default_category('Fruits') - class LoadableFruits(CommandSet): - def __init__(self): - super().__init__() - - def do_apple(self, _: cmd2.Statement): - self._cmd.poutput('Apple') - - def do_banana(self, _: cmd2.Statement): - self._cmd.poutput('Banana') - - - @with_default_category('Vegetables') - class LoadableVegetables(CommandSet): - def __init__(self): - super().__init__() - - def do_arugula(self, _: cmd2.Statement): - self._cmd.poutput('Arugula') - - def do_bokchoy(self, _: cmd2.Statement): - self._cmd.poutput('Bok Choy') - - - class ExampleApp(cmd2.Cmd): - """ - CommandSets are loaded via the `load` and `unload` commands - """ - - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, auto_load_commands=False, **kwargs) - - self._fruits = LoadableFruits() - self._vegetables = LoadableVegetables() - - load_parser = cmd2.Cmd2ArgumentParser() - load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) - - @with_argparser(load_parser) - @with_category('Command Loading') - def do_load(self, ns: argparse.Namespace): - if ns.cmds == 'fruits': - try: - self.register_command_set(self._fruits) - self.poutput('Fruits loaded') - except ValueError: - self.poutput('Fruits already loaded') - - if ns.cmds == 'vegetables': - try: - self.register_command_set(self._vegetables) - self.poutput('Vegetables loaded') - except ValueError: - self.poutput('Vegetables already loaded') - - @with_argparser(load_parser) - def do_unload(self, ns: argparse.Namespace): - if ns.cmds == 'fruits': - self.unregister_command_set(self._fruits) - self.poutput('Fruits unloaded') - - if ns.cmds == 'vegetables': - self.unregister_command_set(self._vegetables) - self.poutput('Vegetables unloaded') - - - if __name__ == '__main__': - app = ExampleApp() - app.cmdloop() - - -Event Handlers --------------- -The following functions are called at different points in the ``CommandSet`` life cycle. - -``on_register(self, cmd) -> None`` - Called by cmd2.Cmd as the first step to -registering a CommandSet. The commands defined in this class have not be -added to the CLI object at this point. Subclasses can override this to -perform any initialization requiring access to the Cmd object -(e.g. configure commands and their parsers based on CLI state data). - -``on_registered(self) -> None`` - Called by cmd2.Cmd after a CommandSet is -registered and all its commands have been added to the CLI. Subclasses can -override this to perform custom steps related to the newly added commands -(e.g. setting them to a disabled state). - -``on_unregister(self) -> None`` - Called by ``cmd2.Cmd`` as the first step to -unregistering a CommandSet. Subclasses can override this to perform any cleanup -steps which require their commands being registered in the CLI. - -``on_unregistered(self) -> None`` - Called by ``cmd2.Cmd`` after a CommandSet -has been unregistered and all its commands removed from the CLI. Subclasses can -override this to perform remaining cleanup steps. - - -Injecting Subcommands ----------------------- - -Description -~~~~~~~~~~~ -Using the `with_argparse` decorator, it is possible to define subcommands for your command. This has a tendency to -either drive your interface into an object-centric interface. For example, imagine you have a tool that manages your -media collection and you want to manage movies or shows. An object-centric approach would push you to have base -commands such as `movies` and `shows` which each have subcommands `add`, `edit`, `list`, `delete`. If you wanted to -present an action-centric command set, so that `add`, `edit`, `list`, and `delete` are the base commands, you'd have -to organize your code around these similar actions rather than organizing your code around similar objects being -managed. - -Subcommand injection allows you to inject subcommands into a base command to present an interface that is sensible to -a user while still organizing your code in whatever structure make more logical sense to the developer. - -Example -~~~~~~~ - -This example is a variation on the Dynamic Commands example above. A `cut` command is introduced as a base -command and each CommandSet - -.. code-block:: python - - import argparse - import cmd2 - from cmd2 import CommandSet, with_argparser, with_category, with_default_category - - - @with_default_category('Fruits') - class LoadableFruits(CommandSet): - def __init__(self): - super().__init__() - - def do_apple(self, _: cmd2.Statement): - self._cmd.poutput('Apple') - - banana_parser = cmd2.Cmd2ArgumentParser() - banana_parser.add_argument('direction', choices=['discs', 'lengthwise']) - - @cmd2.as_subcommand_to('cut', 'banana', banana_parser) - def cut_banana(self, ns: argparse.Namespace): - """Cut banana""" - self._cmd.poutput('cutting banana: ' + ns.direction) - - - @with_default_category('Vegetables') - class LoadableVegetables(CommandSet): - def __init__(self): - super().__init__() - - def do_arugula(self, _: cmd2.Statement): - self._cmd.poutput('Arugula') - - bokchoy_parser = cmd2.Cmd2ArgumentParser() - bokchoy_parser.add_argument('style', choices=['quartered', 'diced']) - - @cmd2.as_subcommand_to('cut', 'bokchoy', bokchoy_parser) - def cut_bokchoy(self, _: argparse.Namespace): - self._cmd.poutput('Bok Choy') - - - class ExampleApp(cmd2.Cmd): - """ - CommandSets are automatically loaded. Nothing needs to be done. - """ - - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, auto_load_commands=False, **kwargs) - - self._fruits = LoadableFruits() - self._vegetables = LoadableVegetables() - - load_parser = cmd2.Cmd2ArgumentParser() - load_parser.add_argument('cmds', choices=['fruits', 'vegetables']) - - @with_argparser(load_parser) - @with_category('Command Loading') - def do_load(self, ns: argparse.Namespace): - if ns.cmds == 'fruits': - try: - self.register_command_set(self._fruits) - self.poutput('Fruits loaded') - except ValueError: - self.poutput('Fruits already loaded') - - if ns.cmds == 'vegetables': - try: - self.register_command_set(self._vegetables) - self.poutput('Vegetables loaded') - except ValueError: - self.poutput('Vegetables already loaded') - - @with_argparser(load_parser) - def do_unload(self, ns: argparse.Namespace): - if ns.cmds == 'fruits': - self.unregister_command_set(self._fruits) - self.poutput('Fruits unloaded') - - if ns.cmds == 'vegetables': - self.unregister_command_set(self._vegetables) - self.poutput('Vegetables unloaded') - - cut_parser = cmd2.Cmd2ArgumentParser() - cut_subparsers = cut_parser.add_subparsers(title='item', help='item to cut') - - @with_argparser(cut_parser) - def do_cut(self, ns: argparse.Namespace): - handler = ns.cmd2_handler.get() - if handler is not None: - # Call whatever subcommand function was selected - handler(ns) - else: - # No subcommand was provided, so call help - self.poutput('This command does nothing without sub-parsers registered') - self.do_help('cut') - - - if __name__ == '__main__': - app = ExampleApp() - app.cmdloop() diff --git a/docs/features/multiline_commands.md b/docs/features/multiline_commands.md new file mode 100644 index 000000000..f7fed85bf --- /dev/null +++ b/docs/features/multiline_commands.md @@ -0,0 +1,18 @@ +# Multiline Commands + +Command input may span multiple lines for the commands whose names are listed in the `multiline_commands` argument to `cmd2.Cmd.__init__()`. These commands will be executed only after the user has entered a *terminator*. By default, the command terminator is `;`; specifying the `terminators` optional argument to `cmd2.Cmd.__init__()` allows different terminators. A blank line is *always* considered a command terminator (cannot be overridden). + +In multiline commands, output redirection characters like `>` and `|` are part of the command arguments unless they appear after the terminator. + +## Continuation prompt + +When a user types a `Multiline Command +`{.interpreted-text role="ref"} it may span more than one line of input. The prompt for the first line of input is specified by the `cmd2.Cmd.prompt`{.interpreted-text role="attr"} instance attribute - see `features/prompt:Customizing the Prompt`{.interpreted-text role="ref"}. The prompt for subsequent lines of input is defined by the `cmd2.Cmd.continuation_prompt`{.interpreted-text role="attr"} attribute. + +## Use cases + +Multiline commands should probably be used sparingly in order to preserve a good user experience for your `cmd2`-based line-oriented command interpreter application. + +However, some use cases benefit significantly from the ability to have commands that span more than one line. For example, you might want the ability for your user to type in a SQL command, which can often span lines and which are terminated with a semicolon. + +We estimate that less than 5 percent of `cmd2` applications use this feature. But it is here for those use cases where it provides value. diff --git a/docs/features/multiline_commands.rst b/docs/features/multiline_commands.rst deleted file mode 100644 index a5ef26d8f..000000000 --- a/docs/features/multiline_commands.rst +++ /dev/null @@ -1,36 +0,0 @@ -Multiline Commands -================== - -Command input may span multiple lines for the commands whose names are listed -in the ``multiline_commands`` argument to ``cmd2.Cmd.__init__()``. These -commands will be executed only after the user has entered a *terminator*. By -default, the command terminator is ``;``; specifying the ``terminators`` -optional argument to ``cmd2.Cmd.__init__()`` allows different terminators. A -blank line is *always* considered a command terminator (cannot be overridden). - -In multiline commands, output redirection characters like ``>`` and ``|`` are -part of the command arguments unless they appear after the terminator. - -Continuation prompt -------------------- - -When a user types a :ref:`Multiline Command -` it may span more than one -line of input. The prompt for the first line of input is specified by the -:attr:`cmd2.Cmd.prompt` instance attribute - see -:ref:`features/prompt:Customizing the Prompt`. The prompt for subsequent lines -of input is defined by the :attr:`cmd2.Cmd.continuation_prompt` attribute. - -Use cases ---------- -Multiline commands should probably be used sparingly in order to preserve a -good user experience for your ``cmd2``-based line-oriented command interpreter -application. - -However, some use cases benefit significantly from the ability to have commands -that span more than one line. For example, you might want the ability for your -user to type in a SQL command, which can often span lines and which are -terminated with a semicolon. - -We estimate that less than 5 percent of ``cmd2`` applications use this feature. -But it is here for those use cases where it provides value. diff --git a/docs/features/os.md b/docs/features/os.md new file mode 100644 index 000000000..ee18fb0a2 --- /dev/null +++ b/docs/features/os.md @@ -0,0 +1,125 @@ +# Integrating with the OS + +## How to redirect output + +See `features/redirection:Output Redirection and Pipes`{.interpreted-text role="ref"} + +## Executing OS commands from within `cmd2` + +`cmd2` includes a `shell` command which executes it's arguments in the operating system shell: + + (Cmd) shell ls -al + +If you use the default `features/shortcuts_aliases_macros:Shortcuts`{.interpreted-text role="ref"} defined in `cmd2` you'll get a `!` shortcut for `shell`, which allows you to type: + + (Cmd) !ls -al + +NOTE: `cmd2` provides user-friendly tab completion throughout the process of running a shell command - first for the shell command name itself, and then for file paths in the argument section. + +## Editors + +`cmd2` includes the built-in `edit` command which runs a text editor and optionally opens a file with it: + + (Cmd) edit foo.txt + +The editor used is determined by the `editor` settable parameter and can be either a text editor such as **vim** or a graphical editor such as **VSCode**. To set it: + + set editor + +If you have the `EDITOR` environment variable set, then this will be the default value for `editor`. If not, then `cmd2` will attempt to search for any in a list of common editors for your operating system. + +## Terminal pagers + +Output of any command can be displayed one page at a time using the `~.cmd2.Cmd.ppaged`{.interpreted-text role="meth"} method. + +Alternatively, a terminal pager can be invoked directly using the ability to run shell commands with the `!` shortcut like so: + + (Cmd) !less foo.txt + +NOTE: Once you are in a terminal pager, that program temporarily has control of your terminal, **NOT** `cmd2`. Typically you can use either the arrow keys or / keys to scroll around or type `q` to quit the pager and return control to your `cmd2` application. + +## Exit codes + +The `self.exit_code` attribute of your `cmd2` application controls what exit code is returned from `cmdloop()` when it completes. It is your job to make sure that this exit code gets sent to the shell when your application exits by calling `sys.exit(app.cmdloop())`. + +## Invoking With Arguments + +Typically you would invoke a `cmd2` program by typing: + + $ python mycmd2program.py + +or: + + $ mycmd2program.py + +Either of these methods will launch your program and enter the `cmd2` command loop, which allows the user to enter commands, which are then executed by your program. + +You may want to execute commands in your program without prompting the user for any input. There are several ways you might accomplish this task. The easiest one is to pipe commands and their arguments into your program via standard input. You don't need to do anything to your program in order to use this technique. Here's a demonstration using the `examples/example.py` included in the source code of `cmd2`: + + $ echo "speak -p some words" | python examples/example.py + omesay ordsway + +Using this same approach you could create a text file containing the commands you would like to run, one command per line in the file. Say your file was called `somecmds.txt`. To run the commands in the text file using your `cmd2` program (from a Windows command prompt): + + c:\cmd2> type somecmds.txt | python.exe examples/example.py + omesay ordsway + +By default, `cmd2` programs also look for commands pass as arguments from the operating system shell, and execute those commands before entering the command loop: + + $ python examples/example.py help + + Documented commands (use 'help -v' for verbose/'help ' for details): + =========================================================================== + alias help macro orate quit run_script set shortcuts + edit history mumble py run_pyscript say shell speak + + (Cmd) + +You may need more control over command line arguments passed from the operating system shell. For example, you might have a command inside your `cmd2` program which itself accepts arguments, and maybe even option strings. Say you wanted to run the `speak` command from the operating system shell, but have it say it in pig latin: + + $ python example/example.py speak -p hello there + python example.py speak -p hello there + usage: speak [-h] [-p] [-s] [-r REPEAT] words [words ...] + speak: error: the following arguments are required: words + *** Unknown syntax: -p + *** Unknown syntax: hello + *** Unknown syntax: there + (Cmd) + +Uh-oh, that's not what we wanted. `cmd2` treated `-p`, `hello`, and `there` as commands, which don't exist in that program, thus the syntax errors. + +There is an easy way around this, which is demonstrated in `examples/cmd_as_argument.py`. By setting `allow_cli_args=False` you can so your own argument parsing of the command line: + + $ python examples/cmd_as_argument.py speak -p hello there + ellohay heretay + +Check the source code of this example, especially the `main()` function, to see the technique. + +Alternatively you can simply wrap the command plus arguments in quotes (either single or double quotes): + + $ python example/example.py "speak -p hello there" + ellohay heretay + (Cmd) + +### Automating cmd2 apps from other CLI/CLU tools + +While `cmd2` is designed to create **interactive** command-line applications which enter a Read-Evaluate-Print-Loop (REPL), there are a great many times when it would be useful to use a `cmd2` application as a run-and-done command-line utility for purposes of automation and scripting. + +This is easily achieved by combining the following capabilities of `cmd2`: + +1. Ability to invoke a `cmd2` application with arguments +2. Ability to set an exit code when leaving a `cmd2` application +3. Ability to exit a `cmd2` application with the `quit` command + +Here is a simple example which doesn't require the quit command since the custom `exit` command quits while returning an exit code: + + $ python examples/exit_code.py "exit 23" + 'examples/exit_code.py' exiting with code: 23 + $ echo $? + 23 + +Here is another example using `quit`: + + $ python example/example.py "speak -p hello there" quit + ellohay heretay + $ diff --git a/docs/features/os.rst b/docs/features/os.rst deleted file mode 100644 index 77bc6a668..000000000 --- a/docs/features/os.rst +++ /dev/null @@ -1,177 +0,0 @@ -Integrating with the OS -======================= - -How to redirect output ----------------------- - -See :ref:`features/redirection:Output Redirection and Pipes` - -Executing OS commands from within ``cmd2`` ------------------------------------------- - -``cmd2`` includes a ``shell`` command which executes it's arguments in the -operating system shell:: - - (Cmd) shell ls -al - -If you use the default :ref:`features/shortcuts_aliases_macros:Shortcuts` -defined in ``cmd2`` you'll get a ``!`` shortcut for ``shell``, which allows you -to type:: - - (Cmd) !ls -al - -NOTE: ``cmd2`` provides user-friendly tab completion throughout the process of -running a shell command - first for the shell command name itself, and then for -file paths in the argument section. - -Editors -------- - -``cmd2`` includes the built-in ``edit`` command which runs a text editor and -optionally opens a file with it:: - - (Cmd) edit foo.txt - -The editor used is determined by the ``editor`` settable parameter and can -be either a text editor such as **vim** or a graphical editor such as -**VSCode**. To set it:: - - set editor - -If you have the ``EDITOR`` environment variable set, then this will be the -default value for ``editor``. If not, then ``cmd2`` will attempt to search -for any in a list of common editors for your operating system. - -Terminal pagers ---------------- - -Output of any command can be displayed one page at a time using the -:meth:`~.cmd2.Cmd.ppaged` method. - -Alternatively, a terminal pager can be invoked directly using the ability -to run shell commands with the ``!`` shortcut like so:: - - (Cmd) !less foo.txt - -NOTE: Once you are in a terminal pager, that program temporarily has control -of your terminal, **NOT** ``cmd2``. Typically you can use either the arrow -keys or / keys to scroll around or type ``q`` to quit the -pager and return control to your ``cmd2`` application. - -Exit codes ----------- - -The ``self.exit_code`` attribute of your ``cmd2`` application controls what -exit code is returned from ``cmdloop()`` when it completes. It is your job to -make sure that this exit code gets sent to the shell when your application -exits by calling ``sys.exit(app.cmdloop())``. - -Invoking With Arguments ------------------------ - -Typically you would invoke a ``cmd2`` program by typing:: - - $ python mycmd2program.py - -or:: - - $ mycmd2program.py - -Either of these methods will launch your program and enter the ``cmd2`` command -loop, which allows the user to enter commands, which are then executed by your -program. - -You may want to execute commands in your program without prompting the user for -any input. There are several ways you might accomplish this task. The easiest -one is to pipe commands and their arguments into your program via standard -input. You don't need to do anything to your program in order to use this -technique. Here's a demonstration using the ``examples/example.py`` included in -the source code of ``cmd2``:: - - $ echo "speak -p some words" | python examples/example.py - omesay ordsway - -Using this same approach you could create a text file containing the commands -you would like to run, one command per line in the file. Say your file was -called ``somecmds.txt``. To run the commands in the text file using your -``cmd2`` program (from a Windows command prompt):: - - c:\cmd2> type somecmds.txt | python.exe examples/example.py - omesay ordsway - -By default, ``cmd2`` programs also look for commands pass as arguments from the -operating system shell, and execute those commands before entering the command -loop:: - - $ python examples/example.py help - - Documented commands (use 'help -v' for verbose/'help ' for details): - =========================================================================== - alias help macro orate quit run_script set shortcuts - edit history mumble py run_pyscript say shell speak - - (Cmd) - -You may need more control over command line arguments passed from the operating -system shell. For example, you might have a command inside your ``cmd2`` -program which itself accepts arguments, and maybe even option strings. Say you -wanted to run the ``speak`` command from the operating system shell, but have -it say it in pig latin:: - - $ python example/example.py speak -p hello there - python example.py speak -p hello there - usage: speak [-h] [-p] [-s] [-r REPEAT] words [words ...] - speak: error: the following arguments are required: words - *** Unknown syntax: -p - *** Unknown syntax: hello - *** Unknown syntax: there - (Cmd) - -Uh-oh, that's not what we wanted. ``cmd2`` treated ``-p``, ``hello``, and -``there`` as commands, which don't exist in that program, thus the syntax -errors. - -There is an easy way around this, which is demonstrated in -``examples/cmd_as_argument.py``. By setting ``allow_cli_args=False`` you can so -your own argument parsing of the command line:: - - $ python examples/cmd_as_argument.py speak -p hello there - ellohay heretay - -Check the source code of this example, especially the ``main()`` function, to -see the technique. - -Alternatively you can simply wrap the command plus arguments in quotes (either -single or double quotes):: - - $ python example/example.py "speak -p hello there" - ellohay heretay - (Cmd) - -Automating cmd2 apps from other CLI/CLU tools -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -While ``cmd2`` is designed to create **interactive** command-line applications -which enter a Read-Evaluate-Print-Loop (REPL), there are a great many times -when it would be useful to use a ``cmd2`` application as a run-and-done -command-line utility for purposes of automation and scripting. - -This is easily achieved by combining the following capabilities of ``cmd2``: - -#. Ability to invoke a ``cmd2`` application with arguments -#. Ability to set an exit code when leaving a ``cmd2`` application -#. Ability to exit a ``cmd2`` application with the ``quit`` command - -Here is a simple example which doesn't require the quit command since the -custom ``exit`` command quits while returning an exit code:: - - $ python examples/exit_code.py "exit 23" - 'examples/exit_code.py' exiting with code: 23 - $ echo $? - 23 - -Here is another example using ``quit``:: - - $ python example/example.py "speak -p hello there" quit - ellohay heretay - $ diff --git a/docs/features/packaging.md b/docs/features/packaging.md new file mode 100644 index 000000000..7c0f48d99 --- /dev/null +++ b/docs/features/packaging.md @@ -0,0 +1,29 @@ +# Packaging a cmd2 application for distribution + +As a general-purpose tool for building interactive command-line applications, `cmd2` is designed to be used in many ways. How you distribute your `cmd2` application to customers or end users is up to you. See the [Overview of Packaging for Python](https://packaging.python.org/overview/) from the Python Packaging Authority for a thorough discussion of the extensive options within the Python ecosystem. + +For developers wishing to package a `cmd2` application into a single binary image or compressed file, we can recommend all of the following based on personal and professional experience: + +- Deploy your `cmd2` Python app using [Docker](https://djangostars.com/blog/what-is-docker-and-how-to-use-it-with-python/) + + * Powerful and flexible - allows you to control entire user space and setup other applications like databases + + * As long as it isn't problematic for your customers to have Docker installed, then this is probably the best option + +- [PyInstaller](https://www.pyinstaller.org) + + * Quick and easy - it "just works" and everything you need is installable via `pip` + + * Packages up all of the dependencies into a single directory which you can then zip up + +- [Nuitka](https://nuitka.net) + + - Converts your Python to C and compiles it to a native binary file + + * This can be particularly convenient if you wish to obfuscate the Python source code behind your application + + * Recommend invoking with `--follow-imports` flag like: `python3 -m nuitka --follow-imports your_app.py` + +- [Conda Constructor](https://github.com/conda/constructor) + + - Allows you to create a custom Python distro based on [Miniconda](https://docs.conda.io/en/latest/miniconda.html) diff --git a/docs/features/packaging.rst b/docs/features/packaging.rst deleted file mode 100644 index a65fd6ddd..000000000 --- a/docs/features/packaging.rst +++ /dev/null @@ -1,39 +0,0 @@ - -Packaging a cmd2 application for distribution -================================================= - -As a general-purpose tool for building interactive command-line applications, -``cmd2`` is designed to be used in many ways. How you distribute your ``cmd2`` -application to customers or end users is up to you. See the -`Overview of Packaging for Python`_ from the Python Packaging Authority for a -thorough discussion of the extensive options within the Python ecosystem. - -For developers wishing to package a ``cmd2`` application into a single binary -image or compressed file, we can recommend all of the following based on -personal and professional experience: - -* Deploy your ``cmd2`` Python app using Docker_ - * Powerful and flexible - allows you to control entire user space and - setup other applications like databases - * As long as it isn't problematic for your customers to have Docker - installed, then this is probably the best option -* PyInstaller_ - * Quick and easy - it "just works" and everything you need is installable - via ``pip`` - * Packages up all of the dependencies into a single directory which you can - then zip up -* Nuitka_ - * Converts your Python to C and compiles it to a native binary file - * This can be particularly convenient if you wish to obfuscate the Python - source code behind your application - * Recommend invoking with ``--follow-imports`` flag like: - ``python3 -m nuitka --follow-imports your_app.py`` -* `Conda Constructor`_ - * Allows you to create a custom Python distro based on Miniconda_ - -.. _`Overview of Packaging for Python`: https://packaging.python.org/overview/ -.. _Docker: https://djangostars.com/blog/what-is-docker-and-how-to-use-it-with-python/ -.. _PyInstaller: https://www.pyinstaller.org -.. _Nuitka: https://nuitka.net -.. _`Conda Constructor`: https://github.com/conda/constructor -.. _Miniconda: https://docs.conda.io/en/latest/miniconda.html diff --git a/docs/features/plugins.md b/docs/features/plugins.md new file mode 100644 index 000000000..6d89ed22e --- /dev/null +++ b/docs/features/plugins.md @@ -0,0 +1,104 @@ +# Plugins + +`cmd2` has a built-in plugin framework which allows developers to create a a `cmd2` plugin which can extend basic `cmd2` functionality and can be used by multiple applications. + +There are many ways to add functionality to `cmd2` using a plugin. Most plugins will be implemented as a mixin. A mixin is a class that encapsulates and injects code into another class. Developers who use a plugin in their `cmd2` project will inject the plugin's code into their subclass of `cmd2.Cmd`{.interpreted-text role="class"}. + +## Mixin and Initialization + +The following short example shows how to mix in a plugin and how the plugin gets initialized. + +Here's the plugin: + + class MyPlugin: + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd initializes + +and an example app which uses the plugin: + + import cmd2 + import cmd2_myplugin + + class Example(cmd2_myplugin.MyPlugin, cmd2.Cmd): + """An class to show how to use a plugin""" + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd or + # any plugins initialize + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd and + # all plugins have initialized + +Note how the plugin must be inherited (or mixed in) before `cmd2.Cmd`{.interpreted-text role="class"}. This is required for two reasons: + +- The `cmd.Cmd.__init__` method in the python standard library does not call `super().__init__()`. Because of this oversight, if you don't inherit from `MyPlugin` first, the `MyPlugin.__init__()` method will never be called. +- You may want your plugin to be able to override methods from `cmd2.Cmd`{.interpreted-text role="class"}. If you mixin the plugin after `cmd2.Cmd`, the python method resolution order will call `cmd2.Cmd`{.interpreted-text role="class"} methods before it calls those in your plugin. + +## Add commands + +Your plugin can add user visible commands. You do it the same way in a plugin that you would in a `cmd2.Cmd`{.interpreted-text role="class"} app: + + class MyPlugin: + def do_say(self, statement): + """Simple say command""" + self.poutput(statement) + +You have all the same capabilities within the plugin that you do inside a `cmd2.Cmd`{.interpreted-text role="class"} app, including argument parsing via decorators and custom help methods. + +## Add (or hide) settings + +A plugin may add user controllable settings to the application. Here's an example: + + class MyPlugin: + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2.Cmd initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2.Cmd initializes + self.mysetting = 'somevalue' + self.add_settable(cmd2.Settable('mysetting', str, 'short help message for mysetting', self)) + +You can hide settings from the user by calling `~cmd2.Cmd.remove_settable`{.interpreted-text role="meth"}. See `features/settings:Settings`{.interpreted-text role="ref"} for more information. + +## Decorators + +Your plugin can provide a decorator which users of your plugin can use to wrap functionality around their own commands. + +## Override methods + +Your plugin can override core `cmd2.Cmd`{.interpreted-text role="class"} methods, changing their behavior. This approach should be used sparingly, because it is very brittle. If a developer chooses to use multiple plugins in their application, and several of the plugins override the same method, only the first plugin to be mixed in will have the overridden method called. + +Hooks are a much better approach. + +## Hooks + +Plugins can register hook methods, which are called by `cmd2.Cmd`{.interpreted-text role="class"} during various points in the application and command processing lifecycle. Plugins should not override any of the deprecated hook methods, instead they should register their hooks as described in the `features/hooks:Hooks`{.interpreted-text role="ref"} section. + +You should name your hooks so that they begin with the name of your plugin. Hook methods get mixed into the `cmd2` application and this naming convention helps avoid unintentional method overriding. + +Here's a simple example: + + class MyPlugin: + def __init__(self, *args, **kwargs): + # code placed here runs before cmd2 initializes + super().__init__(*args, **kwargs) + # code placed here runs after cmd2 initializes + # this is where you register any hook functions + self.register_postparsing_hook(self.cmd2_myplugin_postparsing_hook) + + def cmd2_myplugin_postparsing_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: + """Method to be called after parsing user input, but before running the command""" + self.poutput('in postparsing_hook') + return data + +Registration allows multiple plugins (or even the application itself) to each inject code to be called during the application or command processing lifecycle. + +See the `features/hooks:Hooks`{.interpreted-text role="ref"} documentation for full details of the application and command lifecycle, including all available hooks and the ways hooks can influence the lifecycle. + +## Classes and Functions + +Your plugin can also provide classes and functions which can be used by developers of `cmd2` based applications. Describe these classes and functions in your documentation so users of your plugin will know what's available. + +## Examples + +See for more info. diff --git a/docs/features/plugins.rst b/docs/features/plugins.rst deleted file mode 100644 index 13a3910bb..000000000 --- a/docs/features/plugins.rst +++ /dev/null @@ -1,159 +0,0 @@ -Plugins -======= - -``cmd2`` has a built-in plugin framework which allows developers to create a -a ``cmd2`` plugin which can extend basic ``cmd2`` functionality and can be -used by multiple applications. - -There are many ways to add functionality to ``cmd2`` using a plugin. Most -plugins will be implemented as a mixin. A mixin is a class that encapsulates -and injects code into another class. Developers who use a plugin in their -``cmd2`` project will inject the plugin's code into their subclass of -:class:`cmd2.Cmd`. - - -Mixin and Initialization ------------------------- - -The following short example shows how to mix in a plugin and how the plugin -gets initialized. - -Here's the plugin:: - - class MyPlugin: - def __init__(self, *args, **kwargs): - # code placed here runs before cmd2.Cmd initializes - super().__init__(*args, **kwargs) - # code placed here runs after cmd2.Cmd initializes - -and an example app which uses the plugin:: - - import cmd2 - import cmd2_myplugin - - class Example(cmd2_myplugin.MyPlugin, cmd2.Cmd): - """An class to show how to use a plugin""" - def __init__(self, *args, **kwargs): - # code placed here runs before cmd2.Cmd or - # any plugins initialize - super().__init__(*args, **kwargs) - # code placed here runs after cmd2.Cmd and - # all plugins have initialized - -Note how the plugin must be inherited (or mixed in) before :class:`cmd2.Cmd`. -This is required for two reasons: - -- The ``cmd.Cmd.__init__`` method in the python standard library does not - call ``super().__init__()``. Because of this oversight, if you don't - inherit from ``MyPlugin`` first, the ``MyPlugin.__init__()`` method will - never be called. -- You may want your plugin to be able to override methods from - :class:`cmd2.Cmd`. If you mixin the plugin after ``cmd2.Cmd``, the python - method resolution order will call :class:`cmd2.Cmd` methods before it calls - those in your plugin. - - -Add commands ------------- - -Your plugin can add user visible commands. You do it the same way in a plugin -that you would in a :class:`cmd2.Cmd` app:: - - class MyPlugin: - def do_say(self, statement): - """Simple say command""" - self.poutput(statement) - -You have all the same capabilities within the plugin that you do inside a -:class:`cmd2.Cmd` app, including argument parsing via decorators and custom -help methods. - - -Add (or hide) settings ----------------------- - -A plugin may add user controllable settings to the application. Here's an -example:: - - class MyPlugin: - def __init__(self, *args, **kwargs): - # code placed here runs before cmd2.Cmd initializes - super().__init__(*args, **kwargs) - # code placed here runs after cmd2.Cmd initializes - self.mysetting = 'somevalue' - self.add_settable(cmd2.Settable('mysetting', str, 'short help message for mysetting', self)) - -You can hide settings from the user by calling -:meth:`~cmd2.Cmd.remove_settable`. See :ref:`features/settings:Settings` for -more information. - - -Decorators ----------- - -Your plugin can provide a decorator which users of your plugin can use to -wrap functionality around their own commands. - - -Override methods ----------------- - -Your plugin can override core :class:`cmd2.Cmd` methods, changing their -behavior. This approach should be used sparingly, because it is very brittle. -If a developer chooses to use multiple plugins in their application, and -several of the plugins override the same method, only the first plugin to be -mixed in will have the overridden method called. - -Hooks are a much better approach. - - -Hooks ------ - -Plugins can register hook methods, which are called by :class:`cmd2.Cmd` -during various points in the application and command processing lifecycle. -Plugins should not override any of the deprecated hook methods, instead they -should register their hooks as described in the :ref:`features/hooks:Hooks` -section. - -You should name your hooks so that they begin with the name of your plugin. -Hook methods get mixed into the ``cmd2`` application and this naming -convention helps avoid unintentional method overriding. - -Here's a simple example:: - - class MyPlugin: - def __init__(self, *args, **kwargs): - # code placed here runs before cmd2 initializes - super().__init__(*args, **kwargs) - # code placed here runs after cmd2 initializes - # this is where you register any hook functions - self.register_postparsing_hook(self.cmd2_myplugin_postparsing_hook) - - def cmd2_myplugin_postparsing_hook(self, data: cmd2.plugin.PostparsingData) -> cmd2.plugin.PostparsingData: - """Method to be called after parsing user input, but before running the command""" - self.poutput('in postparsing_hook') - return data - -Registration allows multiple plugins (or even the application itself) to each -inject code to be called during the application or command processing -lifecycle. - -See the :ref:`features/hooks:Hooks` documentation for full details of the -application and command lifecycle, including all available hooks and the -ways hooks can influence the lifecycle. - - -Classes and Functions ---------------------- - -Your plugin can also provide classes and functions which can be used by -developers of ``cmd2`` based applications. Describe these classes and -functions in your documentation so users of your plugin will know what's -available. - - -Examples --------- - -See ``_ for more info. diff --git a/docs/features/prompt.md b/docs/features/prompt.md new file mode 100644 index 000000000..2e864f399 --- /dev/null +++ b/docs/features/prompt.md @@ -0,0 +1,42 @@ +# Prompt + +`cmd2` issues a configurable prompt before soliciting user input. + +## Customizing the Prompt + +This prompt can be configured by setting the `cmd2.Cmd.prompt`{.interpreted-text role="attr"} instance attribute. This contains the string which should be printed as a prompt for user input. See the [Pirate](https://github.com/python-cmd2/cmd2/blob/master/examples/pirate.py#L39) example for the simple use case of statically setting the prompt. + +## Continuation Prompt + +When a user types a `Multiline Command `{.interpreted-text role="ref"} it may span more than one line of input. The prompt for the first line of input is specified by the `cmd2.Cmd.prompt`{.interpreted-text role="attr"} instance attribute. The prompt for subsequent lines of input is defined by the `cmd2.Cmd.continuation_prompt`{.interpreted-text role="attr"} attribute.See the [Initialization](https://github.com/python-cmd2/cmd2/blob/master/examples/initialization.py#L42) example for a demonstration of customizing the continuation prompt. + +## Updating the prompt + +If you wish to update the prompt between commands, you can do so using one of the `Application Lifecycle Hooks `{.interpreted-text role="ref"} such as a `Postcommand hook `{.interpreted-text role="ref"}. See [PythonScripting](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py#L38-L55) for an example of dynamically updating the prompt. + +## Asynchronous Feedback + +`cmd2` provides these functions to provide asynchronous feedback to the user without interfering with the command line. This means the feedback is provided to the user when they are still entering text at the prompt. To use this functionality, the application must be running in a terminal that supports VT100 control characters and readline. Linux, Mac, and Windows 10 and greater all support these. + +TODO replace with mkdocstrings: + + .. automethod:: cmd2.Cmd.async_alert + :noindex: + + .. automethod:: cmd2.Cmd.async_update_prompt + :noindex: + + .. automethod:: cmd2.Cmd.async_refresh_prompt + :noindex: + + .. automethod:: cmd2.Cmd.need_prompt_refresh + :noindex: + +`cmd2` also provides a function to change the title of the terminal window. This feature requires the application be running in a terminal that supports VT100 control characters. Linux, Mac, and Windows 10 and greater all support these. + +TODO replace with mkdocstrings: + + .. automethod:: cmd2.Cmd.set_window_title + :noindex: + +The easiest way to understand these functions is to see the [AsyncPrinting](https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py) example for a demonstration. diff --git a/docs/features/prompt.rst b/docs/features/prompt.rst deleted file mode 100644 index e429e8962..000000000 --- a/docs/features/prompt.rst +++ /dev/null @@ -1,72 +0,0 @@ -Prompt -====== - -``cmd2`` issues a configurable prompt before soliciting user input. - -Customizing the Prompt ----------------------- - -This prompt can be configured by setting the :attr:`cmd2.Cmd.prompt` instance -attribute. This contains the string which should be printed as a prompt -for user input. See the Pirate_ example for the simple use case of statically -setting the prompt. - -.. _Pirate: https://github.com/python-cmd2/cmd2/blob/master/examples/pirate.py#L39 - -Continuation Prompt -------------------- - -When a user types a -:ref:`Multiline Command ` -it may span more than one line of input. The prompt for the first line of input -is specified by the :attr:`cmd2.Cmd.prompt` instance attribute. The prompt for -subsequent lines of input is defined by the -:attr:`cmd2.Cmd.continuation_prompt` attribute.See the Initialization_ example -for a demonstration of customizing the continuation prompt. - -.. _Initialization: https://github.com/python-cmd2/cmd2/blob/master/examples/initialization.py#L42 - -Updating the prompt -------------------- - -If you wish to update the prompt between commands, you can do so using one of -the :ref:`Application Lifecycle Hooks ` such as a -:ref:`Postcommand hook `. See -PythonScripting_ for an example of dynamically updating the prompt. - -.. _PythonScripting: https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py#L38-L55 - -Asynchronous Feedback ---------------------- - -``cmd2`` provides these functions to provide asynchronous feedback to the user -without interfering with the command line. This means the feedback is provided -to the user when they are still entering text at the prompt. To use this -functionality, the application must be running in a terminal that supports -VT100 control characters and readline. Linux, Mac, and Windows 10 and greater -all support these. - -.. automethod:: cmd2.Cmd.async_alert - :noindex: - -.. automethod:: cmd2.Cmd.async_update_prompt - :noindex: - -.. automethod:: cmd2.Cmd.async_refresh_prompt - :noindex: - -.. automethod:: cmd2.Cmd.need_prompt_refresh - :noindex: - -``cmd2`` also provides a function to change the title of the terminal window. -This feature requires the application be running in a terminal that supports -VT100 control characters. Linux, Mac, and Windows 10 and greater all support -these. - -.. automethod:: cmd2.Cmd.set_window_title - :noindex: - -The easiest way to understand these functions is to see the AsyncPrinting_ -example for a demonstration. - -.. _AsyncPrinting: https://github.com/python-cmd2/cmd2/blob/master/examples/async_printing.py diff --git a/docs/features/redirection.md b/docs/features/redirection.md new file mode 100644 index 000000000..259b5f637 --- /dev/null +++ b/docs/features/redirection.md @@ -0,0 +1,55 @@ +# Output Redirection and Pipes + +As in POSIX shells, output of a command can be redirected and/or piped. This feature is fully cross-platform and works identically on Windows, macOS, and Linux. + +## Output Redirection + +### Redirect to a file + +Redirecting the output of a `cmd2` command to a file works just like in POSIX shells: + +> - send to a file with `>`, as in `mycommand args > filename.txt` +> - append to a file with `>>`, as in `mycommand args >> filename.txt` + +If you need to include any of these redirection characters in your command, you can enclose them in quotation marks, `mycommand 'with > in the argument'`. + +### Redirect to the clipboard + +`cmd2` output redirection supports an additional feature not found in most shells - if the file name following the `>` or `>>` is left blank, then the output is redirected to the operating system clipboard so that it can then be pasted into another program. + +> - overwrite the clipboard with `mycommand args >` +> - append to the clipboard with `mycommand args >>` + +## Pipes + +Piping the output of a `cmd2` command to a shell command works just like in POSIX shells: + +> - pipe as input to a shell command with `|`, as in `mycommand args | wc` + +## Multiple Pipes and Redirection + +Multiple pipes, optionally followed by a redirect, are supported. Thus, it is possible to do something like the following: + + (Cmd) help | grep py | wc > output.txt + +The above runs the **help** command, pipes its output to **grep** searching for any lines containing _py_, then pipes the output of grep to the **wc** "word count" command, and finally writes redirects the output of that to a file called _output.txt_. + +## Disabling Redirection + +!!! note + + If you wish to disable cmd2's output redirection and pipes features, you can do so by setting the `allow_redirection` attribute of your `cmd2.Cmd` class instance to `False`. This would be useful, for example, if you want to restrict the ability for an end user to write to disk or interact with shell commands for security reasons: + + from cmd2 import Cmd + class App(Cmd): + def __init__(self): + super().__init__(allow_redirection=False) + + cmd2's parser will still treat the `>`, `>>`, and `|` symbols as output redirection and pipe symbols and will strip arguments after them from the command line arguments accordingly. But output from a command will not be redirected to a file or piped to a shell command. + +## Limitations of Redirection + +Some limitations apply to redirection and piping within `cmd2` applications: + +- Can only pipe to shell commands, not other `cmd2` application commands +- **stdout** gets redirected/piped, **stderr** does not diff --git a/docs/features/redirection.rst b/docs/features/redirection.rst deleted file mode 100644 index ef87d26c7..000000000 --- a/docs/features/redirection.rst +++ /dev/null @@ -1,80 +0,0 @@ -Output Redirection and Pipes -============================ - -As in POSIX shells, output of a command can be redirected and/or piped. This -feature is fully cross-platform and works identically on Windows, macOS, and -Linux. - -Output Redirection ------------------- - -Redirect to a file -~~~~~~~~~~~~~~~~~~ - -Redirecting the output of a ``cmd2`` command to a file works just like in -POSIX shells: - - - send to a file with ``>``, as in ``mycommand args > filename.txt`` - - append to a file with ``>>``, as in ``mycommand args >> filename.txt`` - -If you need to include any of these redirection characters in your command, you -can enclose them in quotation marks, ``mycommand 'with > in the argument'``. - -Redirect to the clipboard -~~~~~~~~~~~~~~~~~~~~~~~~~ - -``cmd2`` output redirection supports an additional feature not found in most -shells - if the file name following the ``>`` or ``>>`` is left blank, then -the output is redirected to the operating system clipboard so that it can -then be pasted into another program. - - - overwrite the clipboard with ``mycommand args >`` - - append to the clipboard with ``mycommand args >>`` - -Pipes ------ -Piping the output of a ``cmd2`` command to a shell command works just like in -POSIX shells: - - - pipe as input to a shell command with ``|``, as in ``mycommand args | wc`` - -Multiple Pipes and Redirection ------------------------------- -Multiple pipes, optionally followed by a redirect, are supported. Thus, it is -possible to do something like the following:: - - (Cmd) help | grep py | wc > output.txt - -The above runs the **help** command, pipes its output to **grep** searching for -any lines containing *py*, then pipes the output of grep to the **wc** -"word count" command, and finally writes redirects the output of that to a file -called *output.txt*. - -Disabling Redirection ---------------------- - -.. note:: - - If you wish to disable cmd2's output redirection and pipes features, you can - do so by setting the ``allow_redirection`` attribute of your ``cmd2.Cmd`` - class instance to ``False``. This would be useful, for example, if you want - to restrict the ability for an end user to write to disk or interact with - shell commands for security reasons:: - - from cmd2 import Cmd - class App(Cmd): - def __init__(self): - super().__init__(allow_redirection=False) - - cmd2's parser will still treat the ``>``, ``>>``, and `|` symbols as output - redirection and pipe symbols and will strip arguments after them from the - command line arguments accordingly. But output from a command will not be - redirected to a file or piped to a shell command. - -Limitations of Redirection --------------------------- - -Some limitations apply to redirection and piping within ``cmd2`` applications: - -- Can only pipe to shell commands, not other ``cmd2`` application commands -- **stdout** gets redirected/piped, **stderr** does not diff --git a/docs/features/scripting.md b/docs/features/scripting.md new file mode 100644 index 000000000..6668e1a55 --- /dev/null +++ b/docs/features/scripting.md @@ -0,0 +1,403 @@ +# Scripting + +Operating system shells have long had the ability to execute a sequence of commands saved in a text file. These script files make long sequences of commands easier to repeatedly execute. `cmd2` supports two similar mechanisms: command scripts and python scripts. + +## Command Scripts + +A command script contains a sequence of commands typed at the the prompt of a `cmd2` based application. Unlike operating system shell scripts, command scripts can't contain logic or loops. + +### Creating Command Scripts + +Command scripts can be created in several ways: + +- creating a text file using any method of your choice +- using the built-in `features/builtin_commands:edit`{.interpreted-text role="ref"} command to create or edit an existing text file +- saving previously entered commands to a script file using `history -s `{.interpreted-text role="ref"} + +If you create create a text file from scratch, just include one command per line, exactly as you would type it inside a `cmd2` application. + +### Running Command Scripts + +Command script files can be executed using the built-in `features/builtin_commands:run_script`{.interpreted-text role="ref"} command or the `@` shortcut (if your application is using the default shortcuts). Both ASCII and UTF-8 encoded unicode text files are supported. The `features/builtin_commands:run_script`{.interpreted-text role="ref"} command supports tab completion of file system paths. There is a variant `features/builtin_commands:_relative_run_script`{.interpreted-text role="ref"} command or `@@` shortcut (if using the default shortcuts) for use within a script which uses paths relative to the first script. + +### Comments + +Any command line input where the first non-whitespace character is a `\#` will be treated as a comment. This means any `\#` character appearing later in the command will be treated as a literal. The same applies to a `\#` in the middle of a multiline command, even if it is the first character on a line. + +Comments are useful in scripts, but would be pointless within an interactive session. + + (Cmd) # this is a comment + (Cmd) command # this is not a comment + +## Python Scripts {: #scripting-python-scripts } + +If you require logic flow, loops, branching, or other advanced features, you can write a python script which executes in the context of your `cmd2` app. This script is run using the `features/builtin_commands:run_pyscript`{.interpreted-text role="ref"} command. Here's a simple example that uses the [arg_printer](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py) script: + + (Cmd) run_pyscript examples/scripts/arg_printer.py foo bar 'baz 23' + Running Python script 'arg_printer.py' which was called with 3 arguments + arg 1: 'foo' + arg 2: 'bar' + arg 3: 'baz 23' + +`features/builtin_commands:run_pyscript`{.interpreted-text role="ref"} supports tab completion of file system paths, and as shown above it has the ability to pass command-line arguments to the scripts invoked. + +## Developing a CMD2 API + +If you as an app designer have not explicitly disabled the run[pyscript]{#pyscript} command it must be assumed that your application is structured for use in higher level python scripting. The following sections are meant as guidelines and highlight possible pitfalls with both production and consumption of API functionality. For clarity when speaking of "scripter" we are referring to those writing scripts to be run by pyscript and "designer" as the CMD2 application author. + +### Basics + +Without any work on the part of the designer, a scripter can take advantage of piecing together workflows using simple `app` calls. The result of a run[pyscript]{#pyscript} app call yields a `CommandResult` object exposing four members: `Stdout`, `Stderr`, `Stop`, and `Data`. + +`Stdout` and `Stderr` are fairly straightforward representations of normal data streams and accurately reflect what is seen by the user during normal cmd2 interaction. `Stop` contains information about how the invoked command has ended its lifecycle. Lastly `Data` contains any information the designer sets via `self.last_result` or `self._cmd.last_result` if called from inside a CommandSet. + +Python scripts executed with `features/builtin_commands:run_pyscript`{.interpreted-text role="ref"} can run `cmd2` application commands by using the syntax: + + app(‘command args’) + +where: + +- `app` is a configurable name which can be changed by setting the `cmd2.Cmd.py_bridge_name`{.interpreted-text role="data"} attribute +- `command` and `args` are entered exactly like they would be entered by a user of your application. + +Using fstrings tends to be the most straight forward and easily readable way to provide parameters.: + + first = 'first' + second = 'second' + + app(f'command {first} -t {second}) + +See [python_scripting](https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py) example and associated [conditional](https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/conditional.py) script for more information. + +### Design principles + +If the cmd2 application follows the [unix_design_philosophy](https://en.wikipedia.org/wiki/Unix_philosophy) a scriptor will have the most flexibility to piece together workflows using different commands. If the designers' application is more complete and less likely to be augmented in the future a scripter may opt for simple serial scripts with little control flow. In either case, choices made by the designer will have effects on scripters. + +The following diagram illustrates the different boundaries to keep in mind. + + +---------------------------------------------+ + | | + | Py scripts | + | | + | +-----------------------------------------+ | + | | CMD2 Application | | + | | | | + | | +-------------------------------------+ | | + | | | Class Library | | | + | | | +------+ +------+ +------+ +------+ | | | + | | | | | | | | | | | | | | + | | | | C | | C | | C | | C | | | | + | | | | | | | | | | | | | | + | | | +------+ +------+ +------+ +------+ | | | + | | | | | | + | | +-------------------------------------+ | | + | | | | + | +-----------------------------------------+ | + | | + +---------------------------------------------+ + +!!! note + + As a designer it is preferable to design from the inside to out. Your code will be infinitely far easier to unit test than at the higher level. While there are regression testing extensions for cmd2 UnitTesting will always be faster for development. + +!!! warning + + It is bad design or a high level py[script]{#script} to know about let alone access low level class libraries of an application. Resist this urge at all costs, unless it's necessary. + +### Developing a Basic API + +CMD2 out of the box allows scripters to take advantage of all exposed `do_*` commands. As a scripter one can easily interact with the application via `stdout` and `stderr`. + +As a baseline lets start off with the familiar FirstApp + + #!/usr/bin/env python + """A simple cmd2 application.""" + import cmd2 + + + class FirstApp(cmd2.Cmd): + """A simple cmd2 application.""" + def __init__(self): + shortcuts = cmd2.DEFAULT_SHORTCUTS + shortcuts.update({'&': 'speak'}) + super().__init__(shortcuts=shortcuts) + + # Make maxrepeats settable at runtime + self.maxrepeats = 3 + self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) + + + speak_parser = cmd2.Cmd2ArgumentParser() + speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') + speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') + speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') + speak_parser.add_argument('words', nargs='+', help='words to say') + + @cmd2.with_argparser(speak_parser) + def do_speak(self, args): + """Repeats what you tell me to.""" + words = [] + for word in args.words: + if args.piglatin: + word = '%s%say' % (word[1:], word[0]) + if args.shout: + word = word.upper() + words.append(word) + repetitions = args.repeat or 1 + for _ in range(min(repetitions, self.maxrepeats)): + # .poutput handles newlines, and accommodates output redirection too + self.poutput(' '.join(words)) + + if __name__ == '__main__': + import sys + c = FirstApp() + sys.exit(c.cmdloop()) + +Lets start off on the wrong foot: + + app('speak' + print('Working') + + SyntaxError: unexpected EOF while parsing + (Cmd) run_pyscript script.py + File "", line 2 + app('speak' + ^ + SyntaxError: unexpected EOF while parsing + +cmd2 pyscripts require **valid** python code as a first step. + +!!! warning + + It is a common misconception that all application exceptions will "bubble" up from below. Unfortunately or fortunately this is not the case. CMD2 sinkholes all application exceptions and there are no means to handle them. + +When executing the `speak` command without parameters you see the following error: + + (Cmd) speak + Usage: speak [-h] [-p] [-s] [-r REPEAT] words [...] + Error: the following arguments are required: words + +Even though this is a fully qualified CMD2 error the py[script]{#script} must look for this error and perform error checking.: + + app('speak') + print("Working") + + (Cmd) run_pyscript script.py + Working + (Cmd) + +You should notice that no error message is printed. Let's utilize the `CommandResult` object to inspect the actual returned data.: + + result = app('speak') + print(result) + + (Cmd) run_pyscript script.py + CommandResult(stdout='', stderr='Usage: speak [-h] [-p] [-s] [-r REPEAT] words [...]\nError: the following arguments are required: words\n\n', stop=False, data=None) + +Now we can see that there has been an error. Let's re write the script to perform error checking.: + + result = app('speak') + + if not result: + print(result.stderr) + + (Cmd) run_pyscript script.py + Something went wrong + +In python development is good practice to fail and exit quickly after user input.: + + import sys + + result = app('speak TRUTH!!') + + if not result: + print("Something went wrong") + sys.exit() + + print("Continuing along..") + + (Cmd) run_pyscript script.py + Continuing along.. + +We changed the input to be a valid `speak` command but no output. Again we must inspect the `CommandResult`: + + import sys + + #Syntax error + result = app('speak TRUTH!!!') + if not result: + print("Something went wrong") + sys.exit() + + print(result.stdout) + + (Cmd) run_pyscript script.py + TRUTH!!! + +By just using `stdout` and `stderr` it is possible to string together commands with rudimentary control flow. In the next section we will show how to take advantage of cmd[result]{#result} data. + +### Developing an Advanced API + +Until now the application designer has paid little attention to scripters and their needs. Wouldn't it be nice if while creating py[scripts]{#scripts} one did not have to parse data from `stdout`? We can accommodate the weary scripter by adding one small line at the end of our `do_*` commands. + +`self.last_result = ` + +Adding the above line supercharges a cmd2 application and opens a new world of possibilities. + +!!! note + + When setting results for a command function inside of a CommandSet use the private cmd instance: + + self._cmd.last_result = + +In the following command example we return an array containing directory elements.: + + dir_parser = cmd2.Cmd2ArgumentParser() + dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") + + @cmd2.with_argparser(dir_parser, with_unknown_args=True) + def do_dir(self, args, unknown): + """List contents of current directory.""" + # No arguments for this command + if unknown: + self.perror("dir does not take any positional arguments:") + self.do_help('dir') + return + + # Get the contents as a list + contents = os.listdir(self.cwd) + + for f in contents: + self.poutput(f'{f}') + self.poutput('') + + self.last_result = contents + +The following script retrieves the array contents.: + + result = app('dir') + print(result.data) + +Results: + + Cmd) run_pyscript script.py + ['.venv', 'app.py', 'script.py'] + +As a rule of thumb it is safer for the designer to return simple scalar types as command results instead of complex objects. If there is benefit in providing class objects designers should choose immutable over mutable types and never provide direct access to class members as this could potentially lead to violation of the [open_closed_principle](https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle). + +When possible, a dataclass is a lightweight solution perfectly suited for data manipulation. Lets dive into an example. + +The following fictitional application has two commands: `build` and `status`. We can pretend that the build action happens somewhere else in the world at an REST API endpoint and has significant computational cost. The status command for all intents and purposes will only show the current status of a build task. The application has provided all that is needed for a user to start a build and then determine it's status. The problem however is that with a long running process the user may want to wait for it to finish. A designer may be tempted to create a command to start a build and then poll for status until finished but this scenario is better solved as an extensible script. + +app.py: + + #!/usr/bin/env python + """A simple cmd2 application.""" + import sys + from dataclasses import dataclass + from random import choice, randint + from typing import Optional + + import cmd2 + from cmd2.parsing import Statement + + + @dataclass(frozen=True) + class BuildStatus: + id: int + name: str + status: str + + + class FirstApp(cmd2.Cmd): + """A simple cmd2 application.""" + + def __init__(self): + self._status_cache = dict() + + def _start_build(self, name: str) -> BuildStatus: + return BuildStatus(randint(10, 100), name, "Started") + + def _get_status(self, name: str) -> Optional[BuildStatus]: + + status = self._status_cache.get(name) + + status_types = ["canceled", "restarted", "error", "finished"] + + if status.status != "finished": + status = BuildStatus(status.id, status.name, choice(status_types)) + self._status_cache[name] = status + + return status + + build_parser = cmd2.Cmd2ArgumentParser() + build_parser.add_argument("name", help="Name of build to start") + + @cmd2.with_argparser(build_parser) + def do_build(self, args: Statement): + """Executes a long running process at an API endpoint""" + status = self._start_build(args.name) + self._status_cache[args.name] = status + + self.poutput( + f"Build {args.name.upper()} successfully stared with id : {status.id}" + ) + self.last_result = status + + status_parser = cmd2.Cmd2ArgumentParser() + status_parser.add_argument("name", help="Name of build determine status of") + + @cmd2.with_argparser(status_parser) + def do_status(self, args: Statement): + """Shows the current status of a build""" + + status = self._get_status(args.name) + + self.poutput(f"Status for Build: {args.name} \n {status.status}") + self.last_result = status + + + if __name__ == "__main__": + import sys + + c = FirstApp() + sys.exit(c.cmdloop()) + +The below is a possible solution via pyscript: + + import sys + import time + + # start build + result = app('build tower') + + # If there was an error then quit now + if not result: + print('Build failed') + sys.exit() + + # This is a BuildStatus dataclass object + build = result.data + + print(f"Build {build.name} : {build.status}") + + # Poll status (it would be wise to NOT hang here) + while True: + + # Perform status check + result = app('status tower') + + #error checking + if not result: + print("Unable to determine status") + break + + build_status = result.data + + # If the status shows complete then we are done + if build_status.status in ['finished', 'canceled']: + print(f"Build {build.name} has completed") + break + + print(f"Current Status: {build_status.status}") + time.sleep(1) diff --git a/docs/features/scripting.rst b/docs/features/scripting.rst deleted file mode 100644 index bb05c460c..000000000 --- a/docs/features/scripting.rst +++ /dev/null @@ -1,523 +0,0 @@ -Scripting -========= - -Operating system shells have long had the ability to execute a sequence of -commands saved in a text file. These script files make long sequences of -commands easier to repeatedly execute. ``cmd2`` supports two similar -mechanisms: command scripts and python scripts. - - -Command Scripts ---------------- - -A command script contains a sequence of commands typed at the the prompt of a -``cmd2`` based application. Unlike operating system shell scripts, command -scripts can't contain logic or loops. - - -Creating Command Scripts -~~~~~~~~~~~~~~~~~~~~~~~~ - -Command scripts can be created in several ways: - -- creating a text file using any method of your choice -- using the built-in :ref:`features/builtin_commands:edit` command to - create or edit an existing text file -- saving previously entered commands to a script file using - :ref:`history -s ` - -If you create create a text file from scratch, just include one command per -line, exactly as you would type it inside a ``cmd2`` application. - - -Running Command Scripts -~~~~~~~~~~~~~~~~~~~~~~~ - -Command script files can be executed using the built-in -:ref:`features/builtin_commands:run_script` command or the ``@`` shortcut (if -your application is using the default shortcuts). Both ASCII and UTF-8 encoded -unicode text files are supported. The -:ref:`features/builtin_commands:run_script` command supports tab completion of -file system paths. There is a variant -:ref:`features/builtin_commands:_relative_run_script` command or ``@@`` -shortcut (if using the default shortcuts) for use within a script which uses -paths relative to the first script. - - -Comments -~~~~~~~~ - -Any command line input where the first non-whitespace character is a `#` will -be treated as a comment. This means any `#` character appearing later in the -command will be treated as a literal. The same applies to a `#` in the middle -of a multiline command, even if it is the first character on a line. - -Comments are useful in scripts, but would be pointless within an interactive -session. - -:: - - (Cmd) # this is a comment - (Cmd) command # this is not a comment - - -.. _scripting-python-scripts: - -Python Scripts --------------- - -.. _arg_printer: - https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/arg_printer.py - -If you require logic flow, loops, branching, or other advanced features, you -can write a python script which executes in the context of your ``cmd2`` app. -This script is run using the :ref:`features/builtin_commands:run_pyscript` -command. Here's a simple example that uses the arg_printer_ script:: - - (Cmd) run_pyscript examples/scripts/arg_printer.py foo bar 'baz 23' - Running Python script 'arg_printer.py' which was called with 3 arguments - arg 1: 'foo' - arg 2: 'bar' - arg 3: 'baz 23' - -:ref:`features/builtin_commands:run_pyscript` supports tab completion of file -system paths, and as shown above it has the ability to pass command-line -arguments to the scripts invoked. - -Developing a CMD2 API ---------------------- - -If you as an app designer have not explicitly disabled the run_pyscript command it must be assumed -that your application is structured for use in higher level python scripting. The following sections -are meant as guidelines and highlight possible pitfalls with both production and consumption -of API functionality. For clarity when speaking of "scripter" we are referring to those writing -scripts to be run by pyscript and "designer" as the CMD2 application author. - -Basics -~~~~~~ - -Without any work on the part of the designer, a scripter can take advantage of piecing together workflows -using simple ``app`` calls. The result of a run_pyscript app call yields a ``CommandResult`` object exposing -four members: ``Stdout``, ``Stderr``, ``Stop``, and ``Data``. - -``Stdout`` and ``Stderr`` are fairly straightforward representations of normal data streams and accurately reflect -what is seen by the user during normal cmd2 interaction. ``Stop`` contains information about how the invoked -command has ended its lifecycle. Lastly ``Data`` contains any information the designer sets via ``self.last_result`` -or ``self._cmd.last_result`` if called from inside a CommandSet. - - -Python scripts executed with :ref:`features/builtin_commands:run_pyscript` can -run ``cmd2`` application commands by using the syntax:: - - app(‘command args’) - -where: - -* ``app`` is a configurable name which can be changed by setting the - :data:`cmd2.Cmd.py_bridge_name` attribute -* ``command`` and ``args`` are entered exactly like they would be entered by - a user of your application. - -Using fstrings tends to be the most straight forward and easily readable way to -provide parameters.:: - - first = 'first' - second = 'second' - - app(f'command {first} -t {second}) - - -See python_scripting_ example and associated conditional_ script for more -information. - - -Design principles -~~~~~~~~~~~~~~~~~ -If the cmd2 application follows the unix_design_philosophy_ a scriptor will have the most flexibility -to piece together workflows using different commands. If the designers' application is more complete -and less likely to be augmented in the future a scripter may opt for simple serial scripts with little -control flow. In either case, choices made by the designer will have effects on scripters. - -The following diagram illustrates the different boundaries to keep in mind. - - -:: - - +---------------------------------------------+ - | | - | Py scripts | - | | - | +-----------------------------------------+ | - | | CMD2 Application | | - | | | | - | | +-------------------------------------+ | | - | | | Class Library | | | - | | | +------+ +------+ +------+ +------+ | | | - | | | | | | | | | | | | | | - | | | | C | | C | | C | | C | | | | - | | | | | | | | | | | | | | - | | | +------+ +------+ +------+ +------+ | | | - | | | | | | - | | +-------------------------------------+ | | - | | | | - | +-----------------------------------------+ | - | | - +---------------------------------------------+ - -.. note:: - - As a designer it is preferable to design from the inside to out. Your code will be - infinitely far easier to unit test than at the higher level. While there are - regression testing extensions for cmd2 UnitTesting will always be faster for development. - -.. warning:: - - It is bad design or a high level py_script to know about let alone access low level class - libraries of an application. Resist this urge at all costs, unless it's necessary. - -Developing a Basic API -~~~~~~~~~~~~~~~~~~~~~~ - -CMD2 out of the box allows scripters to take advantage of all exposed ``do_*`` commands. As a -scripter one can easily interact with the application via ``stdout`` and ``stderr``. - -As a baseline lets start off with the familiar FirstApp - -:: - - #!/usr/bin/env python - """A simple cmd2 application.""" - import cmd2 - - - class FirstApp(cmd2.Cmd): - """A simple cmd2 application.""" - def __init__(self): - shortcuts = cmd2.DEFAULT_SHORTCUTS - shortcuts.update({'&': 'speak'}) - super().__init__(shortcuts=shortcuts) - - # Make maxrepeats settable at runtime - self.maxrepeats = 3 - self.add_settable(cmd2.Settable('maxrepeats', int, 'max repetitions for speak command', self)) - - - speak_parser = cmd2.Cmd2ArgumentParser() - speak_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') - speak_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') - speak_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') - speak_parser.add_argument('words', nargs='+', help='words to say') - - @cmd2.with_argparser(speak_parser) - def do_speak(self, args): - """Repeats what you tell me to.""" - words = [] - for word in args.words: - if args.piglatin: - word = '%s%say' % (word[1:], word[0]) - if args.shout: - word = word.upper() - words.append(word) - repetitions = args.repeat or 1 - for _ in range(min(repetitions, self.maxrepeats)): - # .poutput handles newlines, and accommodates output redirection too - self.poutput(' '.join(words)) - - if __name__ == '__main__': - import sys - c = FirstApp() - sys.exit(c.cmdloop()) - - -Lets start off on the wrong foot:: - - app('speak' - print('Working') - -:: - - SyntaxError: unexpected EOF while parsing - (Cmd) run_pyscript script.py - File "", line 2 - app('speak' - ^ - SyntaxError: unexpected EOF while parsing - -cmd2 pyscripts require **valid** python code as a first step. - -.. warning:: - - It is a common misconception that all application exceptions will "bubble" up from below. Unfortunately or fortunately - this is not the case. CMD2 sinkholes all application exceptions and there are no means to handle them. - - -When executing the ``speak`` command without parameters you see the following error:: - - (Cmd) speak - Usage: speak [-h] [-p] [-s] [-r REPEAT] words [...] - Error: the following arguments are required: words - -Even though this is a fully qualified CMD2 error the py_script must look for this error and perform error checking.:: - - app('speak') - print("Working") - -:: - - (Cmd) run_pyscript script.py - Working - (Cmd) - -You should notice that no error message is printed. Let's utilize the ``CommandResult`` -object to inspect the actual returned data.:: - - result = app('speak') - print(result) - -:: - - (Cmd) run_pyscript script.py - CommandResult(stdout='', stderr='Usage: speak [-h] [-p] [-s] [-r REPEAT] words [...]\nError: the following arguments are required: words\n\n', stop=False, data=None) - -Now we can see that there has been an error. Let's re write the script to perform error checking.:: - - result = app('speak') - - if not result: - print(result.stderr) - -:: - - (Cmd) run_pyscript script.py - Something went wrong - -In python development is good practice to fail and exit quickly after user input.:: - - import sys - - result = app('speak TRUTH!!') - - if not result: - print("Something went wrong") - sys.exit() - - print("Continuing along..") - -:: - - (Cmd) run_pyscript script.py - Continuing along.. - -We changed the input to be a valid ``speak`` command but no output. Again we must inspect the -``CommandResult``:: - - import sys - - #Syntax error - result = app('speak TRUTH!!!') - if not result: - print("Something went wrong") - sys.exit() - - print(result.stdout) - -:: - - (Cmd) run_pyscript script.py - TRUTH!!! - -By just using ``stdout`` and ``stderr`` it is possible to string together commands -with rudimentary control flow. In the next section we will show how to take advantage of -cmd_result data. - -Developing an Advanced API -~~~~~~~~~~~~~~~~~~~~~~~~~~ - -Until now the application designer has paid little attention to scripters and their needs. -Wouldn't it be nice if while creating py_scripts one did not have to parse data from ``stdout``? We can -accommodate the weary scripter by adding one small line at the end of our ``do_*`` commands. - -``self.last_result = `` - -Adding the above line supercharges a cmd2 application and opens a new world of possibilities. - -.. note:: - - When setting results for a command function inside of a CommandSet use the private cmd instance:: - - self._cmd.last_result = - - -In the following command example we return an array containing directory elements.:: - - dir_parser = cmd2.Cmd2ArgumentParser() - dir_parser.add_argument('-l', '--long', action='store_true', help="display in long format with one item per line") - - @cmd2.with_argparser(dir_parser, with_unknown_args=True) - def do_dir(self, args, unknown): - """List contents of current directory.""" - # No arguments for this command - if unknown: - self.perror("dir does not take any positional arguments:") - self.do_help('dir') - return - - # Get the contents as a list - contents = os.listdir(self.cwd) - - for f in contents: - self.poutput(f'{f}') - self.poutput('') - - self.last_result = contents - -The following script retrieves the array contents.:: - - result = app('dir') - print(result.data) - -Results:: - - Cmd) run_pyscript script.py - ['.venv', 'app.py', 'script.py'] - -As a rule of thumb it is safer for the designer to return simple scalar types as command results instead of complex -objects. If there is benefit in providing class objects designers should choose immutable over mutable types and never -provide direct access to class members as this could potentially lead to violation of the open_closed_principle_. - -When possible, a dataclass is a lightweight solution perfectly suited for data manipulation. Lets dive into an -example. - -The following fictitional application has two commands: ``build`` and ``status``. We can pretend that the build action -happens somewhere else in the world at an REST API endpoint and has significant computational cost. The status command -for all intents and purposes will only show the current status of a build task. The application has provided all that is -needed for a user to start a build and then determine it's status. The problem however is that with a long running -process the user may want to wait for it to finish. A designer may be tempted to create a command to start a build and -then poll for status until finished but this scenario is better solved as an extensible script. - -app.py:: - - #!/usr/bin/env python - """A simple cmd2 application.""" - import sys - from dataclasses import dataclass - from random import choice, randint - from typing import Optional - - import cmd2 - from cmd2.parsing import Statement - - - @dataclass(frozen=True) - class BuildStatus: - id: int - name: str - status: str - - - class FirstApp(cmd2.Cmd): - """A simple cmd2 application.""" - - def __init__(self): - self._status_cache = dict() - - def _start_build(self, name: str) -> BuildStatus: - return BuildStatus(randint(10, 100), name, "Started") - - def _get_status(self, name: str) -> Optional[BuildStatus]: - - status = self._status_cache.get(name) - - status_types = ["canceled", "restarted", "error", "finished"] - - if status.status != "finished": - status = BuildStatus(status.id, status.name, choice(status_types)) - self._status_cache[name] = status - - return status - - build_parser = cmd2.Cmd2ArgumentParser() - build_parser.add_argument("name", help="Name of build to start") - - @cmd2.with_argparser(build_parser) - def do_build(self, args: Statement): - """Executes a long running process at an API endpoint""" - status = self._start_build(args.name) - self._status_cache[args.name] = status - - self.poutput( - f"Build {args.name.upper()} successfully stared with id : {status.id}" - ) - self.last_result = status - - status_parser = cmd2.Cmd2ArgumentParser() - status_parser.add_argument("name", help="Name of build determine status of") - - @cmd2.with_argparser(status_parser) - def do_status(self, args: Statement): - """Shows the current status of a build""" - - status = self._get_status(args.name) - - self.poutput(f"Status for Build: {args.name} \n {status.status}") - self.last_result = status - - - if __name__ == "__main__": - import sys - - c = FirstApp() - sys.exit(c.cmdloop()) - - -The below is a possible solution via pyscript:: - - import sys - import time - - # start build - result = app('build tower') - - # If there was an error then quit now - if not result: - print('Build failed') - sys.exit() - - # This is a BuildStatus dataclass object - build = result.data - - print(f"Build {build.name} : {build.status}") - - # Poll status (it would be wise to NOT hang here) - while True: - - # Perform status check - result = app('status tower') - - #error checking - if not result: - print("Unable to determine status") - break - - build_status = result.data - - # If the status shows complete then we are done - if build_status.status in ['finished', 'canceled']: - print(f"Build {build.name} has completed") - break - - print(f"Current Status: {build_status.status}") - time.sleep(1) - - -.. _python_scripting: - https://github.com/python-cmd2/cmd2/blob/master/examples/python_scripting.py - -.. _conditional: - https://github.com/python-cmd2/cmd2/blob/master/examples/scripts/conditional.py - -.. _unix_design_philosophy: - https://en.wikipedia.org/wiki/Unix_philosophy - -.. _open_closed_principle: - https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle diff --git a/docs/features/settings.md b/docs/features/settings.md new file mode 100644 index 000000000..381d05a56 --- /dev/null +++ b/docs/features/settings.md @@ -0,0 +1,116 @@ +# Settings + +Settings provide a mechanism for a user to control the behavior of a `cmd2` based application. A setting is stored in an instance attribute on your subclass of `cmd2.Cmd`{.interpreted-text role="class"} and must also appear in the `cmd2.Cmd.settable`{.interpreted-text role="attr"} dictionary. Developers may set default values for these settings and users can modify them at runtime using the `features/builtin_commands:set`{.interpreted-text role="ref"} command. Developers can `features/settings:Create New Settings`{.interpreted-text role="ref"} and can also `features/settings:Hide Builtin Settings`{.interpreted-text role="ref"} from the user. + +## Builtin Settings + +`cmd2` has a number of builtin settings. These settings control the behavior of certain application features and `features/builtin_commands:Builtin +Commands`{.interpreted-text role="ref"}. Users can use the `features/builtin_commands:set`{.interpreted-text role="ref"} command to show all settings and to modify the value of any setting. + +### allow[style]{#style} + +Output generated by `cmd2` programs may contain ANSI escape sequences which instruct the terminal to apply colors or text styling (i.e. bold) to the output. The `allow_style` setting controls the behavior of these escape sequences in output generated with any of the following methods: + +- `cmd2.Cmd.poutput`{.interpreted-text role="meth"} +- `cmd2.Cmd.perror`{.interpreted-text role="meth"} +- `cmd2.Cmd.pwarning`{.interpreted-text role="meth"} +- `cmd2.Cmd.pexcept`{.interpreted-text role="meth"} +- `cmd2.Cmd.pfeedback`{.interpreted-text role="meth"} +- `cmd2.Cmd.ppaged`{.interpreted-text role="meth"} + +This setting can be one of three values: + +- `Never` - all ANSI escape sequences which instruct the terminal to style output are stripped from the output. +- `Terminal` - (the default value) pass through ANSI escape sequences when the output is being sent to the terminal, but if the output is redirected to a pipe or a file the escape sequences are stripped. +- `Always` - ANSI escape sequences are always passed through to the output + +### always[show_hint]{#show_hint} + +If `True`, display tab completion hint even when completion suggestions print. The default value of this setting is `False`. + +### debug + +The default value of this setting is `False`, which causes the `~cmd2.Cmd.pexcept`{.interpreted-text role="meth"} method to only display the message from an exception. However, if the debug setting is `True`, then the entire stack trace will be printed. + +### echo + +If `True`, each command the user issues will be repeated to the screen before it is executed. This is particularly useful when running scripts. This behavior does not occur when running a command at the prompt. + +### editor + +Similar to the `EDITOR` shell variable, this setting contains the name of the program which should be run by the `features/builtin_commands:edit`{.interpreted-text role="ref"} command. + +### feedback[to_output]{#to_output} + +Controls whether feedback generated with the `~cmd2.Cmd.pfeedback`{.interpreted-text role="meth"} method is sent to `sys.stdout` or `sys.stderr`. If `False` the output will be sent to `sys.stderr` + +If `True` the output is sent to `stdout` (which is often the screen but may be `redirected `{.interpreted-text role="ref"}). The feedback output will be mixed in with and indistinguishable from output generated with `~cmd2.Cmd.poutput`{.interpreted-text role="meth"}. + +### max[completion_items]{#completion_items} + +Maximum number of CompletionItems to display during tab completion. A CompletionItem is a special kind of tab completion hint which displays both a value and description and uses one line for each hint. Tab complete the `set` command for an example. + +If the number of tab completion hints exceeds `max_completion_items`, then they will be displayed in the typical columnized format and will not include the description text of the CompletionItem. + +### quiet + +If `True`, output generated by calling `~cmd2.Cmd.pfeedback`{.interpreted-text role="meth"} is suppressed. If `False`, the `features/settings:feedback_to_output`{.interpreted-text role="ref"} setting controls where the output is sent. + +### scripts[add_to_history]{#add_to_history} + +If `True`, scripts and pyscripts add commands to history. The default value of this setting is `True`. + +### timing + +If `True`, the elapsed time is reported for each command executed. + +## Create New Settings + +Your application can define user-settable parameters which your code can reference. In your initialization code: + +1. Create an instance attribute with a default value. +2. Create a `.Settable`{.interpreted-text role="class"} object which describes your setting. +3. Pass the `.Settable`{.interpreted-text role="class"} object to `cmd2.Cmd.add_settable`{.interpreted-text role="meth"}. + +Here's an example, from `examples/environment.py`: + +~~~ +{% + include "../../examples/environment.py" +%} +~~~ + +If you want to be notified when a setting changes (as we do above), then be sure to supply a method to the `onchange_cb` parameter of the ``.cmd2.utils.Settable``. This method will be called after the user changes a setting, and will receive both the old value and the new value. + +``` text +(Cmd) set | grep sunny +sunny False Is it sunny outside? +(Cmd) set | grep degrees +degrees_c 22 Temperature in Celsius +(Cmd) sunbathe +Too dim. +(Cmd) set degrees_c 41 +degrees_c - was: 22 +now: 41 +(Cmd) set sunny +sunny: True +(Cmd) sunbathe +UV is bad for your skin. +(Cmd) set degrees_c 13 +degrees_c - was: 41 +now: 13 +(Cmd) sunbathe +It's 13 C - are you a penguin? +``` + +## Hide Builtin Settings + +You may want to prevent a user from modifying a builtin setting. A setting must appear in the `cmd2.Cmd.settable`{.interpreted-text role="attr"} dictionary in order for it to be available to the `features/builtin_commands:set`{.interpreted-text role="ref"} command. + +Let's say that you never want end users of your program to be able to enable full debug tracebacks to print out if an error occurs. You might want to hide the `features/settings:debug`{.interpreted-text role="ref"} setting. To do so, remove it from the `cmd2.Cmd.settable`{.interpreted-text role="attr"} dictionary after you initialize your object. The `cmd2.Cmd.remove_settable`{.interpreted-text role="meth"} convenience method makes this easy: + + class MyApp(cmd2.Cmd): + + def __init__(self): + super().__init__() + self.remove_settable('debug') diff --git a/docs/features/settings.rst b/docs/features/settings.rst deleted file mode 100644 index d2a8da29e..000000000 --- a/docs/features/settings.rst +++ /dev/null @@ -1,188 +0,0 @@ -Settings -======== - -Settings provide a mechanism for a user to control the behavior of a ``cmd2`` -based application. A setting is stored in an instance attribute on your -subclass of :class:`cmd2.Cmd` and must also appear in the -:attr:`cmd2.Cmd.settable` dictionary. Developers may set default values -for these settings and users can modify them at runtime using the -:ref:`features/builtin_commands:set` command. Developers can -:ref:`features/settings:Create New Settings` and can also -:ref:`features/settings:Hide Builtin Settings` from the user. - - -Builtin Settings ------------------ - -``cmd2`` has a number of builtin settings. These settings control the behavior -of certain application features and :ref:`features/builtin_commands:Builtin -Commands`. Users can use the :ref:`features/builtin_commands:set` command to -show all settings and to modify the value of any setting. - - -allow_style -~~~~~~~~~~~ - -Output generated by ``cmd2`` programs may contain ANSI escape sequences which -instruct the terminal to apply colors or text styling (i.e. bold) to the -output. The ``allow_style`` setting controls the behavior of these escape -sequences in output generated with any of the following methods: - -- :meth:`cmd2.Cmd.poutput` -- :meth:`cmd2.Cmd.perror` -- :meth:`cmd2.Cmd.pwarning` -- :meth:`cmd2.Cmd.pexcept` -- :meth:`cmd2.Cmd.pfeedback` -- :meth:`cmd2.Cmd.ppaged` - -This setting can be one of three values: - -- ``Never`` - all ANSI escape sequences which instruct the terminal to style - output are stripped from the output. - -- ``Terminal`` - (the default value) pass through ANSI escape sequences when - the output is being sent to the terminal, but if the output is redirected to - a pipe or a file the escape sequences are stripped. - -- ``Always`` - ANSI escape sequences are always passed through to the output - - -always_show_hint -~~~~~~~~~~~~~~~~ - -If ``True``, display tab completion hint even when completion suggestions print. -The default value of this setting is ``False``. - - -debug -~~~~~ - -The default value of this setting is ``False``, which causes the -:meth:`~cmd2.Cmd.pexcept` method to only display the message from an -exception. However, if the debug setting is ``True``, then the entire stack -trace will be printed. - - -echo -~~~~ - -If ``True``, each command the user issues will be repeated to the screen -before it is executed. This is particularly useful when running scripts. -This behavior does not occur when running a command at the prompt. - - -editor -~~~~~~ - -Similar to the ``EDITOR`` shell variable, this setting contains the name of the -program which should be run by the :ref:`features/builtin_commands:edit` -command. - - -feedback_to_output -~~~~~~~~~~~~~~~~~~ - -Controls whether feedback generated with the :meth:`~cmd2.Cmd.pfeedback` -method is sent to ``sys.stdout`` or ``sys.stderr``. If ``False`` the output -will be sent to ``sys.stderr`` - -If ``True`` the output is sent to ``stdout`` (which is often the screen but may -be :ref:`redirected `). The -feedback output will be mixed in with and indistinguishable from output -generated with :meth:`~cmd2.Cmd.poutput`. - - -max_completion_items -~~~~~~~~~~~~~~~~~~~~ - -Maximum number of CompletionItems to display during tab completion. A -CompletionItem is a special kind of tab completion hint which displays both a -value and description and uses one line for each hint. Tab complete the ``set`` -command for an example. - -If the number of tab completion hints exceeds ``max_completion_items``, then -they will be displayed in the typical columnized format and will not include -the description text of the CompletionItem. - - -quiet -~~~~~ - -If ``True``, output generated by calling :meth:`~cmd2.Cmd.pfeedback` is -suppressed. If ``False``, the :ref:`features/settings:feedback_to_output` -setting controls where the output is sent. - - -scripts_add_to_history -~~~~~~~~~~~~~~~~~~~~~~ - -If ``True``, scripts and pyscripts add commands to history. The default value of -this setting is ``True``. - - -timing -~~~~~~ - -If ``True``, the elapsed time is reported for each command executed. - - -Create New Settings -------------------- - -Your application can define user-settable parameters which your code can -reference. In your initialization code: - -1. Create an instance attribute with a default value. -2. Create a :class:`.Settable` object which describes your setting. -3. Pass the :class:`.Settable` object to :meth:`cmd2.Cmd.add_settable`. - -Here's an example, from -``examples/environment.py``: - -.. literalinclude:: ../../examples/environment.py - -If you want to be notified when a setting changes (as we do above), then be -sure to supply a method to the ``onchange_cb`` parameter of the -`.cmd2.utils.Settable`. This method will be called after the user -changes a setting, and will receive both the old value and the new value. - -.. code-block:: text - - (Cmd) set | grep sunny - sunny False Is it sunny outside? - (Cmd) set | grep degrees - degrees_c 22 Temperature in Celsius - (Cmd) sunbathe - Too dim. - (Cmd) set degrees_c 41 - degrees_c - was: 22 - now: 41 - (Cmd) set sunny - sunny: True - (Cmd) sunbathe - UV is bad for your skin. - (Cmd) set degrees_c 13 - degrees_c - was: 41 - now: 13 - (Cmd) sunbathe - It's 13 C - are you a penguin? - - -Hide Builtin Settings ---------------------- - -You may want to prevent a user from modifying a builtin setting. A setting -must appear in the :attr:`cmd2.Cmd.settable` dictionary in order for it -to be available to the :ref:`features/builtin_commands:set` command. - -Let's say that you never want end users of your program to be able to enable -full debug tracebacks to print out if an error occurs. You might want to hide -the :ref:`features/settings:debug` setting. To do so, remove it from the -:attr:`cmd2.Cmd.settable` dictionary after you initialize your object. -The :meth:`cmd2.Cmd.remove_settable` convenience method makes this easy:: - - class MyApp(cmd2.Cmd): - - def __init__(self): - super().__init__() - self.remove_settable('debug') diff --git a/docs/features/shortcuts_aliases_macros.md b/docs/features/shortcuts_aliases_macros.md new file mode 100644 index 000000000..791888d5d --- /dev/null +++ b/docs/features/shortcuts_aliases_macros.md @@ -0,0 +1,91 @@ +# Shortcuts, Aliases, and Macros + +## Shortcuts + +Command shortcuts for long command names and common commands can make life more convenient for your users. Shortcuts are used without a space separating them from their arguments, like `!ls`. By default, the following shortcuts are defined: + +> `?` +> +> : help +> +> `!` +> +> : shell: run as OS-level command +> +> `@` +> +> : run script file +> +> `@@` +> +> : run script file; filename is relative to current script location + +To define more shortcuts, update the dict `App.shortcuts` with the {'shortcut': 'command[name]{#name}'} (omit `do_`): + + class App(Cmd): + def __init__(self): + shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) + shortcuts.update({'*': 'sneeze', '~': 'squirm'}) + cmd2.Cmd.__init__(self, shortcuts=shortcuts) + +!!! warning + + Shortcuts need to be created by updating the `shortcuts` dictionary attribute prior to calling the `cmd2.Cmd` super class `__init__()` method. Moreover, that super class init method needs to be called after updating the `shortcuts` attribute This warning applies in general to many other attributes which are not settable at runtime. + +Note: Command, alias, and macro names cannot start with a shortcut + +## Aliases + +In addition to shortcuts, `cmd2` provides a full alias feature via the `alias` command. Aliases work in a similar fashion to aliases in the Bash shell. + +The syntax to create an alias is: `alias create name command [args]`. + +> Ex: `alias create ls !ls -lF` + +Redirectors and pipes should be quoted in alias definition to prevent the `alias create` command from being redirected: + + alias create save_results print_results ">" out.txt + +Tab completion recognizes an alias, and completes as if its actual value was on the command line. + +For more details run: `help alias create` + +Use `alias list` to see all or some of your aliases. The output of this command displays your aliases using the same command that was used to create them. Therefore you can place this output in a `cmd2` startup script to recreate your aliases each time you start the application + +> Ex: `alias list` + +For more details run: `help alias list` + +Use `alias delete` to remove aliases + +For more details run: `help alias delete` + +Note: Aliases cannot have the same name as a command or macro + +## Macros + +`cmd2` provides a feature that is similar to aliases called macros. The major difference between macros and aliases is that macros can contain argument placeholders. Arguments are expressed when creating a macro using {#} notation where {1} means the first argument. + +The following creates a macro called my[macro]{#macro} that expects two arguments: + +> macro create my[macro]{#macro} make[dinner]{#dinner} -meat {1} -veggie {2} + +When the macro is called, the provided arguments are resolved and the assembled command is run. For example: + +> my[macro]{#macro} beef broccoli ---> make[dinner]{#dinner} -meat beef -veggie broccoli + +Similar to aliases, pipes and redirectors need to be quoted in the definition of a macro: + + macro create lc !cat "{1}" "|" less + +To use the literal string `{1}` in your command, escape it this way: `{{1}}`. Because macros do not resolve until after hitting ``, tab completion will only complete paths while typing a macro. + +For more details run: `help macro create` + +The macro command has `list` and `delete` subcommands that function identically to the alias subcommands of the same name. Like aliases, macros can be created via a `cmd2` startup script to preserve them across application sessions. + +For more details on listing macros run: `help macro list` + +For more details on deleting macros run: `help macro delete` + +Note: Macros cannot have the same name as a command or alias diff --git a/docs/features/shortcuts_aliases_macros.rst b/docs/features/shortcuts_aliases_macros.rst deleted file mode 100644 index 58d6d83cd..000000000 --- a/docs/features/shortcuts_aliases_macros.rst +++ /dev/null @@ -1,117 +0,0 @@ -Shortcuts, Aliases, and Macros -============================== - -Shortcuts ---------- - -Command shortcuts for long command names and common commands can make life more -convenient for your users. Shortcuts are used without a space separating them -from their arguments, like ``!ls``. By default, the following shortcuts are -defined: - - ``?`` - help - - ``!`` - shell: run as OS-level command - - ``@`` - run script file - - ``@@`` - run script file; filename is relative to current script location - -To define more shortcuts, update the dict ``App.shortcuts`` with the -{'shortcut': 'command_name'} (omit ``do_``):: - - class App(Cmd): - def __init__(self): - shortcuts = dict(cmd2.DEFAULT_SHORTCUTS) - shortcuts.update({'*': 'sneeze', '~': 'squirm'}) - cmd2.Cmd.__init__(self, shortcuts=shortcuts) - -.. warning:: - - Shortcuts need to be created by updating the ``shortcuts`` dictionary - attribute prior to calling the ``cmd2.Cmd`` super class ``__init__()`` - method. Moreover, that super class init method needs to be called after - updating the ``shortcuts`` attribute This warning applies in general to many - other attributes which are not settable at runtime. - -Note: Command, alias, and macro names cannot start with a shortcut - -Aliases -------- - -In addition to shortcuts, ``cmd2`` provides a full alias feature via the -``alias`` command. Aliases work in a similar fashion to aliases in the Bash -shell. - -The syntax to create an alias is: ``alias create name command [args]``. - - Ex: ``alias create ls !ls -lF`` - -Redirectors and pipes should be quoted in alias definition to prevent the -``alias create`` command from being redirected:: - - alias create save_results print_results ">" out.txt - -Tab completion recognizes an alias, and completes as if its actual value -was on the command line. - -For more details run: ``help alias create`` - -Use ``alias list`` to see all or some of your aliases. The output of this -command displays your aliases using the same command that was used to create -them. Therefore you can place this output in a ``cmd2`` startup script to -recreate your aliases each time you start the application - - Ex: ``alias list`` - -For more details run: ``help alias list`` - -Use ``alias delete`` to remove aliases - -For more details run: ``help alias delete`` - -Note: Aliases cannot have the same name as a command or macro - -Macros ------- - -``cmd2`` provides a feature that is similar to aliases called macros. The major -difference between macros and aliases is that macros can contain argument -placeholders. Arguments are expressed when creating a macro using {#} notation -where {1} means the first argument. - -The following creates a macro called my_macro that expects two arguments: - - macro create my_macro make_dinner -meat {1} -veggie {2} - -When the macro is called, the provided arguments are resolved and the assembled -command is run. For example: - - my_macro beef broccoli ---> make_dinner -meat beef -veggie broccoli - -Similar to aliases, pipes and redirectors need to be quoted in the definition -of a macro:: - - macro create lc !cat "{1}" "|" less - -To use the literal string ``{1}`` in your command, escape it this way: -``{{1}}``. Because macros do not resolve until after hitting ````, -tab completion will only complete paths while typing a macro. - - -For more details run: ``help macro create`` - -The macro command has ``list`` and ``delete`` subcommands that function -identically to the alias subcommands of the same name. Like aliases, macros can -be created via a ``cmd2`` startup script to preserve them across application -sessions. - -For more details on listing macros run: ``help macro list`` - -For more details on deleting macros run: ``help macro delete`` - -Note: Macros cannot have the same name as a command or alias diff --git a/docs/features/startup_commands.md b/docs/features/startup_commands.md new file mode 100644 index 000000000..e438bb262 --- /dev/null +++ b/docs/features/startup_commands.md @@ -0,0 +1,46 @@ +# Startup Commands + +`cmd2` provides a couple different ways for running commands immediately after your application starts up: + +1. Commands at Invocation +2. Startup Script + +Commands run as part of a startup script are always run immediately after the application finishes initializing so they are guaranteed to run before any _Commands At Invocation_. + +## Commands At Invocation + +You can send commands to your app as you invoke it by including them as extra arguments to the program. `cmd2` interprets each argument as a separate command, so you should enclose each command in quotation marks if it is more than a one-word command. You can use either single or double quotes for this purpose. + +```shell +$ python examples/example.py "say hello" "say Gracie" quit +hello +Gracie +``` + +You can end your commands with a **quit** command so that your `cmd2` application runs like a non-interactive command-line utility (CLU). This means that it can then be scripted from an external application and easily used in automation. + +!!! note + + If you wish to disable cmd2's consumption of command-line arguments, you can do so by setting the `allow_cli_args` argument of your `cmd2.Cmd` class instance to `False`. This would be useful, for example, if you wish to use something like [Argparse](https://docs.python.org/3/library/argparse.html) to parse the overall command line arguments for your application: + + from cmd2 import Cmd + class App(Cmd): + def __init__(self): + super().__init__(allow_cli_args=False) + +## Startup Script + +You can execute commands from an initialization script by passing a file path to the `startup_script` argument to the `cmd2.Cmd.__init__()` method like so: + + class StartupApp(cmd2.Cmd): + def __init__(self): + cmd2.Cmd.__init__(self, startup_script='.cmd2rc') + +This text file should contain a `Command Script +`{.interpreted-text role="ref"}. See the [AliasStartup](https://github.com/python-cmd2/cmd2/blob/master/examples/alias_startup.py) example for a demonstration. + +You can silence a startup script's output by setting `silence_startup_script` to True: + + cmd2.Cmd.__init__(self, startup_script='.cmd2rc', silence_startup_script=True) + +Anything written to stderr will still print. Additionally, a startup script cannot be silenced if `allow_redirection` is False since silencing works by redirecting a script's output to `os.devnull`. diff --git a/docs/features/startup_commands.rst b/docs/features/startup_commands.rst deleted file mode 100644 index 951e7fffc..000000000 --- a/docs/features/startup_commands.rst +++ /dev/null @@ -1,75 +0,0 @@ -Startup Commands -================ - -``cmd2`` provides a couple different ways for running commands immediately -after your application starts up: - -1. Commands at Invocation -2. Startup Script - -Commands run as part of a startup script are always run immediately after the -application finishes initializing so they are guaranteed to run before any -*Commands At Invocation*. - - -Commands At Invocation ----------------------- - -.. _Argparse: https://docs.python.org/3/library/argparse.html - -You can send commands to your app as you invoke it by including them as extra -arguments to the program. ``cmd2`` interprets each argument as a separate -command, so you should enclose each command in quotation marks if it is more -than a one-word command. You can use either single or double quotes for this -purpose. - -.. code-block:: shell - - $ python examples/example.py "say hello" "say Gracie" quit - hello - Gracie - -You can end your commands with a **quit** command so that your ``cmd2`` -application runs like a non-interactive command-line utility (CLU). This -means that it can then be scripted from an external application and easily used -in automation. - -.. note:: - - If you wish to disable cmd2's consumption of command-line arguments, you can - do so by setting the ``allow_cli_args`` argument of your ``cmd2.Cmd`` class - instance to ``False``. This would be useful, for example, if you wish to - use something like Argparse_ to parse the overall command line arguments for - your application:: - - from cmd2 import Cmd - class App(Cmd): - def __init__(self): - super().__init__(allow_cli_args=False) - - -Startup Script --------------- - -.. _AliasStartup: https://github.com/python-cmd2/cmd2/blob/master/examples/alias_startup.py - -You can execute commands from an initialization script by passing a file -path to the ``startup_script`` argument to the ``cmd2.Cmd.__init__()`` method -like so:: - - class StartupApp(cmd2.Cmd): - def __init__(self): - cmd2.Cmd.__init__(self, startup_script='.cmd2rc') - -This text file should contain a :ref:`Command Script -`. See the AliasStartup_ example for a -demonstration. - -You can silence a startup script's output by setting ``silence_startup_script`` -to True:: - - cmd2.Cmd.__init__(self, startup_script='.cmd2rc', silence_startup_script=True) - -Anything written to stderr will still print. Additionally, a startup script -cannot be silenced if ``allow_redirection`` is False since silencing works -by redirecting a script's output to ``os.devnull``. diff --git a/docs/features/table_creation.md b/docs/features/table_creation.md new file mode 100644 index 000000000..9b32ccdc3 --- /dev/null +++ b/docs/features/table_creation.md @@ -0,0 +1,17 @@ +# Table Creation + +`cmd2` provides a table creation class called `cmd2.table_creator.TableCreator`{.interpreted-text role="attr"}. This class handles ANSI style sequences and characters with display widths greater than 1 when performing width calculations. It was designed with the ability to build tables one row at a time. This helps when you have large data sets that you don't want to hold in memory or when you receive portions of the data set incrementally. + +`TableCreator` has one public method: `cmd2.table_creator.TableCreator.generate_row()`{.interpreted-text role="attr"} + +This function and the `cmd2.table_creator.Column`{.interpreted-text role="attr"} class provide all features needed to build tables with headers, borders, colors, horizontal and vertical alignment, and wrapped text. However, it's generally easier to inherit from this class and implement a more granular API rather than use `TableCreator` directly. + +The following table classes build upon `TableCreator` and are provided in the `api/table_creator:cmd2.table_creator`{.interpreted-text role="ref"} module. They can be used as is or as examples for how to build your own table classes. + +`cmd2.table_creator.SimpleTable`{.interpreted-text role="attr"} - Implementation of TableCreator which generates a borderless table with an optional divider row after the header. This class can be used to create the whole table at once or one row at a time. + +`cmd2.table_creator.BorderedTable`{.interpreted-text role="attr"} - Implementation of TableCreator which generates a table with borders around the table and between rows. Borders between columns can also be toggled. This class can be used to create the whole table at once or one row at a time. + +`cmd2.table_creator.AlternatingTable`{.interpreted-text role="attr"} - Implementation of BorderedTable which uses background colors to distinguish between rows instead of row border lines. This class can be used to create the whole table at once or one row at a time. + +See the [table_creation](https://github.com/python-cmd2/cmd2/blob/master/examples/table_creation.py) example to see these classes in use diff --git a/docs/features/table_creation.rst b/docs/features/table_creation.rst deleted file mode 100644 index 44095fd9d..000000000 --- a/docs/features/table_creation.rst +++ /dev/null @@ -1,40 +0,0 @@ -Table Creation -============== - -``cmd2`` provides a table creation class called -:attr:`cmd2.table_creator.TableCreator`. This class handles ANSI style -sequences and characters with display widths greater than 1 when performing -width calculations. It was designed with the ability to build tables one row at -a time. This helps when you have large data sets that you don't want to hold -in memory or when you receive portions of the data set incrementally. - -``TableCreator`` has one public method: -:attr:`cmd2.table_creator.TableCreator.generate_row()` - -This function and the :attr:`cmd2.table_creator.Column` -class provide all features needed to build tables with headers, borders, -colors, horizontal and vertical alignment, and wrapped text. However, it's -generally easier to inherit from this class and implement a more granular API -rather than use ``TableCreator`` directly. - -The following table classes build upon ``TableCreator`` and are provided in -the :ref:`api/table_creator:cmd2.table_creator` module. They can be used as is -or as examples for how to build your own table classes. - -:attr:`cmd2.table_creator.SimpleTable` - Implementation of TableCreator which -generates a borderless table with an optional divider row after the header. -This class can be used to create the whole table at once or one row at a time. - -:attr:`cmd2.table_creator.BorderedTable` - Implementation of TableCreator which -generates a table with borders around the table and between rows. Borders -between columns can also be toggled. This class can be used to create the whole -table at once or one row at a time. - -:attr:`cmd2.table_creator.AlternatingTable` - Implementation of BorderedTable -which uses background colors to distinguish between rows instead of row border -lines. This class can be used to create the whole table at once or one row at a -time. - -See the table_creation_ example to see these classes in use - -.. _table_creation: https://github.com/python-cmd2/cmd2/blob/master/examples/table_creation.py diff --git a/docs/features/transcripts.md b/docs/features/transcripts.md new file mode 100644 index 000000000..eea17826b --- /dev/null +++ b/docs/features/transcripts.md @@ -0,0 +1,159 @@ +# Transcripts + +A transcript is both the input and output of a successful session of a `cmd2`-based app which is saved to a text file. With no extra work on your part, your app can play back these transcripts as a unit test. Transcripts can contain regular expressions, which provide the flexibility to match responses from commands that produce dynamic or variable output. + +## Creating From History + +A transcript can automatically generated based upon commands previously executed in the _history_ using `history -t`: + +```none +(Cmd) help +... +(Cmd) help history +... +(Cmd) history 1:2 -t transcript.txt +2 commands and outputs saved to transcript file 'transcript.txt' +``` + +This is by far the easiest way to generate a transcript. + +!!! warning + +Make sure you use the **poutput()** method in your `cmd2` application for generating command output. This method of the `cmd2.Cmd` class ensure that output is properly redirected when redirecting to a file, piping to a shell command, and when generating a transcript. + +## Creating From A Script File + +A transcript can also be automatically generated from a script file using `run_script -t`: + +```none +(Cmd) run_script scripts/script.txt -t transcript.txt +2 commands and their outputs saved to transcript file 'transcript.txt' +(Cmd) +``` + +This is a particularly attractive option for automatically regenerating transcripts for regression testing as your `cmd2` application changes. + +## Creating Manually + +Here's a transcript created from `python examples/example.py`: + +```none +(Cmd) say -r 3 Goodnight, Gracie +Goodnight, Gracie +Goodnight, Gracie +Goodnight, Gracie +(Cmd) mumble maybe we could go to lunch +like maybe we ... could go to hmmm lunch +(Cmd) mumble maybe we could go to lunch +well maybe we could like go to er lunch right? +``` + +This transcript has three commands: they are on the lines that begin with the prompt. The first command looks like this: + +```none +(Cmd) say -r 3 Goodnight, Gracie +``` + +Following each command is the output generated by that command. + +The transcript ignores all lines in the file until it reaches the first line that begins with the prompt. You can take advantage of this by using the first lines of the transcript as comments: + +```none +# Lines at the beginning of the transcript that do not +; start with the prompt i.e. '(Cmd) ' are ignored. +/* You can use them for comments. */ + +All six of these lines before the first prompt are treated as comments. + +(Cmd) say -r 3 Goodnight, Gracie +Goodnight, Gracie +Goodnight, Gracie +Goodnight, Gracie +(Cmd) mumble maybe we could go to lunch +like maybe we ... could go to hmmm lunch +(Cmd) mumble maybe we could go to lunch +maybe we could like go to er lunch right? +``` + +In this example I've used several different commenting styles, and even bare text. It doesn't matter what you put on those beginning lines. Everything before: + +```none +(Cmd) say -r 3 Goodnight, Gracie +``` + +will be ignored. + +## Regular Expressions + +If we used the above transcript as-is, it would likely fail. As you can see, the `mumble` command doesn't always return the same thing: it inserts random words into the input. + +Regular expressions can be included in the response portion of a transcript, and are surrounded by slashes: + +```none +(Cmd) mumble maybe we could go to lunch +/.*\bmaybe\b.*\bcould\b.*\blunch\b.*/ +(Cmd) mumble maybe we could go to lunch +/.*\bmaybe\b.*\bcould\b.*\blunch\b.*/ +``` + +Without creating a tutorial on regular expressions, this one matches anything that has the words `maybe`, `could`, and `lunch` in that order. It doesn't ensure that `we` or `go` or `to` appear in the output, but it does work if mumble happens to add words to the beginning or the end of the output. + +Since the output could be multiple lines long, `cmd2` uses multiline regular expression matching, and also uses the `DOTALL` flag. These two flags subtly change the behavior of commonly used special characters like `.`, `^` and `$`, so you may want to double check the [Python regular expression documentation](https://docs.python.org/3/library/re.html). + +If your output has slashes in it, you will need to escape those slashes so the stuff between them is not interpred as a regular expression. In this transcript: + +```none +(Cmd) say cd /usr/local/lib/python3.11/site-packages +/usr/local/lib/python3.11/site-packages +``` + +the output contains slashes. The text between the first slash and the second slash, will be interpreted as a regular expression, and those two slashes will not be included in the comparison. When replayed, this transcript would therefore fail. To fix it, we could either write a regular expression to match the path instead of specifying it verbatim, or we can escape the slashes: + +```none +(Cmd) say cd /usr/local/lib/python3.11/site-packages +\/usr\/local\/lib\/python3.11\/site-packages +``` + +!!! warning + +Be aware of trailing spaces and newlines. Your commands might output trailing spaces which are impossible to see. Instead of leaving them invisible, you can add a regular expression to match them, so that you can see where they are when you look at the transcript: + +```none +(Cmd) set editor +editor: vim/ / +``` + +Some terminal emulators strip trailing space when you copy text from them. This could make the actual data generated by your app different than the text you pasted into the transcript, and it might not be readily obvious why the transcript is not passing. Consider using `features/redirection:Output Redirection and Pipes`{.interpreted-text role="ref"} to the clipboard or to a file to ensure you accurately capture the output of your command. + +If you aren't using regular expressions, make sure the newlines at the end of your transcript exactly match the output of your commands. A common cause of a failing transcript is an extra or missing newline. + +If you are using regular expressions, be aware that depending on how you write your regex, the newlines after the regex may or may not matter. `\Z` matches _after_ the newline at the end of the string, whereas `$` matches the end of the string _or_ just before a newline. + +## Running A Transcript + +Once you have created a transcript, it's easy to have your application play it back and check the output. From within the `examples/` directory: + +```none +$ python example.py --test transcript_regex.txt +. +---------------------------------------------------------------------- +Ran 1 test in 0.013s + +OK +``` + +The output will look familiar if you use `unittest`, because that's exactly what happens. Each command in the transcript is run, and we `assert` the output matches the expected result from the transcript. + +!!! note + +If you have passed an `allow_cli_args` parameter containing `False` to `cmd2.Cmd.__init__`{.interpreted-text role="meth"} in order to disable parsing of command line arguments at invocation, then the use of `-t` or `--test` to run transcript testing is automatically disabled. In this case, you can alternatively provide a value for the optional `transcript_files` when constructing the instance of your `cmd2.Cmd` derived class in order to cause a transcript test to run: + +```none +from cmd2 import Cmd +class App(Cmd): + # customized attributes and methods here + +if __name__ == '__main__': + app = App(transcript_files=['exampleSession.txt']) + app.cmdloop() +``` diff --git a/docs/features/transcripts.rst b/docs/features/transcripts.rst deleted file mode 100644 index 9bab89968..000000000 --- a/docs/features/transcripts.rst +++ /dev/null @@ -1,202 +0,0 @@ -Transcripts -=========== - -A transcript is both the input and output of a successful session of a -``cmd2``-based app which is saved to a text file. With no extra work on your -part, your app can play back these transcripts as a unit test. Transcripts can -contain regular expressions, which provide the flexibility to match responses -from commands that produce dynamic or variable output. - -.. highlight:: none - - -Creating From History ---------------------- - -A transcript can automatically generated based upon commands previously -executed in the *history* using ``history -t``:: - - (Cmd) help - ... - (Cmd) help history - ... - (Cmd) history 1:2 -t transcript.txt - 2 commands and outputs saved to transcript file 'transcript.txt' - -This is by far the easiest way to generate a transcript. - -.. warning:: - - Make sure you use the **poutput()** method in your ``cmd2`` application for - generating command output. This method of the ``cmd2.Cmd`` class ensure - that output is properly redirected when redirecting to a file, piping to a - shell command, and when generating a transcript. - - -Creating From A Script File ---------------------------- - -A transcript can also be automatically generated from a script file using -``run_script -t``:: - - (Cmd) run_script scripts/script.txt -t transcript.txt - 2 commands and their outputs saved to transcript file 'transcript.txt' - (Cmd) - -This is a particularly attractive option for automatically regenerating -transcripts for regression testing as your ``cmd2`` application changes. - - -Creating Manually ------------------ - -Here's a transcript created from ``python examples/example.py``:: - - (Cmd) say -r 3 Goodnight, Gracie - Goodnight, Gracie - Goodnight, Gracie - Goodnight, Gracie - (Cmd) mumble maybe we could go to lunch - like maybe we ... could go to hmmm lunch - (Cmd) mumble maybe we could go to lunch - well maybe we could like go to er lunch right? - -This transcript has three commands: they are on the lines that begin with the -prompt. The first command looks like this:: - - (Cmd) say -r 3 Goodnight, Gracie - -Following each command is the output generated by that command. - -The transcript ignores all lines in the file until it reaches the first line -that begins with the prompt. You can take advantage of this by using the first -lines of the transcript as comments:: - - # Lines at the beginning of the transcript that do not - ; start with the prompt i.e. '(Cmd) ' are ignored. - /* You can use them for comments. */ - - All six of these lines before the first prompt are treated as comments. - - (Cmd) say -r 3 Goodnight, Gracie - Goodnight, Gracie - Goodnight, Gracie - Goodnight, Gracie - (Cmd) mumble maybe we could go to lunch - like maybe we ... could go to hmmm lunch - (Cmd) mumble maybe we could go to lunch - maybe we could like go to er lunch right? - -In this example I've used several different commenting styles, and even bare -text. It doesn't matter what you put on those beginning lines. Everything -before:: - - (Cmd) say -r 3 Goodnight, Gracie - -will be ignored. - - -Regular Expressions -------------------- - -If we used the above transcript as-is, it would likely fail. As you can see, -the ``mumble`` command doesn't always return the same thing: it inserts random -words into the input. - -Regular expressions can be included in the response portion of a transcript, -and are surrounded by slashes:: - - (Cmd) mumble maybe we could go to lunch - /.*\bmaybe\b.*\bcould\b.*\blunch\b.*/ - (Cmd) mumble maybe we could go to lunch - /.*\bmaybe\b.*\bcould\b.*\blunch\b.*/ - -Without creating a tutorial on regular expressions, this one matches anything -that has the words ``maybe``, ``could``, and ``lunch`` in that order. It -doesn't ensure that ``we`` or ``go`` or ``to`` appear in the output, but it -does work if mumble happens to add words to the beginning or the end of the -output. - -Since the output could be multiple lines long, ``cmd2`` uses multiline regular -expression matching, and also uses the ``DOTALL`` flag. These two flags subtly -change the behavior of commonly used special characters like ``.``, ``^`` and -``$``, so you may want to double check the `Python regular expression -documentation `_. - -If your output has slashes in it, you will need to escape those slashes so the -stuff between them is not interpred as a regular expression. In this -transcript:: - - (Cmd) say cd /usr/local/lib/python3.11/site-packages - /usr/local/lib/python3.11/site-packages - -the output contains slashes. The text between the first slash and the second -slash, will be interpreted as a regular expression, and those two slashes will -not be included in the comparison. When replayed, this transcript would -therefore fail. To fix it, we could either write a regular expression to match -the path instead of specifying it verbatim, or we can escape the slashes:: - - (Cmd) say cd /usr/local/lib/python3.11/site-packages - \/usr\/local\/lib\/python3.11\/site-packages - -.. warning:: - - Be aware of trailing spaces and newlines. Your commands might output - trailing spaces which are impossible to see. Instead of leaving them - invisible, you can add a regular expression to match them, so that you can - see where they are when you look at the transcript:: - - (Cmd) set editor - editor: vim/ / - - Some terminal emulators strip trailing space when you copy text from them. - This could make the actual data generated by your app different than the - text you pasted into the transcript, and it might not be readily obvious why - the transcript is not passing. Consider using - :ref:`features/redirection:Output Redirection and Pipes` to the clipboard or - to a file to ensure you accurately capture the output of your command. - - If you aren't using regular expressions, make sure the newlines at the end - of your transcript exactly match the output of your commands. A common cause - of a failing transcript is an extra or missing newline. - - If you are using regular expressions, be aware that depending on how you - write your regex, the newlines after the regex may or may not matter. ``\Z`` - matches *after* the newline at the end of the string, whereas ``$`` matches - the end of the string *or* just before a newline. - - -Running A Transcript --------------------- - -Once you have created a transcript, it's easy to have your application play it -back and check the output. From within the ``examples/`` directory:: - - $ python example.py --test transcript_regex.txt - . - ---------------------------------------------------------------------- - Ran 1 test in 0.013s - - OK - -The output will look familiar if you use ``unittest``, because that's exactly -what happens. Each command in the transcript is run, and we ``assert`` the -output matches the expected result from the transcript. - -.. note:: - - If you have passed an ``allow_cli_args`` parameter containing `False` to - :meth:`cmd2.Cmd.__init__` in order to disable parsing of command line - arguments at invocation, then the use of ``-t`` or ``--test`` to run - transcript testing is automatically disabled. In this case, you can - alternatively provide a value for the optional ``transcript_files`` when - constructing the instance of your ``cmd2.Cmd`` derived class in order to - cause a transcript test to run:: - - from cmd2 import Cmd - class App(Cmd): - # customized attributes and methods here - - if __name__ == '__main__': - app = App(transcript_files=['exampleSession.txt']) - app.cmdloop() diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 000000000..78d116c3e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,68 @@ +# cmd2 + +A python package for building powerful command-line interpreter (CLI) programs. Extends the Python Standard Library's [cmd](https://docs.python.org/3/library/cmd.html) package. + +The basic use of `cmd2` is identical to that of [cmd](https://docs.python.org/3/library/cmd.html). + +1. Create a subclass of `cmd2.Cmd`. Define attributes and `do_*` methods to control its behavior. Throughout this documentation, we will assume that you are naming your subclass `App`: + +```py title="Creating a class inherited from cmd2.Cmd" linenums="1" +from cmd2 import Cmd +class App(Cmd): + # customized attributes and methods here +``` + +2. Instantiate `App` and start the command loop: + +```py title="Instatiating and starting a cmd2 app" linenums="1" hl_lines="5-6" +from cmd2 import Cmd +class App(Cmd): + # customized attributes and methods here + +app = App() +app.cmdloop() +``` + +## Getting Started + +{% + include-markdown "./overview/index.md" +%} + +## Migrating from cmd + +{% + include-markdown "./migrating/index.md" +%} + +## Features + +{% + include-markdown "./features/index.md" + start="" + end="" +%} + +## Examples + +{% + include-markdown "./examples/index.md" + start="" + end="" +%} + +## Plugins + +{% + include-markdown "./plugins/index.md" + start="" + end="" +%} + +## [Testing](testing.md) + +## [API Reference](api/index.md) + +## Meta + +[Documentation Conventions](doc_conventions.md) diff --git a/docs/index.rst b/docs/index.rst deleted file mode 100644 index 30584f42f..000000000 --- a/docs/index.rst +++ /dev/null @@ -1,107 +0,0 @@ -==== -cmd2 -==== - -.. default-domain:: py - -.. _cmd: https://docs.python.org/3/library/cmd.html - -A python package for building powerful command-line interpreter (CLI) -programs. Extends the Python Standard Library's cmd_ package. - -The basic use of ``cmd2`` is identical to that of cmd_. - -1. Create a subclass of ``cmd2.Cmd``. Define attributes and - ``do_*`` methods to control its behavior. Throughout this documentation, - we will assume that you are naming your subclass ``App``:: - - from cmd2 import Cmd - class App(Cmd): - # customized attributes and methods here - -2. Instantiate ``App`` and start the command loop:: - - app = App() - app.cmdloop() - - -Getting Started -=============== - -.. include:: overview/summary.rst - -.. toctree:: - :maxdepth: 1 - :hidden: - - overview/index - - -Migrating from cmd -================== - -.. include:: migrating/summary.rst - -.. toctree:: - :maxdepth: 2 - :hidden: - - migrating/index - - -Features -======== - -.. toctree:: - :maxdepth: 2 - - features/index - - -Examples -======== - -.. toctree:: - :maxdepth: 2 - - examples/index - - -Plugins -======= - -.. toctree:: - :maxdepth: 2 - - plugins/index - - -Testing -======= - -.. toctree:: - :maxdepth: 2 - - testing - - -API Reference -============= - -.. toctree:: - :maxdepth: 2 - - api/index - - -Meta -==== - -:doc:`doc_conventions` - -.. toctree:: - :maxdepth: 2 - :hidden: - :caption: Meta - - doc_conventions diff --git a/docs/make.bat b/docs/make.bat deleted file mode 100755 index 3e88e0354..000000000 --- a/docs/make.bat +++ /dev/null @@ -1,113 +0,0 @@ -@ECHO OFF - -REM Command file for Sphinx documentation - -set SPHINXBUILD=sphinx-build -set BUILDDIR=_build -set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . -if NOT "%PAPER%" == "" ( - set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% -) - -if "%1" == "" goto help - -if "%1" == "help" ( - :help - echo.Please use `make ^` where ^ is one of - echo. html to make standalone HTML files - echo. dirhtml to make HTML files named index.html in directories - echo. pickle to make pickle files - echo. json to make JSON files - echo. htmlhelp to make HTML files and a HTML help project - echo. qthelp to make HTML files and a qthelp project - echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter - echo. changes to make an overview over all changed/added/deprecated items - echo. linkcheck to check all external links for integrity - echo. doctest to run all doctests embedded in the documentation if enabled - goto end -) - -if "%1" == "clean" ( - for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i - del /q /s %BUILDDIR%\* - goto end -) - -if "%1" == "html" ( - %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/html. - goto end -) - -if "%1" == "dirhtml" ( - %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml - echo. - echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. - goto end -) - -if "%1" == "pickle" ( - %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle - echo. - echo.Build finished; now you can process the pickle files. - goto end -) - -if "%1" == "json" ( - %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json - echo. - echo.Build finished; now you can process the JSON files. - goto end -) - -if "%1" == "htmlhelp" ( - %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp - echo. - echo.Build finished; now you can run HTML Help Workshop with the ^ -.hhp project file in %BUILDDIR%/htmlhelp. - goto end -) - -if "%1" == "qthelp" ( - %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp - echo. - echo.Build finished; now you can run "qcollectiongenerator" with the ^ -.qhcp project file in %BUILDDIR%/qthelp, like this: - echo.^> qcollectiongenerator %BUILDDIR%\qthelp\cmd2.qhcp - echo.To view the help file: - echo.^> assistant -collectionFile %BUILDDIR%\qthelp\cmd2.ghc - goto end -) - -if "%1" == "latex" ( - %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex - echo. - echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. - goto end -) - -if "%1" == "changes" ( - %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes - echo. - echo.The overview file is in %BUILDDIR%/changes. - goto end -) - -if "%1" == "linkcheck" ( - %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck - echo. - echo.Link check complete; look for any errors in the above output ^ -or in %BUILDDIR%/linkcheck/output.txt. - goto end -) - -if "%1" == "doctest" ( - %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest - echo. - echo.Testing of doctests in the sources finished, look at the ^ -results in %BUILDDIR%/doctest/output.txt. - goto end -) - -:end diff --git a/docs/migrating/incompatibilities.md b/docs/migrating/incompatibilities.md new file mode 100644 index 000000000..2680b35a0 --- /dev/null +++ b/docs/migrating/incompatibilities.md @@ -0,0 +1,21 @@ +# Incompatibilities + +`cmd2` strives to be drop-in compatible with [cmd](https://docs.python.org/3/library/cmd.html), however there are a few incompatibilities. + +## Cmd.emptyline() + +The [Cmd.emptyline()](https://docs.python.org/3/library/cmd.html#cmd.Cmd.emptyline) function is called when an empty line is entered in response to the prompt. By default, in [cmd](https://docs.python.org/3/library/cmd.html) if this method is not overridden, it repeats and executes the last nonempty command entered. However, no end user we have encountered views this as expected or desirable default behavior. `cmd2` completely ignores empty lines and the base class `cmd.emptyline()` method never gets called and thus the empty line behavior cannot be overridden. + +## Cmd.identchars + +In [cmd](https://docs.python.org/3/library/cmd.html), the [Cmd.identchars](https://docs.python.org/3/library/cmd.html#cmd.Cmd.identchars) attribute contains the string of characters accepted for command names. [cmd](https://docs.python.org/3/library/cmd.html) uses those characters to split the first "word" of the input, without requiring the user to type a space. For example, if `identchars` contained a string of all alphabetic characters, the user could enter a command like `L20` and it would be interpreted as the command `L` with the first argument of `20`. + +Since version 0.9.0, `cmd2` has ignored `identchars`; the parsing logic in `cmd2` splits the command and arguments on whitespace. We opted for this breaking change because while [cmd](https://docs.python.org/3/library/cmd.html) supports unicode, using non-ascii unicode characters in command names while simultaneously using `identchars` functionality can be somewhat painful. Requiring white space to delimit arguments also ensures reliable operation of many other useful `cmd2` features, including [Tab Completion](../features/completion.md) and [Shortcuts, Aliases, and Macros](../features/shortcuts_aliases_macros.md). + +If you really need this functionality in your app, you can add it back in by writing a [Postparsing Hook](../features/hooks.md#postparsing-hooks). + +## Cmd.cmdqueue + +In [cmd](https://docs.python.org/3/library/cmd.html), the [Cmd.cmdqueue](https://docs.python.org/3/library/cmd.html#cmd.Cmd.cmdqueue) attribute contains a list of queued input lines. The cmdqueue list is checked in `cmdloop()` when new input is needed; if it is nonempty, its elements will be processed in order, as if entered at the prompt. + +Since version 0.9.13 `cmd2` has removed support for `Cmd.cmdqueue`. Because `cmd2` supports running commands via the main `cmdloop()`, text scripts, Python scripts, transcripts, and history replays, the only way to preserve consistent behavior across these methods was to eliminate the command queue. Additionally, reasoning about application behavior is much easier without this queue present. diff --git a/docs/migrating/incompatibilities.rst b/docs/migrating/incompatibilities.rst deleted file mode 100644 index ba6f2ed10..000000000 --- a/docs/migrating/incompatibilities.rst +++ /dev/null @@ -1,61 +0,0 @@ -Incompatibilities -================= - -.. _cmd: https://docs.python.org/3/library/cmd.html - -``cmd2`` strives to be drop-in compatible with cmd_, however there are a few -incompatibilities. - - -Cmd.emptyline() ---------------- - -The `Cmd.emptyline() -`_ function is -called when an empty line is entered in response to the prompt. By default, in -cmd_ if this method is not overridden, it repeats and executes the last -nonempty command entered. However, no end user we have encountered views this -as expected or desirable default behavior. ``cmd2`` completely ignores empty -lines and the base class ``cmd.emptyline()`` method never gets called and thus -the empty line behavior cannot be overridden. - - -Cmd.identchars --------------- - -In cmd_, the `Cmd.identchars -`_ attribute -contains the string of characters accepted for command names. cmd_ uses those -characters to split the first "word" of the input, without requiring the user -to type a space. For example, if ``identchars`` contained a string of all -alphabetic characters, the user could enter a command like ``L20`` and it would -be interpreted as the command ``L`` with the first argument of ``20``. - -Since version 0.9.0, ``cmd2`` has ignored ``identchars``; the parsing logic in -``cmd2`` splits the command and arguments on whitespace. We opted for this -breaking change because while cmd_ supports unicode, using non-ascii unicode -characters in command names while simultaneously using ``identchars`` -functionality can be somewhat painful. Requiring white space to delimit -arguments also ensures reliable operation of many other useful ``cmd2`` -features, including :ref:`features/completion:Completion` and -:ref:`features/shortcuts_aliases_macros:Shortcuts, Aliases, and Macros`. - -If you really need this functionality in your app, you can add it back in by -writing a :ref:`Postparsing Hook `. - - -Cmd.cmdqueue ------------- - -In cmd_, the `Cmd.cmdqueue -`_ attribute -contains a list of queued input lines. The cmdqueue list is checked in -``cmdloop()`` when new input is needed; if it is nonempty, its elements will be -processed in order, as if entered at the prompt. - -Since version 0.9.13 ``cmd2`` has removed support for ``Cmd.cmdqueue``. Because -``cmd2`` supports running commands via the main ``cmdloop()``, text scripts, -Python scripts, transcripts, and history replays, the only way to preserve -consistent behavior across these methods was to eliminate the command queue. -Additionally, reasoning about application behavior is much easier without this -queue present. diff --git a/docs/migrating/index.md b/docs/migrating/index.md new file mode 100644 index 000000000..86257ff62 --- /dev/null +++ b/docs/migrating/index.md @@ -0,0 +1,8 @@ +# Migrating From cmd + +If you're thinking of migrating your [cmd](https://docs.python.org/3/library/cmd.html) app to `cmd2`, this section will help you decide if it's right for your app, and show you how to do it. + +- [Why cmd2](why.md) - we try and convince you to use `cmd2` instead of [cmd](https://docs.python.org/3/library/cmd.html) +- [Incompatibilities](incompatibilities.md) - `cmd2` is not quite 100% compatible with [cmd](https://docs.python.org/3/library/cmd.html). +- [Minimum Required Changes](minimum.md) - the minimum changes required to move from [cmd](https://docs.python.org/3/library/cmd.html) to `cmd2`. Start your migration here. +- [Next Steps](next_steps.md) - Once you've migrated, here a list of things you can do next to add even more functionality to your app. diff --git a/docs/migrating/index.rst b/docs/migrating/index.rst deleted file mode 100644 index 73d6b1c77..000000000 --- a/docs/migrating/index.rst +++ /dev/null @@ -1,14 +0,0 @@ -Migrating From cmd -================== - -.. toctree:: - :maxdepth: 1 - :hidden: - - why - incompatibilities - minimum - next_steps - - -.. include:: summary.rst diff --git a/docs/migrating/minimum.md b/docs/migrating/minimum.md new file mode 100644 index 000000000..91101f37b --- /dev/null +++ b/docs/migrating/minimum.md @@ -0,0 +1,41 @@ +# Minimum Required Changes + +`cmd2.Cmd` subclasses `Cmd.cmd` from the standard library, and overrides most of the methods. Most apps based on the standard library can be migrated to `cmd2` in just a couple of minutes. + +## Import and Inheritance + +You need to change your import from this: + +```py +import cmd +``` + +to this: + +```py +import cmd2 +``` + +Then you need to change your class definition from: + +```py +class CmdLineApp(cmd.Cmd): +``` + +to: + +```py +class CmdLineApp(cmd2.Cmd): +``` + +## Exiting + +Have a look at the commands you created to exit your application. You probably have one called `exit` and maybe a similar one called `quit`. You also might have implemented a `do_EOF()` method so your program exits like many operating system shells. If all these commands do is quit the application, you may be able to remove them. See [Exiting](../features/misc.md#exiting). + +## Distribution + +If you are distributing your application, you'll also need to ensure that `cmd2` is properly installed. You will need to add the following dependency to your `pyproject.toml` or `setup.py`: + + 'cmd2>=2,<3' + +See [Integrate cmd2 Into Your Project](../overview/integrating.md) for more details. diff --git a/docs/migrating/minimum.rst b/docs/migrating/minimum.rst deleted file mode 100644 index c23bbbd73..000000000 --- a/docs/migrating/minimum.rst +++ /dev/null @@ -1,51 +0,0 @@ -Minimum Required Changes -======================== - -``cmd2.Cmd`` subclasses ``Cmd.cmd`` from the standard library, and overrides -most of the methods. Most apps based on the standard library can be migrated to -``cmd2`` in just a couple of minutes. - - -Import and Inheritance ----------------------- - -You need to change your import from this:: - - import cmd - -to this:: - - import cmd2 - -Then you need to change your class definition from:: - - class CmdLineApp(cmd.Cmd): - -to:: - - class CmdLineApp(cmd2.Cmd): - - -Exiting -------- - -Have a look at the commands you created to exit your application. You probably -have one called ``exit`` and maybe a similar one called ``quit``. You also -might have implemented a ``do_EOF()`` method so your program exits like many -operating system shells. If all these commands do is quit the application, -you may be able to remove them. See :ref:`features/misc:Exiting`. - - -Distribution ------------- - -If you are distributing your application, you'll also need to ensure -that ``cmd2`` is properly installed. You will need to add this to -your ``setup()`` method in ``setup.py``:: - - install_requires=[ - 'cmd2>=1,<2` - ] - -See :ref:`overview/integrating:Integrate cmd2 Into Your Project` for more -details. diff --git a/docs/migrating/next_steps.md b/docs/migrating/next_steps.md new file mode 100644 index 000000000..b1c5c3a6f --- /dev/null +++ b/docs/migrating/next_steps.md @@ -0,0 +1,25 @@ +# Next Steps + +Once your current application is using `cmd2`, you can start to expand the functionality by levering other `cmd2` features. The three ideas here will get you started. Browse the rest of the [Features](../features/index.md) to see what else `cmd2` can help you do. + +## Argument Parsing + +For all but the simplest of commands, it's probably easier to use [argparse](https://docs.python.org/3/library/argparse.html) to parse user input. `cmd2` provides a `@with_argparser()` decorator which associates an `ArgumentParser` object with one of your commands. Using this method will: + +1. Pass your command a [Namespace](https://docs.python.org/3/library/argparse.html#argparse.Namespace) containing the arguments instead of a string of text. +2. Properly handle quoted string input from your users. +3. Create a help message for you based on the `ArgumentParser`. +4. Give you a big headstart adding [Tab Completion](../features/completion.md) to your application. +5. Make it much easier to implement subcommands (i.e. `git` has a bunch of subcommands such as `git pull`, `git diff`, etc). + +There's a lot more about [Argument Processing](../features/argument_processing.md) if you want to dig in further. + +## Help + +If you have lot of commands in your application, `cmd2` can categorize those commands using a one line decorator `@with_category()`. When a user types `help` the available commands will be organized by the category you specified. + +If you were already using `argparse` or decided to switch to it, you can easily [standardize all of your help messages](../features/argument_processing.md#help-messages) to be generated by your argument parsers and displayed by `cmd2`. No more help messages that don't match what the code actually does. + +## Generating Output + +If your program generates output by printing directly to `sys.stdout`, you should consider switching to `cmd2.Cmd.poutput`, `cmd2.Cmd.perror`, and `cmd2.Cmd.pfeedback`. These methods work with several of the built in [Settings](../features/settings.md) to allow the user to view or suppress feedback (i.e. progress or status output). They also properly handle ansi colored output according to user preference. Speaking of colored output, you can use any color library you want, or use the included `cmd2.ansi.style` function. These and other related topics are covered in [Generating Output](../features/generating_output.md). diff --git a/docs/migrating/next_steps.rst b/docs/migrating/next_steps.rst deleted file mode 100644 index 545e1c0a5..000000000 --- a/docs/migrating/next_steps.rst +++ /dev/null @@ -1,61 +0,0 @@ -Next Steps -========== - -Once your current application is using ``cmd2``, you can start to expand the -functionality by levering other ``cmd2`` features. The three ideas here will -get you started. Browse the rest of the :ref:`features/index:Features` to see -what else ``cmd2`` can help you do. - - -Argument Parsing ----------------- - -For all but the simplest of commands, it's probably easier to use `argparse -`_ to parse user input. -``cmd2`` provides a ``@with_argparser()`` decorator which associates an -``ArgumentParser`` object with one of your commands. Using this method will: - -1. Pass your command a `Namespace `_ - containing the arguments instead of a string of text. - -2. Properly handle quoted string input from your users. - -3. Create a help message for you based on the ``ArgumentParser``. - -4. Give you a big headstart adding :ref:`features/completion:Completion` to - your application. - -5. Make it much easier to implement subcommands (i.e. ``git`` has - a bunch of subcommands such as ``git pull``, ``git diff``, etc). - -There's a lot more about :ref:`features/argument_processing:Argument -Processing` if you want to dig in further. - - -Help ----- - -If you have lot of commands in your application, ``cmd2`` can categorize those -commands using a one line decorator ``@with_category()``. When a user types -``help`` the available commands will be organized by the category you -specified. - -If you were already using ``argparse`` or decided to switch to it, you can -easily :ref:`standardize all of your help messages -` to be generated by your argument -parsers and displayed by ``cmd2``. No more help messages that don't match what -the code actually does. - - -Generating Output ------------------ - -If your program generates output by printing directly to ``sys.stdout``, you -should consider switching to :meth:`~cmd2.Cmd.poutput`, -:meth:`~cmd2.Cmd.perror`, and :meth:`~cmd2.Cmd.pfeedback`. These -methods work with several of the built in :ref:`features/settings:Settings` to -allow the user to view or suppress feedback (i.e. progress or status output). -They also properly handle ansi colored output according to user preference. -Speaking of colored output, you can use any color library you want, or use the -included :meth:`cmd2.ansi.style` function. These and other related topics are -covered in :ref:`features/generating_output:Generating Output`. diff --git a/docs/migrating/summary.rst b/docs/migrating/summary.rst deleted file mode 100644 index 5bde2d0ca..000000000 --- a/docs/migrating/summary.rst +++ /dev/null @@ -1,15 +0,0 @@ - -.. _cmd: https://docs.python.org/3/library/cmd.html - -If you're thinking of migrating your cmd_ app to ``cmd2``, this section -will help you decide if it's right for your app, and show you how to -do it. - -* :ref:`migrating/why:Why cmd2` - we try and convince you - to use ``cmd2`` instead of cmd_ -* :ref:`migrating/incompatibilities:Incompatibilities` - ``cmd2`` is not - quite 100% compatible with cmd_. -* :ref:`migrating/minimum:Minimum Required Changes` - the minimum changes - required to move from cmd_ to ``cmd2``. Start your migration here. -* :ref:`migrating/next_steps:Next Steps` - Once you've migrated, here a list - of things you can do next to add even more functionality to your app. diff --git a/docs/migrating/why.md b/docs/migrating/why.md new file mode 100644 index 000000000..d7c1efc9b --- /dev/null +++ b/docs/migrating/why.md @@ -0,0 +1,30 @@ +# Why cmd2 + +## cmd + +[cmd](#cmd) is the Python Standard Library's module for creating simple interactive command-line applications. [cmd](#cmd) is an extremely bare-bones framework which leaves a lot to be desired. It doesn't even include a built-in way to exit from an application! + +Since the API provided by [cmd](#cmd) provides the foundation on which `cmd2` is based, understanding the use of [cmd](#cmd) is the first step in learning the use of `cmd2`. Once you have read the [cmd](#cmd) docs, return here to learn the ways that `cmd2` differs from [cmd](#cmd). + +## cmd2 + +`cmd2` is a batteries-included extension of [cmd](#cmd), which provides a wealth of functionality to make it quicker and easier for developers to create feature-rich interactive command-line applications which delight customers. + +`cmd2` can be used as a drop-in replacement for [cmd](#cmd) with a few minor discrepancies as discussed in the [Incompatibilities](incompatibilities.md) section. Simply importing `cmd2` in place of [cmd](#cmd) will add many features to an application without any further modifications. Migrating to `cmd2` will also open many additional doors for making it possible for developers to provide a top-notch interactive command-line experience for their users. + +## Free Features + +After switching from [cmd](#cmd) to `cmd2`, your application will have the following new features and capabilities, without you having to do anything: + +- More robust [History](../features/history.md). Both [cmd](#cmd) and `cmd2` have readline history, but `cmd2` also has a robust `history` command which allows you to edit prior commands in a text editor of your choosing, re-run multiple commands at a time, and save prior commands as a script to be executed later. +- Users can redirect output to a file or pipe it to some other operating system command. You did remember to use `self.stdout` instead of `sys.stdout` in all of your print functions, right? If you did, then this will work out of the box. If you didn't, you'll have to go back and fix them. Before you do, you might consider the various ways `cmd2` has of [Generatoring Output](../features/generating_output.md). +- Users can load script files, which contain a series of commands to be executed. +- Users can create [Shortcuts, Aliases, and Macros](../features/shortcuts_aliases_macros.md) to reduce the typing required for repetitive commands. +- Embedded python shell allows a user to execute python code from within your `cmd2` app. How meta. +- [Clipboard Integration](../features/clipboard.md) allows you to save command output to the operating system clipboard. +- A built-in [Timer](../features/misc.md#Timer) can show how long it takes a command to execute +- A [Transcript](../features/transcripts.md) is a file which contains both the input and output of a successful session of a `cmd2`-based app. The transcript can be played back into the app as a unit test. + +## Next Steps + +In addition to the features you get with no additional work, `cmd2` offers a broad range of additional capabilities which can be easily added to your application. [Next Steps](next_steps.md) has some ideas of where you can start, or you can dig in to all the [Features](../features/index.md). diff --git a/docs/migrating/why.rst b/docs/migrating/why.rst deleted file mode 100644 index 2bfd45f14..000000000 --- a/docs/migrating/why.rst +++ /dev/null @@ -1,82 +0,0 @@ -Why cmd2 -======== - -.. _cmd: https://docs.python.org/3/library/cmd.html - -cmd ---- - -cmd_ is the Python Standard Library's module for creating simple interactive -command-line applications. cmd_ is an extremely bare-bones framework which -leaves a lot to be desired. It doesn't even include a built-in way to exit -from an application! - -Since the API provided by cmd_ provides the foundation on which ``cmd2`` is -based, understanding the use of cmd_ is the first step in learning the use of -``cmd2``. Once you have read the cmd_ docs, return here to learn the ways that -``cmd2`` differs from cmd_. - - -cmd2 ----- - -``cmd2`` is a batteries-included extension of cmd_, which provides a wealth of -functionality to make it quicker and easier for developers to create -feature-rich interactive command-line applications which delight customers. - -``cmd2`` can be used as a drop-in replacement for cmd_ with a few minor -discrepancies as discussed in the -:ref:`migrating/incompatibilities:Incompatibilities` section. Simply importing -``cmd2`` in place of cmd_ will add many features to an application without any -further modifications. Migrating to ``cmd2`` will also open many additional -doors for making it possible for developers to provide a top-notch interactive -command-line experience for their users. - - -Free Features -------------- - -After switching from cmd_ to ``cmd2``, your application will have the following -new features and capabilities, without you having to do anything: - -- More robust :ref:`features/history:History`. Both cmd_ and ``cmd2`` have - readline history, but ``cmd2`` also has a robust ``history`` command which - allows you to edit prior commands in a text editor of your choosing, re-run - multiple commands at a time, and save prior commands as a script to be - executed later. - -- Users can redirect output to a file or pipe it to some other operating system - command. You did remember to use ``self.stdout`` instead of ``sys.stdout`` in - all of your print functions, right? If you did, then this will work out of - the box. If you didn't, you'll have to go back and fix them. Before you do, - you might consider the various ways ``cmd2`` has of - :ref:`features/generating_output:Generating Output`. - -- Users can load script files, which contain a series of commands - to be executed. - -- Users can create :ref:`features/shortcuts_aliases_macros:Shortcuts, Aliases, - and Macros` to reduce the typing required for repetitive commands. - -- Embedded python shell allows a user to execute python code from within your - ``cmd2`` app. How meta. - -- :ref:`features/clipboard:Clipboard Integration` allows you to save command - output to the operating system clipboard. - -- A built-in :ref:`features/misc:Timer` can show how long it takes a command to - execute - -- A :ref:`Transcript ` is a file which - contains both the input and output of a successful session of a - ``cmd2``-based app. The transcript can be played back into the app as a unit - test. - - -Next Steps ----------- - -In addition to the features you get with no additional work, ``cmd2`` offers a -broad range of additional capabilities which can be easily added to your -application. :ref:`migrating/next_steps:Next Steps` has some ideas of where -you can start, or you can dig in to all the :ref:`features/index:Features`. diff --git a/docs/overview/alternatives.md b/docs/overview/alternatives.md new file mode 100644 index 000000000..e54e8f098 --- /dev/null +++ b/docs/overview/alternatives.md @@ -0,0 +1,16 @@ +# Alternatives + +For programs that do not interact with the user in a continuous loop - programs that simply accept a set of arguments from the command line, return results, and do not keep the user within the program's environment - all you need are [sys](https://docs.python.org/3/library/sys.html).argv (the command-line arguments) and [argparse](https://docs.python.org/3/library/argparse.html) (for parsing UNIX-style options and flags). Though some people may prefer [docopt](https://pypi.org/project/docopt) or [click](https://click.palletsprojects.com) to [argparse](https://docs.python.org/3/library/argparse.html). + +The [textual](https://textual.textualize.io/) module is capable of building sophisticated full-screen terminal user interfaces that are not limited to simple text input and output; they can paint the screen with options that are selected from using the cursor keys and even mouse clicks. However, programming a `textual` application is not as straightforward as using `cmd2`. + +Several Python packages exist for building interactive command-line applications approximately similar in concept to [cmd](https://docs.python.org/3/library/cmd.html) applications. None of them share `cmd2`'s close ties to [cmd](https://docs.python.org/3/library/cmd.html), but they may be worth investigating nonetheless. Two of the most mature and full featured are: + +- [Python Prompt Toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) +- [Click](https://click.palletsprojects.com) + +[Python Prompt Toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) is a library for building powerful interactive command lines and terminal applications in Python. It provides a lot of advanced visual features like syntax highlighting, bottom bars, and the ability to create fullscreen apps. + +[Click](https://click.palletsprojects.com) is a Python package for creating beautiful command line interfaces in a composable way with as little code as necessary. It is more geared towards command line utilities instead of command line interpreters, but it can be used for either. + +Getting a working command-interpreter application based on either [Python Prompt Toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit) or [Click](https://click.palletsprojects.com) requires a good deal more effort and boilerplate code than `cmd2`. `cmd2` focuses on providing an excellent out-of-the-box experience with as many useful features as possible built in for free with as little work required on the developer's part as possible. We believe that `cmd2` provides developers the easiest way to write a command-line interpreter, while allowing a good experience for end users. If you are seeking a visually richer end-user experience and don't mind investing more development time, we would recommend checking out [Python Prompt Toolkit](https://github.com/prompt-toolkit/python-prompt-toolkit). diff --git a/docs/overview/alternatives.rst b/docs/overview/alternatives.rst deleted file mode 100644 index 27b356bce..000000000 --- a/docs/overview/alternatives.rst +++ /dev/null @@ -1,54 +0,0 @@ -Alternatives -============ - -For programs that do not interact with the user in a continuous loop - programs -that simply accept a set of arguments from the command line, return results, -and do not keep the user within the program's environment - all you need are -sys_\ .argv (the command-line arguments) and argparse_ (for parsing UNIX-style -options and flags). Though some people may prefer docopt_ or click_ to -argparse_. - -.. _sys: https://docs.python.org/3/library/sys.html -.. _argparse: https://docs.python.org/3/library/argparse.html -.. _docopt: https://pypi.org/project/docopt -.. _click: https://click.palletsprojects.com - - -The curses_ module produces applications that interact via a plaintext terminal -window, but are not limited to simple text input and output; they can paint the -screen with options that are selected from using the cursor keys. However, -programming a curses_-based application is not as straightforward as using -cmd_. - -.. _curses: https://docs.python.org/3/library/curses.html -.. _cmd: https://docs.python.org/3/library/cmd.html - -Several Python packages exist for building interactive command-line -applications approximately similar in concept to cmd_ applications. None of -them share ``cmd2``'s close ties to cmd_, but they may be worth investigating -nonetheless. Two of the most mature and full featured are: - - * `Python Prompt Toolkit`_ - * Click_ - -.. _`Python Prompt Toolkit`: https://github.com/prompt-toolkit/python-prompt-toolkit - -`Python Prompt Toolkit`_ is a library for building powerful interactive command -lines and terminal applications in Python. It provides a lot of advanced -visual features like syntax highlighting, bottom bars, and the ability to -create fullscreen apps. - -Click_ is a Python package for creating beautiful command line interfaces in a -composable way with as little code as necessary. It is more geared towards -command line utilities instead of command line interpreters, but it can be used -for either. - -Getting a working command-interpreter application based on either `Python -Prompt Toolkit`_ or Click_ requires a good deal more effort and boilerplate -code than ``cmd2``. ``cmd2`` focuses on providing an excellent out-of-the-box -experience with as many useful features as possible built in for free with as -little work required on the developer's part as possible. We believe that -``cmd2`` provides developers the easiest way to write a command-line -interpreter, while allowing a good experience for end users. If you are -seeking a visually richer end-user experience and don't mind investing more -development time, we would recommend checking out `Python Prompt Toolkit`_. diff --git a/docs/overview/index.md b/docs/overview/index.md new file mode 100644 index 000000000..92a01bf4a --- /dev/null +++ b/docs/overview/index.md @@ -0,0 +1,13 @@ +# Getting Started + +Building a new [REPL](https://en.wikipedia.org/wiki/Read–eval–print_loop) or [Command Line Interface](https://en.wikipedia.org/wiki/Command-line_interface) application? + +Already built an application that uses [cmd](https://docs.python.org/3/library/cmd.html) from the python standard library and want to add more functionality with very little work? + +`cmd2` is a powerful python library for building command line applications. Start here to find out if this library is a good fit for your needs. + +- [Installation Instructions](installation.md) - how to install `cmd2` and associated optional dependencies +- [First Application](../examples/first_app.md) - a sample application showing 8 key features of `cmd2` +- [Integrate cmd2 Into Your Project](integrating.md) - adding `cmd2` to your project +- [Alternatives](alternatives.md) - other python packages that might meet your needs +- [Resources](resources.md) - related links and other materials diff --git a/docs/overview/index.rst b/docs/overview/index.rst deleted file mode 100644 index 231191c12..000000000 --- a/docs/overview/index.rst +++ /dev/null @@ -1,15 +0,0 @@ -Getting Started -=============== - -.. toctree:: - :maxdepth: 1 - :hidden: - - installation - ../examples/first_app - integrating - alternatives - resources - - -.. include:: summary.rst diff --git a/docs/overview/installation.md b/docs/overview/installation.md new file mode 100644 index 000000000..adf0044c9 --- /dev/null +++ b/docs/overview/installation.md @@ -0,0 +1,111 @@ +# Installation Instructions + +`cmd2` works on Linux, macOS, and Windows. It requires Python 3.8 or higher, [pip](https://pypi.org/project/pip), and [setuptools](https://pypi.org/project/setuptools). If you've got all that, then you can just: + +```shell +$ pip install cmd2 +``` + +!!! note + + Depending on how and where you have installed Python on your system and on what OS you are using, you may need to have administrator or root privileges to install Python packages. If this is the case, take the necessary steps required to run the commands in this section as root/admin, e.g.: on most Linux or Mac systems, you can precede them with `sudo`: + + ```shell + $ sudo pip install + ``` + +## Prerequisites + +If you have Python 3 >=3.8 installed from [python.org](https://www.python.org), you will already have [pip](https://pypi.org/project/pip) and [setuptools](https://pypi.org/project/setuptools), but may need to upgrade to the latest versions: + +On Linux or OS X: + +```shell +$ pip install -U pip setuptools +``` + +On Windows: + +```shell +C:\> python -m pip install -U pip setuptools +``` + +## Install from PyPI {: #pip_install } + +[pip](https://pypi.org/project/pip) is the recommended installer. Installing packages from [PyPI](https://pypi.org) with pip is easy: + +```shell +$ pip install cmd2 +``` + +This will install the required 3rd-party dependencies, if necessary. + +## Install from GitHub {: #github } + +The latest version of `cmd2` can be installed directly from the master branch on GitHub using [pip](https://pypi.org/project/pip): + +```shell +$ pip install -U git+git://github.com/python-cmd2/cmd2.git +``` + +## Install from Debian or Ubuntu repos + +We recommend installing from [pip](https://pypi.org/project/pip), but if you wish to install from Debian or Ubuntu repos this can be done with apt-get. + +For Python 3: + + $ sudo apt-get install python3-cmd2 + +This will also install the required 3rd-party dependencies. + +!!! warning + + Versions of `cmd2` before 0.8.9 should be considered to be of unstable "beta" quality and should not be relied upon for production use. If you cannot get a version >= 0.8.9 from your OS repository, then we recommend installing from either pip or GitHub - see [Pip Install](installation.md#pip_install) or [Install from GitHub](installation.md#github). + +## Upgrading cmd2 + +Upgrade an already installed `cmd2` to the latest version from [PyPI](https://pypi.org): + + pip install -U cmd2 + +This will upgrade to the newest stable version of `cmd2` and will also upgrade any dependencies if necessary. + +## Uninstalling cmd2 + +If you wish to permanently uninstall `cmd2`, this can also easily be done with [pip](https://pypi.org/project/pip): + + $ pip uninstall cmd2 + +## macOS Considerations + +macOS comes with the [libedit](http://thrysoee.dk/editline/) library which is similar, but not identical, to GNU Readline. Tab completion for `cmd2` applications is only tested against GNU Readline. + +There are several ways GNU Readline can be installed within a Python environment on a Mac, detailed in the following subsections. + +### gnureadline Python module + +Install the [gnureadline](https://pypi.org/project/gnureadline) Python module which is statically linked against a specific compatible version of GNU Readline: + +```shell +$ pip install -U gnureadline +``` + +### readline via conda + +Install the **readline** package using the `conda` package manager included with the Anaconda Python distribution: + +```shell +$ conda install readline +``` + +### readline via brew + +Install the **readline** package using the Homebrew package manager (compiles from source): + +```shell +$ brew install openssl +$ brew install pyenv +$ brew install readline +``` + +Then use pyenv to compile Python and link against the installed readline diff --git a/docs/overview/installation.rst b/docs/overview/installation.rst deleted file mode 100644 index 841807c82..000000000 --- a/docs/overview/installation.rst +++ /dev/null @@ -1,162 +0,0 @@ - -Installation Instructions -========================= - - -.. _pip: https://pypi.org/project/pip -.. _setuptools: https://pypi.org/project/setuptools -.. _PyPI: https://pypi.org - -``cmd2`` works on Linux, macOS, and Windows. It requires Python 3.8 or -higher, pip_, and setuptools_. If you've got all that, then you can just: - -.. code-block:: shell - - $ pip install cmd2 - -.. note:: - - Depending on how and where you have installed Python on your system and on - what OS you are using, you may need to have administrator or root privileges - to install Python packages. If this is the case, take the necessary steps - required to run the commands in this section as root/admin, e.g.: on most - Linux or Mac systems, you can precede them with ``sudo``: - - .. code-block:: shell - - $ sudo pip install - - -Prerequisites -------------- - -If you have Python 3 >=3.8 installed from `python.org -`_, you will already have pip_ and setuptools_, but may -need to upgrade to the latest versions: - - On Linux or OS X: - - .. code-block:: shell - - $ pip install -U pip setuptools - - - On Windows: - - .. code-block:: shell - - > python -m pip install -U pip setuptools - - -.. _`pip_install`: - -Install from PyPI ------------------ - -pip_ is the recommended installer. Installing packages from PyPI_ with pip is -easy: - -.. code-block:: shell - - $ pip install cmd2 - -This will install the required 3rd-party dependencies, if necessary. - - -.. _github: - -Install from GitHub -------------------- - -The latest version of ``cmd2`` can be installed directly from the master branch -on GitHub using pip_: - -.. code-block:: shell - - $ pip install -U git+git://github.com/python-cmd2/cmd2.git - - -Install from Debian or Ubuntu repos ------------------------------------ - -We recommend installing from pip_, but if you wish to install from Debian or -Ubuntu repos this can be done with apt-get. - -For Python 3:: - - $ sudo apt-get install python3-cmd2 - -This will also install the required 3rd-party dependencies. - -.. warning:: - - Versions of ``cmd2`` before 0.8.9 should be considered to be of unstable - "beta" quality and should not be relied upon for production use. If you - cannot get a version >= 0.8.9 from your OS repository, then we recommend - installing from either pip or GitHub - see :ref:`pip_install` or - :ref:`github`. - - -Upgrading cmd2 --------------- - -Upgrade an already installed ``cmd2`` to the latest version from PyPI_:: - - pip install -U cmd2 - -This will upgrade to the newest stable version of ``cmd2`` and will also -upgrade any dependencies if necessary. - - -Uninstalling cmd2 ------------------ -If you wish to permanently uninstall ``cmd2``, this can also easily be done with pip_:: - - $ pip uninstall cmd2 - - -macOS Considerations --------------------- - -macOS comes with the `libedit `_ library which is -similar, but not identical, to GNU Readline. Tab completion for ``cmd2`` -applications is only tested against GNU Readline. - -There are several ways GNU Readline can be installed within a Python -environment on a Mac, detailed in the following subsections. - - -gnureadline Python module -~~~~~~~~~~~~~~~~~~~~~~~~~ - -Install the `gnureadline `_ Python module which is statically linked against a specific compatible version of GNU Readline: - -.. code-block:: shell - - $ pip install -U gnureadline - - -readline via conda -~~~~~~~~~~~~~~~~~~ - -Install the **readline** package using the ``conda`` package manager included -with the Anaconda Python distribution: - -.. code-block:: shell - - $ conda install readline - - -readline via brew -~~~~~~~~~~~~~~~~~ - -Install the **readline** package using the Homebrew package manager (compiles -from source): - -.. code-block:: shell - - $ brew install openssl - $ brew install pyenv - $ brew install readline - -Then use pyenv to compile Python and link against the installed readline diff --git a/docs/overview/integrating.md b/docs/overview/integrating.md new file mode 100644 index 000000000..e53a89779 --- /dev/null +++ b/docs/overview/integrating.md @@ -0,0 +1,15 @@ +# Integrate cmd2 Into Your Project + +Once installed, you will want to ensure that your project's dependencies include `cmd2`. Make sure your `pyproject.toml` or `setup.py` includes the following dependency + + 'cmd2>=2.4' + +The `cmd2` project uses [Semantic Versioning](https://semver.org), which means that any incompatible API changes will be release with a new major version number. The public API is documented in the [API Reference](../api/index.md). + +We recommend that you follow the advice given by the Python Packaging User Guide related to [install_requires](https://packaging.python.org/discussions/install-requires-vs-requirements/). By setting an upper bound on the allowed version, you can ensure that your project does not inadvertently get installed with an incompatible future version of `cmd2`. + +## OS Considerations + +If you would like to use [Tab Completion](../features/completion.md), then you need a compatible version of [readline](https://tiswww.case.edu/php/chet/readline/rltop.html) installed on your operating system (OS). `cmd2` forces a sane install of `readline` on both `Windows` and `MacOS`, but does not do so on `Linux`. If for some reason, you have a Linux OS that has the [Editline Library (libedit)](https://www.thrysoee.dk/editline/) installed instead of `readline`, you will need to manually add a dependency on `gnureadline`. Make sure to include the following dependency in your `pyproject.toml` or `setup.py`: + + 'gnureadline' diff --git a/docs/overview/integrating.rst b/docs/overview/integrating.rst deleted file mode 100644 index 9dc77e8c0..000000000 --- a/docs/overview/integrating.rst +++ /dev/null @@ -1,35 +0,0 @@ -Integrate cmd2 Into Your Project -==================================== - -Once installed, you will want to ensure that your project's dependencies -include ``cmd2``. Make sure your ``setup.py`` includes the following:: - - install_requires=[ - 'cmd2>=1,<2', - ] - -The ``cmd2`` project uses `Semantic Versioning `_, which -means that any incompatible API changes will be release with a new major -version number. The public API is documented in the :ref:`api/index:API -Reference`. - -We recommend that you follow the advice given by the Python Packaging User -Guide related to `install_requires -`_. -By setting an upper bound on the allowed version, you can ensure that your -project does not inadvertently get installed with an incompatible future -version of ``cmd2``. - - -Windows Considerations ----------------------- - -If you would like to use :ref:`features/completion:Completion`, and you want -your application to run on Windows, you will need to ensure you install the -``pyreadline3`` package. Make sure to include the following -in your ``setup.py``:: - - install_requires=[ - 'cmd2>=1,<2', - ":sys_platform=='win32'": ['pyreadline3'], - ] diff --git a/docs/overview/resources.md b/docs/overview/resources.md new file mode 100644 index 000000000..db42e935e --- /dev/null +++ b/docs/overview/resources.md @@ -0,0 +1,8 @@ +# Resources + +Project related links and other resources: + +- [cmd](https://docs.python.org/3/library/cmd.html) +- [cmd2 project page](https://github.com/python-cmd2/cmd2) +- [project bug tracker](https://github.com/python-cmd2/cmd2/issues) +- PyOhio 2019: [slides](https://github.com/python-cmd2/talks/blob/master/PyOhio_2019/cmd2-PyOhio_2019.pdf), [video](https://www.youtube.com/watch?v=pebeWrTqIIw), [examples](https://github.com/python-cmd2/talks/tree/master/PyOhio_2019/examples) diff --git a/docs/overview/resources.rst b/docs/overview/resources.rst deleted file mode 100644 index 142dbd200..000000000 --- a/docs/overview/resources.rst +++ /dev/null @@ -1,13 +0,0 @@ -Resources -========= - -.. _cmd: https://docs.python.org/3/library/cmd.html -.. _`cmd2 project page`: https://github.com/python-cmd2/cmd2 -.. _`project bug tracker`: https://github.com/python-cmd2/cmd2/issues - -Project related links and other resources: - -* cmd_ -* `cmd2 project page`_ -* `project bug tracker`_ -* PyOhio 2019: `slides `_, `video `_, `examples `_ diff --git a/docs/overview/summary.rst b/docs/overview/summary.rst deleted file mode 100644 index 9285014c0..000000000 --- a/docs/overview/summary.rst +++ /dev/null @@ -1,22 +0,0 @@ - -.. _cmd: https://docs.python.org/3/library/cmd.html - -Building a new `REPL `_ or -`Command Line Interface `_ -application? - -Already built an application that uses cmd_ from the python standard library -and want to add more functionality with very little work? - -``cmd2`` is a powerful python library for building command line applications. -Start here to find out if this library is a good fit for your needs. - -* :ref:`overview/installation:Installation Instructions` - how to install - ``cmd2`` and associated optional dependencies -* :ref:`examples/first_app:First Application` - a sample application showing 8 - key features of ``cmd2`` -* :ref:`overview/integrating:Integrate cmd2 Into Your Project` - adding - ``cmd2`` to your project -* :ref:`overview/alternatives:Alternatives` - other python packages that might - meet your needs -* :ref:`overview/resources:Resources` - related links and other materials diff --git a/docs/plugins/external_test.md b/docs/plugins/external_test.md new file mode 100644 index 000000000..247f6c829 --- /dev/null +++ b/docs/plugins/external_test.md @@ -0,0 +1,62 @@ +# External Test Plugin + +## Overview + +The [External Test Plugin](https://github.com/python-cmd2/cmd2/tree/master/plugins/ext_test) supports testing of a cmd2 application by exposing access cmd2 commands with the same context as from within a cmd2 [Python Scripts](../features/scripting.md#scripting-python-scripts). This interface captures `stdout`, `stderr`, as well as any application-specific data returned by the command. This also allows for verification of an application's support for [Python Scripts](../features/scripting.md#scripting-python-scripts) and enables the cmd2 application to be tested as part of a larger system integration test. + +## Example cmd2 Application + +The following short example shows how to mix in the external test plugin to create a fixture for testing your cmd2 application. + +Define your cmd2 application + +``` python +import cmd2 +class ExampleApp(cmd2.Cmd): + """An class to show how to use a plugin""" + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, **kwargs) + + def do_something(self, arg): + self.last_result = 5 + self.poutput('this is the something command') +``` + +## Defining the test fixture + +In your test, define a fixture for your cmd2 application + +``` python +import cmd2_ext_test +import pytest + +class ExampleAppTester(cmd2_ext_test.ExternalTestMixin, ExampleApp): + def __init__(self, *args, **kwargs): + # gotta have this or neither the plugin or cmd2 will initialize + super().__init__(*args, **kwargs) + +@pytest.fixture +def example_app(): + app = ExampleAppTester() + app.fixture_setup() + yield app + app.fixture_teardown() +``` + +## Writing Tests + +Now write your tests that validate your application using the `~cmd2_ext_test.ExternalTestMixin.app_cmd()`{.interpreted-text role="meth"} function to access the cmd2 application's commands. This allows invocation of the application's commands in the same format as a user would type. The results from calling a command matches what is returned from running an python script with cmd2's [run_pyscript](../features/builtin_commands.md#feature-builtin-commands-run-pyscript) command, which provides `stdout`, `stderr`, and the command's result data. + +``` python +from cmd2 import CommandResult + +def test_something(example_app): + # execute a command + out = example_app.app_cmd("something") + + # validate the command output and result data + assert isinstance(out, CommandResult) + assert str(out.stdout).strip() == 'this is the something command' + assert out.data == 5 +``` diff --git a/docs/plugins/external_test.rst b/docs/plugins/external_test.rst deleted file mode 100644 index 8427a8be9..000000000 --- a/docs/plugins/external_test.rst +++ /dev/null @@ -1,82 +0,0 @@ -External Test Plugin -==================== - -Overview -~~~~~~~~ - -.. _cmd2_external_test_plugin: - https://github.com/python-cmd2/cmd2/tree/master/plugins/ext_test - -The `External Test Plugin `_ supports testing of a cmd2 application by exposing access cmd2 -commands with the same context as from within a cmd2 :ref:`Python Scripts `. This interface -captures ``stdout``, ``stderr``, as well as any application-specific data returned by the command. This also allows -for verification of an application's support for :ref:`Python Scripts ` and enables the cmd2 -application to be tested as part of a larger system integration test. - - -Example cmd2 Application -~~~~~~~~~~~~~~~~~~~~~~~~ - -The following short example shows how to mix in the external test plugin to create a fixture for testing -your cmd2 application. - -Define your cmd2 application - -.. code-block:: python - - import cmd2 - class ExampleApp(cmd2.Cmd): - """An class to show how to use a plugin""" - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - def do_something(self, arg): - self.last_result = 5 - self.poutput('this is the something command') - -Defining the test fixture -~~~~~~~~~~~~~~~~~~~~~~~~~ - -In your test, define a fixture for your cmd2 application - -.. code-block:: python - - import cmd2_ext_test - import pytest - - class ExampleAppTester(cmd2_ext_test.ExternalTestMixin, ExampleApp): - def __init__(self, *args, **kwargs): - # gotta have this or neither the plugin or cmd2 will initialize - super().__init__(*args, **kwargs) - - @pytest.fixture - def example_app(): - app = ExampleAppTester() - app.fixture_setup() - yield app - app.fixture_teardown() - - -Writing Tests -~~~~~~~~~~~~~ - -Now write your tests that validate your application using the :meth:`~cmd2_ext_test.ExternalTestMixin.app_cmd()` -function to access the cmd2 application's commands. This allows invocation of the application's commands in the -same format as a user would type. The results from calling a command matches what is returned -from running an python script with cmd2's :ref:`feature-builtin-commands-run-pyscript` command, which provides -``stdout``, ``stderr``, and the command's result data. - -.. code-block:: python - - from cmd2 import CommandResult - - def test_something(example_app): - # execute a command - out = example_app.app_cmd("something") - - # validate the command output and result data - assert isinstance(out, CommandResult) - assert str(out.stdout).strip() == 'this is the something command' - assert out.data == 5 - diff --git a/docs/plugins/index.md b/docs/plugins/index.md new file mode 100644 index 000000000..0059915fe --- /dev/null +++ b/docs/plugins/index.md @@ -0,0 +1,7 @@ +# Plugins + + + +- [External Test Plugin](external_test.md) + + diff --git a/docs/plugins/index.rst b/docs/plugins/index.rst deleted file mode 100644 index 23221c3de..000000000 --- a/docs/plugins/index.rst +++ /dev/null @@ -1,7 +0,0 @@ -Plugins -======== - -.. toctree:: - :maxdepth: 1 - - external_test diff --git a/docs/requirements.txt b/docs/requirements.txt index 58a108b52..f680db57c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -1,8 +1,7 @@ - +mkdocs-include-markdown-plugin +mkdocs-macros-plugin +mkdocs-material pyperclip setuptools setuptools-scm -Sphinx -sphinx-autobuild -sphinx-rtd-theme wcwidth diff --git a/docs/testing.md b/docs/testing.md new file mode 100644 index 000000000..6a00c1bff --- /dev/null +++ b/docs/testing.md @@ -0,0 +1,21 @@ +# Testing + +
+ +
+ +## Overview + +This covers special considerations when writing unit tests for a cmd2 application. + +## Testing Commands + +The [External Test Plugin](plugins/external_test.md) provides a mixin class with an :meth:`` function that allows external calls to application commands. The :meth:`~cmd2_ext_test.ExternalTestMixin.app_cmd()` function captures and returns stdout, stderr, and the command-specific result data. Mocking ~~~~~~~ .. _python_mock_autospeccing: https://docs.python.org/3/library/unittest.mock.html#autospeccing .. _python_mock_patch: https://docs.python.org/3/library/unittest.mock.html#patch If you need to mock anything in your cmd2 application, and most specifically in sub-classes of :class:`~cmd2.Cmd` or :class:`~cmd2.command_definition.CommandSet`, you must use `Autospeccing `_, `spec=True `_, or whatever equivalent is provided in the mocking library you're using. In order to automatically load functions as commands cmd2 performs a number of reflection calls to look up attributes of classes defined in your cmd2 application. Many mocking libraries will automatically create mock objects to match any attribute being requested, regardless of whether they're present in the object being mocked. This behavior can incorrectly instruct cmd2 to treat a function or attribute as something it needs to recognize and process. To prevent this, you should always mock with `Autospeccing `_ or `spec=True `_ enabled. If you don't have autospeccing on, your unit tests will failing with an error message like:: cmd2.exceptions.CommandSetRegistrationError: Subcommand is not valid: must be a string. Received instead Examples ~~~~~~~~ .. code-block:: python def test_mocked_methods(): with mock.patch.object(MockMethodApp, 'foo', spec=True): cli = MockMethodApp() Another one using `pytest-mock `_ to provide a ``mocker`` fixture: + +``` python +def test_mocked_methods2(mocker): + mock_cmdloop = mocker.patch("cmd2.Cmd.cmdloop", autospec=True) + cli = cmd2.Cmd() + cli.cmdloop() + assert mock_cmdloop.call_count == 1 +``` diff --git a/docs/testing.rst b/docs/testing.rst deleted file mode 100644 index 1a5163b43..000000000 --- a/docs/testing.rst +++ /dev/null @@ -1,70 +0,0 @@ -Testing -======= - -.. toctree:: - :maxdepth: 1 - -Overview -~~~~~~~~ - -This covers special considerations when writing unit tests for a cmd2 application. - - -Testing Commands -~~~~~~~~~~~~~~~~ - -The :doc:`External Test Plugin ` provides a mixin class with an :meth:`` function that -allows external calls to application commands. The :meth:`~cmd2_ext_test.ExternalTestMixin.app_cmd()` function captures -and returns stdout, stderr, and the command-specific result data. - - -Mocking -~~~~~~~ - -.. _python_mock_autospeccing: - https://docs.python.org/3/library/unittest.mock.html#autospeccing -.. _python_mock_patch: - https://docs.python.org/3/library/unittest.mock.html#patch - -If you need to mock anything in your cmd2 application, and most specifically in -sub-classes of :class:`~cmd2.Cmd` or -:class:`~cmd2.command_definition.CommandSet`, you must use `Autospeccing -`_, `spec=True `_, or whatever -equivalent is provided in the mocking library you're using. - -In order to automatically load functions as commands cmd2 performs a number of -reflection calls to look up attributes of classes defined in your cmd2 -application. Many mocking libraries will automatically create mock objects to -match any attribute being requested, regardless of whether they're present in -the object being mocked. This behavior can incorrectly instruct cmd2 to treat a -function or attribute as something it needs to recognize and process. To -prevent this, you should always mock with `Autospeccing -`_ or `spec=True `_ enabled. - -If you don't have autospeccing on, your unit tests will failing with an error -message like:: - - cmd2.exceptions.CommandSetRegistrationError: Subcommand - is not - valid: must be a string. Received instead - - -Examples -~~~~~~~~ - -.. code-block:: python - - def test_mocked_methods(): - with mock.patch.object(MockMethodApp, 'foo', spec=True): - cli = MockMethodApp() - -Another one using `pytest-mock `_ to -provide a ``mocker`` fixture: - -.. code-block:: python - - def test_mocked_methods2(mocker): - mock_cmdloop = mocker.patch("cmd2.Cmd.cmdloop", autospec=True) - cli = cmd2.Cmd() - cli.cmdloop() - assert mock_cmdloop.call_count == 1 diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 000000000..7961a1a29 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,191 @@ +# Project information +site_name: cmd2 +site_description: cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python. +site_dir: build/html +site_url: https://cmd2.readthedocs.io/ + +# Repository +repo_name: cmd2 +repo_url: https://github.com/python-cmd2/cmd2 +edit_uri: edit/main/docs + +# Copyright +copyright: Copyright © 2010-2024, cmd2 contributors. + +# Configuration +theme: + name: material + language: en + icon: + repo: fontawesome/brands/github + logo: material/home + edit: material/pencil + view: material/eye + palette: + # Palette toggle for light mode + - media: "(prefers-color-scheme: light)" + scheme: default + primary: light blue + accent: deep orange + toggle: + icon: material/weather-night + name: Switch to dark mode + # Palette toggle for dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: blue + accent: deep purple + toggle: + icon: material/weather-sunny + name: Switch to light mode + features: + - announce.dismiss + - content.action.view + - content.action.edit + - content.code.copy + - content.tabs.link + - navigation.footer + - navigation.indexes + - navigation.prune + - navigation.sections + - navigation.tabs + - navigation.top + - navigation.tracking + - toc.follow + - toc.integrate + +# Plugins +plugins: + - search + - include-markdown: + preserve_includer_indent: true + dedent: true + comments: false + - macros: + render_by_default: false + on_error_fail: true + on_undefined: strict + j2_block_start_string: "[[%" + j2_block_end_string: "%]]" + - mkdocstrings + +# Customizations +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/python-cmd2/cmd2 + - icon: simple/pypi + link: https://pypi.org/project/cmd2/ + version: "2.5" + +# Extensions +# - These are carefully chosen to work with pandoc markdown support for whole document translation +markdown_extensions: + - admonition + - attr_list + - def_list + - md_in_html + - tables + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + options: + custom_icons: + - overrides/.icons + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.smartsymbols + - pymdownx.superfences: + - pymdownx.tabbed: + alternate_style: true + +# Validation +validation: + nav: + omitted_files: info + not_found: warn + absolute_links: info + links: + not_found: warn + absolute_links: info + unrecognized_links: info + +not_in_nav: | + **/download/*.md + +# Page tree +nav: + - Home: index.md + - Getting Started: + - Overview: overview/index.md + - Installation Instructions: overview/installation.md + - Integrate cmd2 Into Your Project: overview/integrating.md + - Alternatives: overview/alternatives.md + - Resources: overview/resources.md + - Migrating From cmd: + - migrating/index.md + - migrating/why.md + - migrating/incompatibilities.md + - migrating/minimum.md + - migrating/next_steps.md + - Features: + - features/index.md + - features/argument_processing.md + - features/builtin_commands.md + - features/clipboard.md + - features/commands.md + - features/completion.md + - features/disable_commands.md + - features/embedded_python_shells.md + - features/generating_output.md + - features/help.md + - features/history.md + - features/hooks.md + - features/initialization.md + - features/misc.md + - features/modular_commands.md + - features/multiline_commands.md + - features/os.md + - features/packaging.md + - features/plugins.md + - features/prompt.md + - features/redirection.md + - features/scripting.md + - features/settings.md + - features/shortcuts_aliases_macros.md + - features/startup_commands.md + - features/table_creation.md + - features/transcripts.md + - Examples: + - examples/index.md + - examples/first_app.md + - examples/alternate_event_loops.md + - Plugins: + - plugins/index.md + - plugins/external_test.md + - Testing: + - testing.md + - API Reference: + - api/index.md + - api/cmd.md + - api/ansi.md + - api/argparse_completer.md + - api/argparse_custom.md + - api/constants.md + - api/command_definition.md + - api/decorators.md + - api/exceptions.md + - api/history.md + - api/parsing.md + - api/plugin.md + - api/py_bridge.md + - api/table_creator.md + - api/utils.md + - api/plugin_external_test.md + - Meta: + - doc_conventions.md diff --git a/pyproject.toml b/pyproject.toml index 8d9192ebb..bbcedadbc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,14 +9,7 @@ description = "cmd2 - quickly build feature-rich and user-friendly interactive c authors = [{ name = "cmd2 Contributors" }] readme = "README.md" requires-python = ">=3.8" -keywords = [ - "CLI", - "cmd", - "command", - "interactive", - "prompt", - "Python", -] +keywords = ["CLI", "cmd", "command", "interactive", "prompt", "Python"] license = { file = "LICENSE" } classifiers = [ "Development Status :: 5 - Production/Stable", @@ -42,83 +35,46 @@ dependencies = [ ] [project.optional-dependencies] -build = [ - "build", - "setuptools", - "setuptools-scm", -] +build = ["build", "setuptools", "setuptools-scm"] dev = [ "codecov", - "doc8", "invoke", + "mkdocs-include-markdown-plugin", + "mkdocs-macros-plugin", + "mkdocs-material", + "mkdocstrings[python]", "mypy", "pytest", "pytest-cov", "pytest-mock", - "sphinx", - "sphinx-autobuild", - "sphinx-rtd-theme", "ruff", "twine", ] docs = [ + "mkdocs-include-markdown-plugin", + "mkdocs-macros-plugin", + "mkdocs-material", + "mkdocstrings[python]", "setuptools", "setuptools_scm", - "sphinx", - "sphinx-autobuild", - "sphinx-rtd-theme", -] -test = [ - "codecov", - "coverage", - "pytest", - "pytest-cov", - "pytest-mock", -] -validate = [ - "mypy", - "ruff", - "types-setuptools", ] - -[tool.doc8] -ignore-path = [ - "__pycache__", - "*.egg", - ".git", - ".idea", - ".nox", - ".pytest_cache", - ".tox", - ".venv", - ".vscode", - "build", - "cmd2", - "cmd2.egg-info", - "dist", - "docs/_build", - "examples", - "htmlcov", - "plugins", - "tests", -] -max-line-length = 120 -verbose = 0 +test = ["codecov", "coverage", "pytest", "pytest-cov", "pytest-mock"] +validate = ["mypy", "ruff", "types-setuptools"] [tool.mypy] disallow_incomplete_defs = true disallow_untyped_calls = true disallow_untyped_defs = true exclude = [ - "^build/", # .build directory - "^docs/", # docs directory - "^examples/", # examples directory - "^plugins/*", # plugins directory - "^noxfile\\.py$", # nox config file - "setup\\.py$", # any files named setup.py - "^tasks\\.py$", # tasks.py invoke config file - "^tests/", # tests directory - "^tests_isolated/" # tests_isolated directory + "^build/", # .build directory + "^docs/", # docs directory + "^examples/", # examples directory + "^plugins/*", # plugins directory + "^noxfile\\.py$", # nox config file + "setup\\.py$", # any files named setup.py + "^tasks\\.py$", # tasks.py invoke config file + "^tests/", # tests directory + "^tests_isolated/", # tests_isolated directory ] show_column_numbers = true show_error_codes = true @@ -130,9 +86,7 @@ warn_unreachable = true warn_unused_ignores = false [tool.pytest.ini_options] -testpaths = [ - "tests", -] +testpaths = ["tests"] addopts = [ "--cov=cmd2", "--cov-append", @@ -201,7 +155,7 @@ select = [ # "EM", # flake8-errmsg # "ERA", # eradicate # "EXE", # flake8-executable - "F", # Pyflakes + "F", # Pyflakes "FA", # flake8-future-annotations # "FBT", # flake8-boolean-trap "G", # flake8-logging-format @@ -212,7 +166,7 @@ select = [ # "ISC", # flake8-implicit-str-concat # "N", # pep8-naming "NPY", # NumPy-specific rules - "PD", # pandas-vet + "PD", # pandas-vet # "PGH", # pygrep-hooks # "PIE", # flake8-pie # "PL", # Pylint @@ -238,21 +192,21 @@ select = [ ] ignore = [ # `ruff rule S101` for a description of that rule - "B904", # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME - "B905", # `zip()` without an explicit `strict=` parameter -- FIX ME - "E501", # Line too long - "EM101", # Exception must not use a string literal, assign to variable first - "EXE001", # Shebang is present but file is not executable -- DO NOT FIX - "G004", # Logging statement uses f-string + "B904", # Within an `except` clause, raise exceptions with `raise ... from err` -- FIX ME + "B905", # `zip()` without an explicit `strict=` parameter -- FIX ME + "E501", # Line too long + "EM101", # Exception must not use a string literal, assign to variable first + "EXE001", # Shebang is present but file is not executable -- DO NOT FIX + "G004", # Logging statement uses f-string "PLC1901", # `{}` can be simplified to `{}` as an empty string is falsey - "PLW060", # Using global for `{name}` but no assignment is done -- DO NOT FIX + "PLW060", # Using global for `{name}` but no assignment is done -- DO NOT FIX "PLW2901", # PLW2901: Redefined loop variable -- FIX ME - "PT011", # `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception - "PT018", # Assertion should be broken down into multiple parts - "S101", # Use of `assert` detected -- DO NOT FIX - "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME - "SLF001", # Private member accessed: `_Iterator` -- FIX ME - "UP038", # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX + "PT011", # `pytest.raises(Exception)` is too broad, set the `match` parameter or use a more specific exception + "PT018", # Assertion should be broken down into multiple parts + "S101", # Use of `assert` detected -- DO NOT FIX + "S311", # Standard pseudo-random generators are not suitable for cryptographic purposes -- FIX ME + "SLF001", # Private member accessed: `_Iterator` -- FIX ME + "UP038", # Use `X | Y` in `{}` call instead of `(X, Y)` -- DO NOT FIX ] # Allow fix for all enabled rules (when `--fix`) is provided. @@ -322,16 +276,16 @@ dev-dependencies = [ "build", "cmd2-ext-test", "codecov", - "doc8", "invoke", + "mkdocs-include-markdown-plugin", + "mkdocs-macros-plugin", + "mkdocs-material", + "mkdocstrings[python]", "mypy", "pytest", "pytest-cov", "pytest-mock", "ruff", - "sphinx", - "sphinx-autobuild", - "sphinx-rtd-theme", "ruff", "twine", ] diff --git a/tasks.py b/tasks.py index e4ea17dfc..6d95b3503 100644 --- a/tasks.py +++ b/tasks.py @@ -122,32 +122,20 @@ def mypy_clean(context): # documentation # ##### -DOCS_SRCDIR = 'docs' -DOCS_BUILDDIR = os.path.join('docs', '_build') -SPHINX_OPTS = '-nvWT' # Be nitpicky, verbose, and treat warnings as errors +DOCS_BUILDDIR = 'build' +MKDOCS_OPTS = '-nvWT' # Be nitpicky, verbose, and treat warnings as errors @invoke.task() def docs(context, builder='html'): - """Build documentation using sphinx""" + """Build documentation using MkDocs""" with context.cd(TASK_ROOT_STR): - cmdline = 'python -msphinx -M {} {} {} {}'.format(builder, DOCS_SRCDIR, DOCS_BUILDDIR, SPHINX_OPTS) - context.run(cmdline, pty=True) + context.run('mkdocs build', pty=True) namespace.add_task(docs) -@invoke.task() -def doc8(context): - """Check documentation with doc8""" - with context.cd(TASK_ROOT_STR): - context.run('doc8 docs --ignore-path docs/_build --ignore-path docs/.nox') - - -namespace.add_task(doc8) - - @invoke.task def docs_clean(context): """Remove rendered documentation""" @@ -159,24 +147,11 @@ def docs_clean(context): namespace_clean.add_task(docs_clean, name='docs') -@invoke.task() -def linkcheck(context): - """Check external links in Sphinx documentation for integrity.""" - with context.cd(str(TASK_ROOT / 'docs')): - context.run('make linkcheck', pty=True) - - -namespace.add_task(linkcheck) - - @invoke.task def livehtml(context): """Launch webserver on http://localhost:8000 with rendered documentation""" with context.cd(TASK_ROOT_STR): - builder = 'html' - outputdir = os.path.join(DOCS_BUILDDIR, builder) - cmdline = 'sphinx-autobuild -b {} {} {}'.format(builder, DOCS_SRCDIR, outputdir) - context.run(cmdline, pty=True) + context.run('mkdocs serve', pty=True) namespace.add_task(livehtml)