cmd2-0.8.5/0000755000076500000240000000000013264716261014337 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/PKG-INFO0000644000076500000240000000647013264716261015443 0ustar toddleonhardtstaff00000000000000Metadata-Version: 1.1 Name: cmd2 Version: 0.8.5 Summary: cmd2 - a tool for building interactive command line applications in Python Home-page: https://github.com/python-cmd2/cmd2 Author: Catherine Devlin Author-email: catherine.devlin@gmail.com License: MIT Description-Content-Type: UNKNOWN Description: cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It provides a simple API which is an extension of Python's built-in cmd module. cmd2 provides a wealth of features on top of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary when using cmd. The latest documentation for cmd2 can be read online here: https://cmd2.readthedocs.io/ Main features: - Searchable command history (`history` command and `+r`) - Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`) - Python scripting of your application with ``pyscript`` - Run shell commands with ``!`` - Pipe command output to shell commands with `|` - Redirect command output to file with `>`, `>>`; input from file with `<` - Bare `>`, `>>` with no filename send output to paste buffer (clipboard) - `py` enters interactive Python console (opt-in `ipy` for IPython console) - Multi-line commands - Special-character command shortcuts (beyond cmd's `?` and `!`) - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for sub-commands - Unicode character support (*Python 3 only*) - Good tab-completion of commands, sub-commands, file system paths, and shell commands - Python 2.7 and 3.4+ support - Linux, macOS and Windows support - Trivial to provide built-in help for all commands - Built-in regression testing framework for your applications (transcript-based testing) - Transcripts for use with built-in regression can be automatically generated from `history -t` Usable without modification anywhere cmd is used; simply import cmd2.Cmd in place of cmd.Cmd. Keywords: command prompt console cmd Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Operating System :: OS Independent Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules cmd2-0.8.5/LICENSE0000644000076500000240000000211313043433273015333 0ustar toddleonhardtstaff00000000000000The MIT License (MIT) Copyright (c) 2008-2016 Catherine Devlin and others Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. cmd2-0.8.5/CHANGELOG.md0000644000076500000240000004527413264702441016157 0ustar toddleonhardtstaff00000000000000## 0.8.5 (April 15, 2018) * Bug Fixes * Fixed a bug with all argument decorators where the wrapped function wasn't returning a value and thus couldn't cause the cmd2 app to quit * Enhancements * Added support for verbose help with -v where it lists a brief summary of what each command does * Added support for categorizing commands into groups within the help menu * See the [Grouping Commands](http://cmd2.readthedocs.io/en/latest/argument_processing.html?highlight=verbose#grouping-commands) section of the docs for more info * See [help_categories.py](https://github.com/python-cmd2/cmd2/blob/master/examples/help_categories.py) for an example * Tab completion of paths now supports ~user user path expansion * Simplified implementation of various tab completion functions so they no longer require ``ctypes`` * Expanded documentation of ``display_matches`` list to clarify its purpose. See cmd2.py for this documentation. * Adding opening quote to tab completion if any of the completion suggestions have a space. * **Python 2 EOL notice** * This is the last release where new features will be added to ``cmd2`` for Python 2.7 * The 0.9.0 release of ``cmd2`` will support Python 3.4+ only * Additional 0.8.x releases may be created to supply bug fixes for Python 2.7 up until August 31, 2018 * After August 31, 2018 not even bug fixes will be provided for Python 2.7 ## 0.8.4 (April 10, 2018) * Bug Fixes * Fixed conditional dependency issue in setup.py that was in 0.8.3. ## 0.8.3 (April 09, 2018) * Bug Fixes * Fixed ``help`` command not calling functions for help topics * Fixed not being able to use quoted paths when redirecting with ``<`` and ``>`` * Enhancements * Tab completion has been overhauled and now supports completion of strings with quotes and spaces. * Tab completion will automatically add an opening quote if a string with a space is completed. * Added ``delimiter_complete`` function for tab completing delimited strings * Added more control over tab completion behavior including the following flags. The use of these flags is documented in cmd2.py * ``allow_appended_space`` * ``allow_closing_quote`` * Due to the tab completion changes, non-Windows platforms now depend on [wcwidth](https://pypi.python.org/pypi/wcwidth). * An alias name can now match a command name. * An alias can now resolve to another alias. * Attribute Changes (Breaks backward compatibility) * ``exclude_from_help`` is now called ``hidden_commands`` since these commands are hidden from things other than help, including tab completion * This list also no longer takes the function names of commands (``do_history``), but instead uses the command names themselves (``history``) * ``excludeFromHistory`` is now called ``exclude_from_history`` * ``cmd_with_subs_completer()`` no longer takes an argument called ``base``. Adding tab completion to subcommands has been simplified to declaring it in the subcommand parser's default settings. This easily allows arbitrary completers like path_complete to be used. See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use tab completion in subcommands. In addition, the docstring for ``cmd_with_subs_completer()`` offers more details. ## 0.8.2 (March 21, 2018) * Bug Fixes * Fixed a bug in tab-completion of command names within sub-menus * Fixed a bug when using persistent readline history in Python 2.7 * Fixed a bug where the ``AddSubmenu`` decorator didn't work with a default value for ``shared_attributes`` * Added a check to ``ppaged()`` to only use a pager when running in a real fully functional terminal * Enhancements * Added [quit_on_sigint](http://cmd2.readthedocs.io/en/latest/settingchanges.html#quit-on-sigint) attribute to enable canceling current line instead of quitting when Ctrl+C is typed * Added possibility of having readline history preservation in a SubMenu * Added [table_display.py](https://github.com/python-cmd2/cmd2/blob/master/examples/table_display.py) example to demonstrate how to display tabular data * Added command aliasing with ``alias`` and ``unalias`` commands * Added the ability to load an initialization script at startup * See [alias_startup.py](https://github.com/python-cmd2/cmd2/blob/master/examples/alias_startup.py) for an example * Added a default SIGINT handler which terminates any open pipe subprocesses and re-raises a KeyboardInterrupt * For macOS, will load the ``gnureadline`` module if available and ``readline`` if not ## 0.8.1 (March 9, 2018) * Bug Fixes * Fixed a bug if a non-existent **do_*** method was added to the ``exclude_from_help`` list * Fixed a bug in a unit test which would fail if your home directory was empty on a Linux system * Fixed outdated help text for the **edit** command * Fixed outdated [remove_unused.py](https://github.com/python-cmd2/cmd2/blob/master/examples/remove_unused.py) * Enhancements * Added support for sub-menus. * See [submenus.py](https://github.com/python-cmd2/cmd2/blob/master/examples/submenus.py) for an example of how to use it * Added option for persistent readline history * See [persistent_history.py](https://github.com/python-cmd2/cmd2/blob/master/examples/persistent_history.py) for an example * See the [Searchable command history](http://cmd2.readthedocs.io/en/latest/freefeatures.html#searchable-command-history) section of the documentation for more info * Improved PyPI packaging by including unit tests and examples in the tarball * Improved documentation to make it more obvious that **poutput()** should be used instead of **print()** * ``exclude_from_help`` and ``excludeFromHistory`` are now instance instead of class attributes * Added flag and index based tab completion helper functions * See [tab_completion.py](https://github.com/python-cmd2/cmd2/blob/master/examples/tab_completion.py) * Added support for displaying output which won't fit on the screen via a pager using ``ppaged()`` * See [paged_output.py](https://github.com/python-cmd2/cmd2/blob/master/examples/paged_output.py) * Attributes Removed (**can cause breaking changes**) * ``abbrev`` - Removed support for abbreviated commands * Good tab completion makes this unnecessary and its presence could cause harmful unintended actions * ``case_insensitive`` - Removed support for case-insensitive command parsing * Its presence wasn't very helpful and could cause harmful unintended actions ## 0.8.0 (February 1, 2018) * Bug Fixes * Fixed unit tests on Python 3.7 due to changes in how re.escape() behaves in Python 3.7 * Fixed a bug where unknown commands were getting saved in the history * Enhancements * Three new decorators for **do_*** commands to make argument parsing easier * **with_argument_list** decorator to change argument type from str to List[str] * **do_*** commands get a single argument which is a list of strings, as pre-parsed by shlex.split() * **with_arparser** decorator for strict argparse-based argument parsing of command arguments * **do_*** commands get a single argument which is the output of argparse.parse_args() * **with_argparser_and_unknown_args** decorator for argparse-based argument parsing, but allows unknown args * **do_*** commands get two arguments, the output of argparse.parse_known_args() * See the [Argument Processing](http://cmd2.readthedocs.io/en/latest/argument_processing.html) section of the documentation for more information on these decorators * Alternatively, see the [argparse_example.py](https://github.com/python-cmd2/cmd2/blob/master/examples/argparse_example.py) and [arg_print.py](https://github.com/python-cmd2/cmd2/blob/master/examples/arg_print.py) examples * Added support for Argparse sub-commands when using the **with_argument_parser** or **with_argparser_and_unknown_args** decorators * See [subcommands.py](https://github.com/python-cmd2/cmd2/blob/master/examples/subcommands.py) for an example of how to use subcommands * Tab-completion of sub-command names is automatically supported * The **__relative_load** command is now hidden from the help menu by default * This command is not intended to be called from the command line, only from within scripts * The **set** command now has an additional **-a/--all** option to also display read-only settings * The **history** command can now run, edit, and save prior commands, in addition to displaying prior commands. * The **history** command can now automatically generate a transcript file for regression testing * This makes creating regression tests for your ``cmd2`` application trivial * Commands Removed * The **cmdenvironment** has been removed and its functionality incorporated into the **-a/--all** argument to **set** * The **show** command has been removed. Its functionality has always existing within **set** and continues to do so * The **save** command has been removed. The capability to save commands is now part of the **history** command. * The **run** command has been removed. The capability to run prior commands is now part of the **history** command. * Other changes * The **edit** command no longer allows you to edit prior commands. The capability to edit prior commands is now part of the **history** command. The **edit** command still allows you to edit arbitrary files. * the **autorun_on_edit** setting has been removed. * For Python 3.4 and earlier, ``cmd2`` now has an additional dependency on the ``contextlib2`` module * Deprecations * The old **options** decorator for optparse-based argument parsing is now *deprecated* * The old decorator is still present for now, but will be removed in a future release * ``cmd2`` no longer includes **optparse.make_option**, so if your app needs it import directly from optparse ## 0.7.9 (January 4, 2018) * Bug Fixes * Fixed a couple broken examples * Enhancements * Improved documentation for modifying shortcuts (command aliases) * Made ``pyreadline`` a dependency on Windows to ensure tab-completion works * Other changes * Abandoned official support for Python 3.3. It should still work, just don't have an easy way to test it anymore. ## 0.7.8 (November 8, 2017) * Bug Fixes * Fixed ``poutput()`` so it can print an integer zero and other **falsy** things * Fixed a bug which was causing autodoc to fail for building docs on Readthedocs * Fixed bug due to ``pyperclip`` dependency radically changing its project structure in latest version * Enhancements * Improved documentation for user-settable environment parameters * Improved documentation for overriding the default supported comment styles * Added ``runcmds_plus_hooks()`` method to run multiple commands w/o a cmdloop ## 0.7.7 (August 25, 2017) * Bug Fixes * Added workaround for bug which occurs in Python 2.7 on Linux when ``pygtk`` is installed * ``pfeedback()`` now honors feedback_to_output setting and won't redirect when it is ``False`` * For ``edit`` command, both **editor** and **filename** can now have spaces in the name/path * Fixed a bug which occurred when stdin was a pipe instead of a tty due to input redirection * Enhancements * ``feedback_to_output`` now defaults to ``False`` so info like command timing won't redirect * Transcript regular expressions now have predictable, tested, and documented behavior * This makes a breaking change to the format and expectations of transcript testing * The prior behavior removed whitespace before making the comparison, now whitespace must match exactly * Prior version did not allow regexes with whitespace, new version allows any regex * Improved display for ``load`` command and input redirection when **echo** is ``True`` ## 0.7.6 (August 11, 2017) * Bug Fixes * Case-sensitive command parsing was completely broken and has been fixed * ``+d`` now properly quits when case-sensitive command parsing is enabled * Fixed some pyperclip clipboard interaction bugs on Linux * Fixed some timing bugs when running unit tests in parallel by using monkeypatch * Enhancements * Enhanced tab-completion of cmd2 command names to support case-insensitive completion * Added an example showing how to remove unused commands * Improved how transcript testing handles prompts with ANSI escape codes by stripping them * Greatly improved implementation for how command output gets piped to a shell command ## 0.7.5 (July 8, 2017) * Bug Fixes * `case_insensitive` is no longer a runtime-settable parameter, but it was still listed as such * Fixed a recursive loop bug when abbreviated commands are enabled and it could get stuck in the editor forever * Added additional command abbreviations to the "exclude from history" list * Fixed argparse_example.py and pirate.py examples and transcript_regex.txt transcript * Fixed a bug in a unit test which occurred under unusual circumstances * Enhancements * Organized all attributes used to configure the ParserManager into a single location * Set the default value of `abbrev` to `False` (which controls whether or not abbreviated commands are allowed) * With good tab-completion of command names, using abbreviated commands isn't particularly useful * And it can create complications if you are't careful * Improved implementation of `load` to use command queue instead of nested inner loop ## 0.7.4 (July 3, 2017) * Bug fixes * Fixed a couple bugs in interacting with pastebuffer/clipboard on macOS and Linux * Fixed a couple bugs in edit and save commands if called when history is empty * Ability to pipe ``cmd2`` command output to a shell command is now more reliable, particularly on Windows * Fixed a bug in ``pyscript`` command on Windows related to ``\`` being interpreted as an escape * Enhancements * Ensure that path and shell command tab-completion results are alphabetically sorted * Removed feature for load command to load scripts from URLS * It didn't work, there were no unit tests, and it felt out of place * Removed presence of a default file name and default file extension * These also strongly felt out of place * ``load`` and ``_relative_load`` now require a file path * ``edit`` and ``save`` now use a temporary file if a file path isn't provided * ``load`` command has better error checking and reporting * Clipboard copy and paste functionality is now handled by the **pyperclip** module * ``shell`` command now supports redirection and piping of output * Added a lot of unit tests * Other changes * Removed pause command * Added a dependency on the **pyperclip** module ## 0.7.3 (June 23, 2017) * Bug fixes * Fixed a bug in displaying a span of history items when only an end index is supplied * Fixed a bug which caused transcript test failures to display twice * Enhancements * Added the ability to exclude commands from the help menu (**eof** included by default) * Redundant **list** command removed and features merged into **history** command * Added **pyscript** command which supports tab-completion and running Python scripts with arguments * Improved tab-completion of file system paths, command names, and shell commands * Thanks to Kevin Van Brunt for all of the help with debugging and testing this * Changed default value of USE_ARG_LIST to True - this affects the beavhior of all **@options** commands * **WARNING**: This breaks backwards compatibility, to restore backwards compatibility, add this to the **__init__()** method in your custom class derived from cmd2.Cmd: * cmd2.set_use_arg_list(False) * This change improves argument parsing for all new applications * Refactored code to encapsulate most of the pyparsing logic into a ParserManager class ## 0.7.2 (May 22, 2017) * Added a MANIFEST.ini file to make sure a few extra files get included in the PyPI source distribution ## 0.7.1 (May 22, 2017) * Bug fixes * ``-`` wasn't being treated as a legal character * The allow_cli_args attribute wasn't properly disabling parsing of args at invocation when False * py command wasn't allowing scripts which used *cmd* function prior to entering an interactive Python session * Don't throw exception when piping output to a shell command * Transcript testing now properly calls ``preloop`` before and ``postloop`` after * Fixed readline bug related to ANSI color escape codes in the prompt * Added CONTRIBUTING.md and CODE_OF_CONDUCT.md files * Added unicode parsing unit tests and listed unicode support as a feature when using Python 3 * Added more examples and improved documentation * Example for how use cmd2 in a way where it doesn't own the main loop so it can integrate with external event loops * Example for how to use argparse for parsing command-line args at invocation * Example for how to use the **py** command to run Python scripts which use conditional control flow * Example of how to use regular expressions in a transcript test * Added CmdResult namedtumple for returning and storing results * Added local file system path completion for ``edit``, ``load``, ``save``, and ``shell`` commands * Add shell command completion for ``shell`` command or ``!`` shortcut * Abbreviated multiline commands are no longer allowed (they never worked correctly anyways) ## 0.7.0 (February 23, 2017) * Refactored to use six module for a unified codebase which supports both Python 2 and Python 3 * Stabilized on all platforms (Windows, Mac, Linux) and all supported Python versions (2.7, 3.3, 3.4, 3.5, 3.6, PyPy) * Added lots of unit tests and fixed a number of bugs * Improved documentation and moved it to cmd2.readthedocs.io ## 0.6.9 (October 3, 2016) * Support Python 3 input() * Fix subprocess.mswindows bug * Add Python3.6 support * Drop distutils from setup.py ## 0.6.8 (December 9, 2014) * better editor checking (by Ian Cordascu) ## 0.6.6.1 (August 14, 2013) * No changes to code trunk. Generated sdist from Python 2.7 to avoid 2to3 changes being applied to source. (Issue https://bitbucket.org/catherinedevlin/cmd2/issue/6/packaging-bug) ## 0.6.6 (August 6, 2013) * Added fix by bitbucket.org/desaintmartin to silence the editor check. bitbucket.org/catherinedevlin/cmd2/issue/1/silent-editor-check ## 0.6.5.1 (March 18, 2013) * Bugfix for setup.py version check for Python 2.6, contributed by Tomaz Muraus (https://bitbucket.org/kami) ## 0.6.5 (February 29, 2013) * Belatedly began a NEWS.txt * Changed pyparsing requirement for compatibility with Python version (2 vs 3) cmd2-0.8.5/CODEOWNERS0000644000076500000240000000105413130227101015710 0ustar toddleonhardtstaff00000000000000# Lines starting with '#' are comments. # Each line is a file pattern followed by one or more owners. # Owners of code are automatically nominated to review PRs involving that code. # These owners will be the default owners for everything in the repo. * @tleonhardt # Order is important. The last matching pattern has the most precedence. # So if a pull request only touches javascript files, only these owners # will be requested to review. #*.js @octocat @github/js # You can also use email addresses if you prefer. #docs/* docs@example.com cmd2-0.8.5/tests/0000755000076500000240000000000013264716261015501 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/tests/test_parsing.py0000644000076500000240000003135213263556236020564 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Unit/functional testing for helper functions/classes in the cmd2.py module. These are primarily tests related to parsing. Moreover, they are mostly a port of the old doctest tests which were problematic because they worked properly for some versions of pyparsing but not for others. Copyright 2017 Todd Leonhardt Released under MIT license, see LICENSE file """ import sys import cmd2 import pytest @pytest.fixture def hist(): from cmd2 import HistoryItem h = cmd2.History([HistoryItem('first'), HistoryItem('second'), HistoryItem('third'), HistoryItem('fourth')]) return h # Case-sensitive parser @pytest.fixture def parser(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, preparse=c.preparse, postparse=c.postparse, aliases=c.aliases, shortcuts=c.shortcuts) return c.parser_manager.main_parser # Case-sensitive ParserManager @pytest.fixture def cs_pm(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, preparse=c.preparse, postparse=c.postparse, aliases=c.aliases, shortcuts=c.shortcuts) return c.parser_manager @pytest.fixture def input_parser(): c = cmd2.Cmd() return c.parser_manager.input_source_parser @pytest.fixture def option_parser(): op = cmd2.OptionParser() return op def test_remaining_args(): assert cmd2.remaining_args('-f bar bar cow', ['bar', 'cow']) == 'bar cow' def test_history_span(hist): h = hist assert h == ['first', 'second', 'third', 'fourth'] assert h.span('-2..') == ['third', 'fourth'] assert h.span('2..3') == ['second', 'third'] # Inclusive of end assert h.span('3') == ['third'] assert h.span(':') == h assert h.span('2..') == ['second', 'third', 'fourth'] assert h.span('-1') == ['fourth'] assert h.span('-2..-3') == ['third', 'second'] assert h.span('*') == h def test_history_get(hist): h = hist assert h == ['first', 'second', 'third', 'fourth'] assert h.get('') == h assert h.get('-2') == h[:-2] assert h.get('5') == [] assert h.get('2-3') == ['second'] # Exclusive of end assert h.get('ir') == ['first', 'third'] # Normal string search for all elements containing "ir" assert h.get('/i.*d/') == ['third'] # Regex string search "i", then anything, then "d" def test_cast(): cast = cmd2.cast # Boolean assert cast(True, True) == True assert cast(True, False) == False assert cast(True, 0) == False assert cast(True, 1) == True assert cast(True, 'on') == True assert cast(True, 'off') == False assert cast(True, 'ON') == True assert cast(True, 'OFF') == False assert cast(True, 'y') == True assert cast(True, 'n') == False assert cast(True, 't') == True assert cast(True, 'f') == False # Non-boolean same type assert cast(1, 5) == 5 assert cast(3.4, 2.7) == 2.7 assert cast('foo', 'bar') == 'bar' assert cast([1,2], [3,4]) == [3,4] def test_cast_problems(capsys): cast = cmd2.cast expected = 'Problem setting parameter (now {}) to {}; incorrect type?\n' # Boolean current, with new value not convertible to bool current = True new = [True, True] assert cast(current, new) == current out, err = capsys.readouterr() assert out == expected.format(current, new) # Non-boolean current, with new value not convertible to current type current = 1 new = 'octopus' assert cast(current, new) == current out, err = capsys.readouterr() assert out == expected.format(current, new) def test_parse_empty_string(parser): assert parser.parseString('').dump() == '[]' def test_parse_only_comment(parser): assert parser.parseString('/* empty command */').dump() == '[]' def test_parse_single_word(parser): line = 'plainword' results = parser.parseString(line) assert results.command == line def test_parse_word_plus_terminator(parser): line = 'termbare;' results = parser.parseString(line) assert results.command == 'termbare' assert results.terminator == ';' def test_parse_suffix_after_terminator(parser): line = 'termbare; suffx' results = parser.parseString(line) assert results.command == 'termbare' assert results.terminator == ';' assert results.suffix == 'suffx' def test_parse_command_with_args(parser): line = 'command with args' results = parser.parseString(line) assert results.command == 'command' assert results.args == 'with args' def test_parse_command_with_args_terminator_and_suffix(parser): line = 'command with args and terminator; and suffix' results = parser.parseString(line) assert results.command == 'command' assert results.args == "with args and terminator" assert results.terminator == ';' assert results.suffix == 'and suffix' def test_parse_simple_piped(parser): line = 'simple | piped' results = parser.parseString(line) assert results.command == 'simple' assert results.pipeTo == " piped" def test_parse_double_pipe_is_not_a_pipe(parser): line = 'double-pipe || is not a pipe' results = parser.parseString(line) assert results.command == 'double-pipe' assert results.args == '|| is not a pipe' assert not 'pipeTo' in results def test_parse_complex_pipe(parser): line = 'command with args, terminator;sufx | piped' results = parser.parseString(line) assert results.command == 'command' assert results.args == "with args, terminator" assert results.terminator == ';' assert results.suffix == 'sufx' assert results.pipeTo == ' piped' def test_parse_output_redirect(parser): line = 'output into > afile.txt' results = parser.parseString(line) assert results.command == 'output' assert results.args == 'into' assert results.output == '>' assert results.outputTo == 'afile.txt' def test_parse_output_redirect_with_dash_in_path(parser): line = 'output into > python-cmd2/afile.txt' results = parser.parseString(line) assert results.command == 'output' assert results.args == 'into' assert results.output == '>' assert results.outputTo == 'python-cmd2/afile.txt' def test_case_sensitive_parsed_single_word(cs_pm): line = 'HeLp' statement = cs_pm.parsed(line) assert statement.parsed.command == line def test_parse_input_redirect(input_parser): line = '< afile.txt' results = input_parser.parseString(line) assert results.inputFrom == line def test_parse_input_redirect_with_dash_in_path(input_parser): line = "< python-cmd2/afile.txt" results = input_parser.parseString(line) assert results.inputFrom == line def test_parse_pipe_and_redirect(parser): line = 'output into;sufx | pipethrume plz > afile.txt' results = parser.parseString(line) assert results.command == 'output' assert results.args == 'into' assert results.terminator == ';' assert results.suffix == 'sufx' assert results.pipeTo == ' pipethrume plz' assert results.output == '>' assert results.outputTo == 'afile.txt' def test_parse_output_to_paste_buffer(parser): line = 'output to paste buffer >> ' results = parser.parseString(line) assert results.command == 'output' assert results.args == 'to paste buffer' assert results.output == '>>' def test_parse_ignore_commented_redirectors(parser): line = 'ignore the /* commented | > */ stuff;' results = parser.parseString(line) assert results.command == 'ignore' assert results.args == 'the /* commented | > */ stuff' assert results.terminator == ';' def test_parse_has_redirect_inside_terminator(parser): """The terminator designates the end of the commmand/arguments portion. If a redirector occurs before a terminator, then it will be treated as part of the arguments and not as a redirector.""" line = 'has > inside;' results = parser.parseString(line) assert results.command == 'has' assert results.args == '> inside' assert results.terminator == ';' def test_parse_what_if_quoted_strings_seem_to_start_comments(parser): line = 'what if "quoted strings /* seem to " start comments?' results = parser.parseString(line) assert results.command == 'what' assert results.args == 'if "quoted strings /* seem to " start comments?' def test_parse_unfinished_multiliine_command(parser): line = 'multiline has > inside an unfinished command' results = parser.parseString(line) assert results.multilineCommand == 'multiline' assert not 'args' in results def test_parse_multiline_command_ignores_redirectors_within_it(parser): line = 'multiline has > inside;' results = parser.parseString(line) assert results.multilineCommand == 'multiline' assert results.args == 'has > inside' assert results.terminator == ';' def test_parse_multiline_with_incomplete_comment(parser): """A terminator within a comment will be ignored and won't terminate a multiline command. Un-closed comments effectively comment out everything after the start.""" line = 'multiline command /* with comment in progress;' results = parser.parseString(line) assert results.multilineCommand == 'multiline' assert not 'args' in results def test_parse_multiline_with_complete_comment(parser): line = 'multiline command /* with comment complete */ is done;' results = parser.parseString(line) assert results.multilineCommand == 'multiline' assert results.args == 'command /* with comment complete */ is done' assert results.terminator == ';' def test_parse_multiline_termninated_by_empty_line(parser): line = 'multiline command ends\n\n' results = parser.parseString(line) assert results.multilineCommand == 'multiline' assert results.args == 'command ends' assert len(results.terminator) == 2 assert results.terminator[0] == '\n' assert results.terminator[1] == '\n' def test_parse_multiline_ignores_terminators_in_comments(parser): line = 'multiline command "with term; ends" now\n\n' results = parser.parseString(line) assert results.multilineCommand == 'multiline' assert results.args == 'command "with term; ends" now' assert len(results.terminator) == 2 assert results.terminator[0] == '\n' assert results.terminator[1] == '\n' # Unicode support is only present in cmd2 for Python 3 @pytest.mark.skipif(sys.version_info < (3,0), reason="cmd2 unicode support requires python3") def test_parse_command_with_unicode_args(parser): line = 'drink café' results = parser.parseString(line) assert results.command == 'drink' assert results.args == 'café' @pytest.mark.skipif(sys.version_info < (3, 0), reason="cmd2 unicode support requires python3") def test_parse_unicode_command(parser): line = 'café au lait' results = parser.parseString(line) assert results.command == 'café' assert results.args == 'au lait' @pytest.mark.skipif(sys.version_info < (3,0), reason="cmd2 unicode support requires python3") def test_parse_redirect_to_unicode_filename(parser): line = 'dir home > café' results = parser.parseString(line) assert results.command == 'dir' assert results.args == 'home' assert results.output == '>' assert results.outputTo == 'café' @pytest.mark.skipif(sys.version_info < (3,0), reason="cmd2 unicode support requires python3") def test_parse_input_redirect_from_unicode_filename(input_parser): line = '< café' results = input_parser.parseString(line) assert results.inputFrom == line def test_option_parser_exit_with_msg(option_parser, capsys): msg = 'foo bar' option_parser.exit(msg=msg) out, err = capsys.readouterr() assert out == msg + '\n' assert err == '' def test_empty_statement_raises_exception(): app = cmd2.Cmd() with pytest.raises(cmd2.EmptyStatement): app._complete_statement('') with pytest.raises(cmd2.EmptyStatement): app._complete_statement(' ') cmd2-0.8.5/tests/conftest.py0000644000076500000240000001073113263534244017700 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Cmd2 unit/functional testing Copyright 2016 Federico Ceratto Released under MIT license, see LICENSE file """ import sys from pytest import fixture import cmd2 # Help text for base cmd2.Cmd application BASE_HELP = """Documented commands (type help ): ======================================== alias help load pyscript set shortcuts edit history py quit shell unalias """ BASE_HELP_VERBOSE = """ Documented commands (type help ): ================================================================================ alias Define or display aliases edit Edit a file in a text editor. help List available commands with "help" or detailed help with "help cmd". history View, run, edit, and save previously entered commands. load Runs commands in script file that is encoded as either ASCII or UTF-8 text. py Invoke python command, shell, or script pyscript Runs a python script file inside the console quit Exits this application. set Sets a settable parameter or shows current settings of parameters. shell Execute a command as if at the OS prompt. shortcuts Lists shortcuts (aliases) available. unalias Unsets aliases """ # Help text for the history command HELP_HISTORY = """usage: history [-h] [-r | -e | -s | -o FILE | -t TRANSCRIPT] [arg] View, run, edit, and save previously entered commands. positional arguments: arg empty all history items a one history item by number a..b, a:b, a:, ..b items by indices (inclusive) [string] items containing string /regex/ items matching regular expression optional arguments: -h, --help show this help message and exit -r, --run run selected history items -e, --edit edit and then run selected history items -s, --script script format; no separation lines -o FILE, --output-file FILE output commands to a script file -t TRANSCRIPT, --transcript TRANSCRIPT output commands and results to a transcript file """ # Output from the shortcuts command with default built-in shortcuts SHORTCUTS_TXT = """Shortcuts for other commands: !: shell ?: help @: load @@: _relative_load """ expect_colors = True if sys.platform.startswith('win'): expect_colors = False # Output from the show command with default settings SHOW_TXT = """colors: {} continuation_prompt: > debug: False echo: False editor: vim feedback_to_output: False locals_in_py: True prompt: (Cmd) quiet: False timing: False """.format(expect_colors) if expect_colors: color_str = 'True ' else: color_str = 'False' SHOW_LONG = """ colors: {} # Colorized output (*nix only) continuation_prompt: > # On 2nd+ line of input debug: False # Show full error stack on error echo: False # Echo command issued into output editor: vim # Program used by ``edit`` feedback_to_output: False # Include nonessentials in `|`, `>` results locals_in_py: True # Allow access to your application in py via self prompt: (Cmd) # The prompt issued to solicit input quiet: False # Don't print nonessential feedback timing: False # Report execution times """.format(color_str) class StdOut(object): """ Toy class for replacing self.stdout in cmd2.Cmd instances for unit testing. """ def __init__(self): self.buffer = '' def write(self, s): self.buffer += s def read(self): raise NotImplementedError def clear(self): self.buffer = '' def normalize(block): """ Normalize a block of text to perform comparison. Strip newlines from the very beginning and very end Then split into separate lines and strip trailing whitespace from each line. """ assert isinstance(block, str) block = block.strip('\n') return [line.rstrip() for line in block.splitlines()] def run_cmd(app, cmd): """ Clear StdOut buffer, run the command, extract the buffer contents, """ app.stdout.clear() app.onecmd_plus_hooks(cmd) out = app.stdout.buffer app.stdout.clear() return normalize(out) @fixture def base_app(): c = cmd2.Cmd() c.stdout = StdOut() return c cmd2-0.8.5/tests/transcripts/0000755000076500000240000000000013264716261020055 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/tests/transcripts/slashes_escaped.txt0000644000076500000240000000027013147151335023736 0ustar toddleonhardtstaff00000000000000# escape those slashes (Cmd) say /some/unix/path \/some\/unix\/path (Cmd) say mix 2/3 c. sugar, 1/2 c. butter, and 1/2 tsp. salt mix 2\/3 c. sugar, 1\/2 c. butter, and 1\/2 tsp. salt cmd2-0.8.5/tests/transcripts/dotstar.txt0000644000076500000240000000024213147151335022267 0ustar toddleonhardtstaff00000000000000# ensure the old standby .* works. We use the non-greedy flavor (Cmd) say Adopt the pace of nature: her secret is patience. Adopt the pace of /.*?/ is patience. cmd2-0.8.5/tests/transcripts/regex_set.txt0000644000076500000240000000067513246364227022614 0ustar toddleonhardtstaff00000000000000# Run this transcript with "python example.py -t transcript_regex.txt" # The regex for colors is because no color on Windows. # The regex for editor will match whatever program you use. # Regexes on prompts just make the trailing space obvious (Cmd) set colors: /(True|False)/ continuation_prompt: >/ / debug: False echo: False editor: /.*/ feedback_to_output: False locals_in_py: True maxrepeats: 3 prompt: (Cmd)/ / quiet: False timing: False cmd2-0.8.5/tests/transcripts/word_boundaries.txt0000644000076500000240000000033013147151335023773 0ustar toddleonhardtstaff00000000000000# use word boundaries to check for key words in the output (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.*/ cmd2-0.8.5/tests/transcripts/spaces.txt0000644000076500000240000000030513147151335022065 0ustar toddleonhardtstaff00000000000000# check spaces in all their forms (Cmd) say how many spaces how many spaces (Cmd) say how many spaces how/\s{1}/many/\s{1}/spaces (Cmd) say "how many spaces" how/\s+/many/\s+/spaces cmd2-0.8.5/tests/transcripts/bol_eol.txt0000644000076500000240000000021013147151335022215 0ustar toddleonhardtstaff00000000000000# match the text with regular expressions and the newlines as literal text (Cmd) say -r 3 -s yabba dabba do /^Y.*?$/ /^Y.*?$/ /^Y.*?$/ cmd2-0.8.5/tests/transcripts/singleslash.txt0000644000076500000240000000016613147151335023130 0ustar toddleonhardtstaff00000000000000# even if you only have a single slash, you have # to escape it (Cmd) say use 2/3 cup of sugar use 2\/3 cup of sugar cmd2-0.8.5/tests/transcripts/extension_notation.txt0000644000076500000240000000013413147151335024536 0ustar toddleonhardtstaff00000000000000# inception: a regular expression that matches itself (Cmd) say (?:fred) /(?:\(\?:fred\))/ cmd2-0.8.5/tests/transcripts/multiline_regex.txt0000644000076500000240000000024313147151335024004 0ustar toddleonhardtstaff00000000000000# these regular expressions match multiple lines of text (Cmd) say -r 3 -s yabba dabba do /\A(YA.*?DO\n?){3}/ (Cmd) say -r 5 -s yabba dabba do /\A([A-Z\s]*$){3}/ cmd2-0.8.5/tests/transcripts/characterclass.txt0000644000076500000240000000021613147151335023572 0ustar toddleonhardtstaff00000000000000# match using character classes and special sequence for digits (\d) (Cmd) say 555-1212 /[0-9]{3}-[0-9]{4}/ (Cmd) say 555-1212 /\d{3}-\d{4}/ cmd2-0.8.5/tests/transcripts/slashslash.txt0000644000076500000240000000010513147151335022752 0ustar toddleonhardtstaff00000000000000# ensure consecutive slashes are parsed correctly (Cmd) say // \/\/ cmd2-0.8.5/tests/transcripts/from_cmdloop.txt0000644000076500000240000000273313263556236023306 0ustar toddleonhardtstaff00000000000000# responses with trailing spaces have been matched with a regex # so you can see where they are. (Cmd) help Documented commands (type help ): ======================================== alias help load orate pyscript say shell speak/ */ edit history mumble py quit set shortcuts unalias/ */ (Cmd) help say Repeats what you tell me to. Usage: speak [options] (text to say) Options: -h, --help show this help message and exit -p, --piglatin atinLay -s, --shout N00B EMULATION MODE -r REPEAT, --repeat=REPEAT output [n] times (Cmd) say goodnight, Gracie goodnight, Gracie (Cmd) say -ps --repeat=5 goodnight, Gracie OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY (Cmd) set maxrepeats 5 maxrepeats - was: 3 now: 5 (Cmd) say -ps --repeat=5 goodnight, Gracie OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY (Cmd) history -------------------------[1] help -------------------------[2] help say -------------------------[3] say goodnight, Gracie -------------------------[4] say -ps --repeat=5 goodnight, Gracie -------------------------[5] set maxrepeats 5 -------------------------[6] say -ps --repeat=5 goodnight, Gracie (Cmd) history -r 4 say -ps --repeat=5 goodnight, Gracie OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY (Cmd) set prompt "---> " prompt - was: (Cmd)/ */ now: --->/ */ cmd2-0.8.5/tests/transcripts/multiline_no_regex.txt0000644000076500000240000000021713147151335024501 0ustar toddleonhardtstaff00000000000000# test a multi-line command (Cmd) orate This is a test > of the > emergency broadcast system This is a test of the emergency broadcast system cmd2-0.8.5/tests/redirect.txt0000644000076500000240000000001013110635531020020 0ustar toddleonhardtstaff00000000000000history cmd2-0.8.5/tests/test_cmd2.py0000644000076500000240000015426313264702441017745 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Cmd2 unit/functional testing Copyright 2016 Federico Ceratto Released under MIT license, see LICENSE file """ import os import sys import io import tempfile import mock import pytest import six from code import InteractiveConsole from optparse import make_option # Used for sm.input: raw_input() for Python 2 or input() for Python 3 import six.moves as sm import cmd2 from conftest import run_cmd, normalize, BASE_HELP, BASE_HELP_VERBOSE, \ HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut def test_ver(): assert cmd2.__version__ == '0.8.5' def test_empty_statement(base_app): out = run_cmd(base_app, '') expected = normalize('') assert out == expected def test_base_help(base_app): out = run_cmd(base_app, 'help') expected = normalize(BASE_HELP) assert out == expected def test_base_help_verbose(base_app): out = run_cmd(base_app, 'help -v') expected = normalize(BASE_HELP_VERBOSE) assert out == expected out = run_cmd(base_app, 'help --verbose') assert out == expected def test_base_help_history(base_app): out = run_cmd(base_app, 'help history') assert out == normalize(HELP_HISTORY) def test_base_argparse_help(base_app, capsys): # Verify that "set -h" gives the same output as "help set" and that it starts in a way that makes sense run_cmd(base_app, 'set -h') out, err = capsys.readouterr() out1 = normalize(str(out)) out2 = run_cmd(base_app, 'help set') assert out1 == out2 assert out1[0].startswith('usage: set') assert out1[1] == '' assert out1[2].startswith('Sets a settable parameter') def test_base_invalid_option(base_app, capsys): run_cmd(base_app, 'set -z') out, err = capsys.readouterr() expected = ['usage: set [-h] [-a] [-l] [settable [settable ...]]', 'set: error: unrecognized arguments: -z'] assert normalize(str(err)) == expected def test_base_shortcuts(base_app): out = run_cmd(base_app, 'shortcuts') expected = normalize(SHORTCUTS_TXT) assert out == expected def test_base_show(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' out = run_cmd(base_app, 'set') expected = normalize(SHOW_TXT) assert out == expected def test_base_show_long(base_app): # force editor to be 'vim' so test is repeatable across platforms base_app.editor = 'vim' out = run_cmd(base_app, 'set -l') expected = normalize(SHOW_LONG) assert out == expected def test_base_show_readonly(base_app): base_app.editor = 'vim' out = run_cmd(base_app, 'set -a') expected = normalize(SHOW_TXT + '\nRead only settings:' + """ Commands may be terminated with: {} Arguments at invocation allowed: {} Output redirection and pipes allowed: {} Parsing of @options commands: Shell lexer mode for command argument splitting: {} Strip Quotes after splitting arguments: {} Argument type: {} """.format(base_app.terminators, base_app.allow_cli_args, base_app.allow_redirection, "POSIX" if cmd2.POSIX_SHLEX else "non-POSIX", "True" if cmd2.STRIP_QUOTES_FOR_NON_POSIX and not cmd2.POSIX_SHLEX else "False", "List of argument strings" if cmd2.USE_ARG_LIST else "string of space-separated arguments")) assert out == expected def test_base_set(base_app): out = run_cmd(base_app, 'set quiet True') expected = normalize(""" quiet - was: False now: True """) assert out == expected out = run_cmd(base_app, 'set quiet') assert out == ['quiet: True'] def test_set_not_supported(base_app, capsys): run_cmd(base_app, 'set qqq True') out, err = capsys.readouterr() expected = normalize(""" EXCEPTION of type 'LookupError' occurred with message: 'Parameter 'qqq' not supported (type 'show' for list of parameters).' To enable full traceback, run the following command: 'set debug true' """) assert normalize(str(err)) == expected def test_set_quiet(base_app): out = run_cmd(base_app, 'set quie True') expected = normalize(""" quiet - was: False now: True """) assert out == expected out = run_cmd(base_app, 'set quiet') assert out == ['quiet: True'] def test_base_shell(base_app, monkeypatch): m = mock.Mock() subprocess = 'subprocess' if six.PY2: subprocess = 'subprocess32' monkeypatch.setattr("{}.Popen".format(subprocess), m) out = run_cmd(base_app, 'shell echo a') assert out == [] assert m.called def test_base_py(base_app, capsys): run_cmd(base_app, 'py qqq=3') out, err = capsys.readouterr() assert out == '' run_cmd(base_app, 'py print(qqq)') out, err = capsys.readouterr() assert out.rstrip() == '3' @pytest.mark.skipif(sys.platform == 'win32', reason="Unit test doesn't work on win32, but feature does") def test_base_run_python_script(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') expected = 'This is a python script running ...\n' run_cmd(base_app, "py run('{}')".format(python_script)) out, err = capsys.readouterr() assert out == expected def test_base_run_pyscript(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'script.py') expected = 'This is a python script running ...\n' run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert out == expected def test_recursive_pyscript_not_allowed(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'scripts', 'recursive.py') expected = 'ERROR: Recursively entering interactive Python consoles is not allowed.\n' run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert err == expected def test_pyscript_with_nonexist_file(base_app, capsys): python_script = 'does_not_exist.py' run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert err.startswith('ERROR: [Errno 2] No such file or directory:') def test_pyscript_with_exception(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, 'scripts', 'raises_exception.py') run_cmd(base_app, "pyscript {}".format(python_script)) out, err = capsys.readouterr() assert err.startswith('Traceback') assert err.endswith("TypeError: unsupported operand type(s) for +: 'int' and 'str'\n") def test_pyscript_requires_an_argument(base_app, capsys): run_cmd(base_app, "pyscript") out, err = capsys.readouterr() assert err.startswith('ERROR: pyscript command requires at least 1 argument ...') def test_base_error(base_app): out = run_cmd(base_app, 'meow') assert out == ["*** Unknown syntax: meow"] def test_base_history(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history') expected = normalize(""" -------------------------[1] help -------------------------[2] shortcuts """) assert out == expected out = run_cmd(base_app, 'history he') expected = normalize(""" -------------------------[1] help """) assert out == expected out = run_cmd(base_app, 'history sh') expected = normalize(""" -------------------------[2] shortcuts """) assert out == expected def test_history_script_format(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history -s') expected = normalize(""" help shortcuts """) assert out == expected def test_history_with_string_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history help') expected = normalize(""" -------------------------[1] help -------------------------[3] help history """) assert out == expected def test_history_with_integer_argument(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history 1') expected = normalize(""" -------------------------[1] help """) assert out == expected def test_history_with_integer_span(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history 1..2') expected = normalize(""" -------------------------[1] help -------------------------[2] shortcuts """) assert out == expected def test_history_with_span_start(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history 2:') expected = normalize(""" -------------------------[2] shortcuts -------------------------[3] help history """) assert out == expected def test_history_with_span_end(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') out = run_cmd(base_app, 'history :2') expected = normalize(""" -------------------------[1] help -------------------------[2] shortcuts """) assert out == expected def test_history_with_span_index_error(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'help history') run_cmd(base_app, '!ls -hal :') out = run_cmd(base_app, 'history "hal :"') expected = normalize(""" -------------------------[3] !ls -hal : """) assert out == expected def test_history_output_file(base_app): run_cmd(base_app, 'help') run_cmd(base_app, 'shortcuts') run_cmd(base_app, 'help history') fd, fname = tempfile.mkstemp(prefix='', suffix='.txt') os.close(fd) run_cmd(base_app, 'history -o "{}"'.format(fname)) expected = normalize('\n'.join(['help', 'shortcuts', 'help history'])) with open(fname) as f: content = normalize(f.read()) assert content == expected def test_history_edit(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really # going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) # Run help command just so we have a command in history run_cmd(base_app, 'help') run_cmd(base_app, 'history -e 1') # We have an editor, so should expect a system call m.assert_called_once() def test_history_run_all_commands(base_app): # make sure we refuse to run all commands as a default run_cmd(base_app, 'shortcuts') out = run_cmd(base_app, 'history -r') # this should generate an error, but we don't currently have a way to # capture stderr in these tests. So we assume that if we got nothing on # standard out, that the error occured because if the commaned executed # then we should have a list of shortcuts in our output assert out == [] def test_history_run_one_command(base_app): expected = run_cmd(base_app, 'help') output = run_cmd(base_app, 'history -r 1') assert output == expected def test_base_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, 'load {}'.format(filename)) assert base_app.cmdqueue == ['help history', 'eos'] sdir = os.path.dirname(filename) assert base_app._script_dir == [sdir] assert base_app._current_script_dir == sdir def test_load_with_empty_args(base_app, capsys): # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load') out, err = capsys.readouterr() # The load command requires a file path argument, so we should get an error message expected = normalize("""ERROR: load command requires a file path:\n""") assert normalize(str(err)) == expected assert base_app.cmdqueue == [] def test_load_with_nonexistent_file(base_app, capsys): # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load does_not_exist.txt') out, err = capsys.readouterr() # The load command requires a path to an existing file assert str(err).startswith("ERROR") assert "does not exist or is not a file" in str(err) assert base_app.cmdqueue == [] def test_load_with_empty_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'empty.txt') # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load {}'.format(filename)) out, err = capsys.readouterr() # The load command requires non-empty scripts files assert str(err).startswith("ERROR") assert "is empty" in str(err) assert base_app.cmdqueue == [] def test_load_with_binary_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'binary.bin') # The way the load command works, we can't directly capture its stdout or stderr run_cmd(base_app, 'load {}'.format(filename)) out, err = capsys.readouterr() # The load command requires non-empty scripts files assert str(err).startswith("ERROR") assert "is not an ASCII or UTF-8 encoded text file" in str(err) assert base_app.cmdqueue == [] def test_load_with_utf8_file(base_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'utf8.txt') assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, 'load {}'.format(filename)) assert base_app.cmdqueue == ['!echo γνωρίζω', 'eos'] sdir = os.path.dirname(filename) assert base_app._script_dir == [sdir] assert base_app._current_script_dir == sdir def test_load_nested_loads(base_app, request): # Verify that loading a script with nested load commands works correctly, # and loads the nested script commands in the correct order. The recursive # loads don't happen all at once, but as the commands are interpreted. So, # we will need to drain the cmdqueue and inspect the stdout to see if all # steps were executed in the expected order. test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'scripts', 'nested.txt') assert base_app.cmdqueue == [] # Load the top level script and then run the command queue until all # commands have been exhausted. initial_load = 'load ' + filename run_cmd(base_app, initial_load) while base_app.cmdqueue: base_app.onecmd_plus_hooks(base_app.cmdqueue.pop(0)) # Check that the right commands were executed. expected = """ %s _relative_load precmds.txt set colors on help shortcuts _relative_load postcmds.txt set colors off""" % initial_load assert run_cmd(base_app, 'history -s') == normalize(expected) def test_base_runcmds_plus_hooks(base_app, request): # Make sure that runcmds_plus_hooks works as intended. I.E. to run multiple # commands and process any commands added, by them, to the command queue. test_dir = os.path.dirname(request.module.__file__) prefilepath = os.path.join(test_dir, 'scripts', 'precmds.txt') postfilepath = os.path.join(test_dir, 'scripts', 'postcmds.txt') assert base_app.cmdqueue == [] base_app.runcmds_plus_hooks(['load ' + prefilepath, 'help', 'shortcuts', 'load ' + postfilepath]) expected = """ load %s set colors on help shortcuts load %s set colors off""" % (prefilepath, postfilepath) assert run_cmd(base_app, 'history -s') == normalize(expected) def test_base_relative_load(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') assert base_app.cmdqueue == [] assert base_app._script_dir == [] assert base_app._current_script_dir is None # Run the load command, which populates the command queue and sets the script directory run_cmd(base_app, '_relative_load {}'.format(filename)) assert base_app.cmdqueue == ['help history', 'eos'] sdir = os.path.dirname(filename) assert base_app._script_dir == [sdir] assert base_app._current_script_dir == sdir def test_relative_load_requires_an_argument(base_app, capsys): run_cmd(base_app, '_relative_load') out, err = capsys.readouterr() assert out == '' assert err.startswith('ERROR: _relative_load command requires a file path:\n') assert base_app.cmdqueue == [] def test_output_redirection(base_app): fd, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(fd) try: # Verify that writing to a file works run_cmd(base_app, 'help > {}'.format(filename)) expected = normalize(BASE_HELP) with open(filename) as f: content = normalize(f.read()) assert content == expected # Verify that appending to a file also works run_cmd(base_app, 'help history >> {}'.format(filename)) expected = normalize(BASE_HELP + '\n' + HELP_HISTORY) with open(filename) as f: content = normalize(f.read()) assert content == expected except: raise finally: os.remove(filename) def test_feedback_to_output_true(base_app): base_app.feedback_to_output = True base_app.timing = True f, filename = tempfile.mkstemp(prefix='cmd2_test', suffix='.txt') os.close(f) try: run_cmd(base_app, 'help > {}'.format(filename)) with open(filename) as f: content = f.readlines() assert content[-1].startswith('Elapsed: ') except: raise finally: os.remove(filename) def test_feedback_to_output_false(base_app, capsys): base_app.feedback_to_output = False base_app.timing = True f, filename = tempfile.mkstemp(prefix='feedback_to_output', suffix='.txt') os.close(f) try: run_cmd(base_app, 'help > {}'.format(filename)) out, err = capsys.readouterr() with open(filename) as f: content = f.readlines() assert not content[-1].startswith('Elapsed: ') assert err.startswith('Elapsed') except: raise finally: os.remove(filename) def test_allow_redirection(base_app): # Set allow_redirection to False base_app.allow_redirection = False filename = 'test_allow_redirect.txt' # Verify output wasn't redirected out = run_cmd(base_app, 'help > {}'.format(filename)) expected = normalize(BASE_HELP) assert out == expected # Verify that no file got created assert not os.path.exists(filename) def test_input_redirection(base_app, request): test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'redirect.txt') # NOTE: File 'redirect.txt" contains 1 word "history" # Verify that redirecting input ffom a file works out = run_cmd(base_app, 'help < {}'.format(filename)) assert out == normalize(HELP_HISTORY) def test_pipe_to_shell(base_app, capsys): if sys.platform == "win32": # Windows command = 'help | sort' else: # Mac and Linux # Get help on help and pipe it's output to the input of the word count shell command command = 'help help | wc' # # Mac and Linux wc behave the same when piped from shell, but differently when piped stdin from file directly # if sys.platform == 'darwin': # expected = "1 11 70" # else: # expected = "1 11 70" # assert out.strip() == expected.strip() run_cmd(base_app, command) out, err = capsys.readouterr() # Unfortunately with the improved way of piping output to a subprocess, there isn't any good way of getting # access to the output produced by that subprocess within a unit test, but we can verify that no error occured assert not err def test_pipe_to_shell_error(base_app, capsys): # Try to pipe command output to a shell command that doesn't exist in order to produce an error run_cmd(base_app, 'help | foobarbaz.this_does_not_exist') out, err = capsys.readouterr() assert not out expected_error = 'FileNotFoundError' if six.PY2: if sys.platform.startswith('win'): expected_error = 'WindowsError' else: expected_error = 'OSError' assert err.startswith("EXCEPTION of type '{}' occurred with message:".format(expected_error)) @pytest.mark.skipif(not cmd2.can_clip, reason="Pyperclip could not find a copy/paste mechanism for your system") def test_send_to_paste_buffer(base_app): # Test writing to the PasteBuffer/Clipboard run_cmd(base_app, 'help >') expected = normalize(BASE_HELP) assert normalize(cmd2.get_paste_buffer()) == expected # Test appending to the PasteBuffer/Clipboard run_cmd(base_app, 'help history >>') expected = normalize(BASE_HELP + '\n' + HELP_HISTORY) assert normalize(cmd2.get_paste_buffer()) == expected def test_base_timing(base_app, capsys): base_app.feedback_to_output = False out = run_cmd(base_app, 'set timing True') expected = normalize("""timing - was: False now: True """) assert out == expected out, err = capsys.readouterr() if sys.platform == 'win32': assert err.startswith('Elapsed: 0:00:00') else: assert err.startswith('Elapsed: 0:00:00.0') def test_base_debug(base_app, capsys): # Try to set a non-existent parameter with debug set to False by default run_cmd(base_app, 'set does_not_exist 5') out, err = capsys.readouterr() assert err.startswith('EXCEPTION') # Set debug true out = run_cmd(base_app, 'set debug True') expected = normalize(""" debug - was: False now: True """) assert out == expected # Verify that we now see the exception traceback run_cmd(base_app, 'set does_not_exist 5') out, err = capsys.readouterr() assert str(err).startswith('Traceback (most recent call last):') def test_base_colorize(base_app): # If using base_app test fixture it won't get colorized because we replaced self.stdout color_test = base_app.colorize('Test', 'red') assert color_test == 'Test' # But if we create a fresh Cmd() instance, it will fresh_app = cmd2.Cmd() color_test = fresh_app.colorize('Test', 'red') # Actually, colorization only ANSI escape codes is only applied on non-Windows systems if sys.platform == 'win32': assert color_test == 'Test' else: assert color_test == '\x1b[31mTest\x1b[39m' def _expected_no_editor_error(): expected_exception = 'OSError' # If using Python 2 or PyPy (either 2 or 3), expect a different exception than with Python 3 if six.PY2 or hasattr(sys, "pypy_translation_info"): expected_exception = 'EnvironmentError' expected_text = normalize(""" EXCEPTION of type '{}' occurred with message: 'Please use 'set editor' to specify your text editing program of choice.' To enable full traceback, run the following command: 'set debug true' """.format(expected_exception)) return expected_text def test_edit_no_editor(base_app, capsys): # Purposely set the editor to None base_app.editor = None # Make sure we get an exception, but cmd2 handles it run_cmd(base_app, 'edit') out, err = capsys.readouterr() expected = _expected_no_editor_error() assert normalize(str(err)) == expected def test_edit_file(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'script.txt') run_cmd(base_app, 'edit {}'.format(filename)) # We think we have an editor, so should expect a system call m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename)) def test_edit_file_with_spaces(base_app, request, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) test_dir = os.path.dirname(request.module.__file__) filename = os.path.join(test_dir, 'my commands.txt') run_cmd(base_app, 'edit "{}"'.format(filename)) # We think we have an editor, so should expect a system call m.assert_called_once_with('"{}" "{}"'.format(base_app.editor, filename)) def test_edit_blank(base_app, monkeypatch): # Set a fake editor just to make sure we have one. We aren't really going to call it due to the mock base_app.editor = 'fooedit' # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) run_cmd(base_app, 'edit') # We have an editor, so should expect a system call m.assert_called_once() def test_base_py_interactive(base_app): # Mock out the InteractiveConsole.interact() call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='interact') InteractiveConsole.interact = m run_cmd(base_app, "py") # Make sure our mock was called once and only once m.assert_called_once() def test_exclude_from_history(base_app, monkeypatch): # Mock out the os.system call so we don't actually open an editor m = mock.MagicMock(name='system') monkeypatch.setattr("os.system", m) # Run edit command run_cmd(base_app, 'edit') # Run history command run_cmd(base_app, 'history') # Verify that the history is empty out = run_cmd(base_app, 'history') assert out == [] # Now run a command which isn't excluded from the history run_cmd(base_app, 'help') # And verify we have a history now ... out = run_cmd(base_app, 'history') expected = normalize("""-------------------------[1] help""") assert out == expected def test_base_cmdloop_with_queue(): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = cmd2.Cmd() app.use_rawinput = True intro = 'Hello World, this is an intro ...' app.cmdqueue.append('quit\n') app.stdout = StdOut() # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] expected = intro + '\n' with mock.patch.object(sys, 'argv', testargs): # Run the command loop with custom intro app.cmdloop(intro=intro) out = app.stdout.buffer assert out == expected def test_base_cmdloop_without_queue(): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = cmd2.Cmd() app.use_rawinput = True app.intro = 'Hello World, this is an intro ...' app.stdout = StdOut() # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='quit') sm.input = m # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] expected = app.intro + '\n' with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() out = app.stdout.buffer assert out == expected def test_cmdloop_without_rawinput(): # Create a cmd2.Cmd() instance and make sure basic settings are like we want for test app = cmd2.Cmd() app.use_rawinput = False app.echo = False app.intro = 'Hello World, this is an intro ...' app.stdout = StdOut() # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='quit') sm.input = m # Need to patch sys.argv so cmd2 doesn't think it was called with arguments equal to the py.test args testargs = ["prog"] expected = app.intro + '\n' with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() out = app.stdout.buffer assert out == expected class HookFailureApp(cmd2.Cmd): def __init__(self, *args, **kwargs): # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) def postparsing_precmd(self, statement): """Simulate precmd hook failure.""" return True, statement @pytest.fixture def hook_failure(): app = HookFailureApp() app.stdout = StdOut() return app def test_precmd_hook_success(base_app): out = base_app.onecmd_plus_hooks('help') assert out is None def test_precmd_hook_failure(hook_failure): out = hook_failure.onecmd_plus_hooks('help') assert out == True class SayApp(cmd2.Cmd): def __init__(self, *args, **kwargs): # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) def do_say(self, arg): self.poutput(arg) @pytest.fixture def say_app(): app = SayApp() app.stdout = StdOut() return app def test_interrupt_quit(say_app): say_app.quit_on_sigint = True # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] sm.input = m say_app.cmdloop() # And verify the expected output to stdout out = say_app.stdout.buffer assert out == 'hello\n' def test_interrupt_noquit(say_app): say_app.quit_on_sigint = False # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') m.side_effect = ['say hello', KeyboardInterrupt(), 'say goodbye', 'eof'] sm.input = m say_app.cmdloop() # And verify the expected output to stdout out = say_app.stdout.buffer assert out == 'hello\n^C\ngoodbye\n' class ShellApp(cmd2.Cmd): def __init__(self, *args, **kwargs): # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) self.default_to_shell = True @pytest.fixture def shell_app(): app = ShellApp() app.stdout = StdOut() return app def test_default_to_shell_unknown(shell_app): unknown_command = 'zyxcw23' out = run_cmd(shell_app, unknown_command) assert out == ["*** Unknown syntax: {}".format(unknown_command)] def test_default_to_shell_good(capsys): app = cmd2.Cmd() app.default_to_shell = True if sys.platform.startswith('win'): line = 'dir' else: line = 'ls' statement = app.parser_manager.parsed(line) retval = app.default(statement) assert not retval out, err = capsys.readouterr() assert out == '' def test_default_to_shell_failure(capsys): app = cmd2.Cmd() app.default_to_shell = True line = 'ls does_not_exist.xyz' statement = app.parser_manager.parsed(line) retval = app.default(statement) assert not retval out, err = capsys.readouterr() assert out == "*** Unknown syntax: {}\n".format(line) def test_ansi_prompt_not_esacped(base_app): prompt = '(Cmd) ' assert base_app._surround_ansi_escapes(prompt) == prompt def test_ansi_prompt_escaped(): app = cmd2.Cmd() color = 'cyan' prompt = 'InColor' color_prompt = app.colorize(prompt, color) readline_hack_start = "\x01" readline_hack_end = "\x02" readline_safe_prompt = app._surround_ansi_escapes(color_prompt) if sys.platform.startswith('win'): # colorize() does nothing on Windows due to lack of ANSI color support assert prompt == color_prompt assert readline_safe_prompt == prompt else: assert prompt != color_prompt assert readline_safe_prompt.startswith(readline_hack_start + app._colorcodes[color][True] + readline_hack_end) assert readline_safe_prompt.endswith(readline_hack_start + app._colorcodes[color][False] + readline_hack_end) class HelpApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" def __init__(self, *args, **kwargs): # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) def do_squat(self, arg): """This docstring help will never be shown because the help_squat method overrides it.""" pass def help_squat(self): self.stdout.write('This command does diddly squat...\n') def do_edit(self, arg): """This overrides the edit command and does nothing.""" pass # This command will be in the "undocumented" section of the help menu def do_undoc(self, arg): pass @pytest.fixture def help_app(): app = HelpApp() app.stdout = StdOut() return app def test_custom_command_help(help_app): out = run_cmd(help_app, 'help squat') expected = normalize('This command does diddly squat...') assert out == expected def test_custom_help_menu(help_app): out = run_cmd(help_app, 'help') expected = normalize(""" Documented commands (type help ): ======================================== alias help load pyscript set shortcuts unalias edit history py quit shell squat Undocumented commands: ====================== undoc """) assert out == expected def test_help_undocumented(help_app): out = run_cmd(help_app, 'help undoc') expected = normalize('*** No help on undoc') assert out == expected def test_help_overridden_method(help_app): out = run_cmd(help_app, 'help edit') expected = normalize('This overrides the edit command and does nothing.') assert out == expected class HelpCategoriesApp(cmd2.Cmd): """Class for testing custom help_* methods which override docstring help.""" def __init__(self, *args, **kwargs): # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) @cmd2.with_category('Some Category') def do_diddly(self, arg): """This command does diddly""" pass def do_squat(self, arg): """This docstring help will never be shown because the help_squat method overrides it.""" pass def help_squat(self): self.stdout.write('This command does diddly squat...\n') def do_edit(self, arg): """This overrides the edit command and does nothing.""" pass cmd2.categorize((do_squat, do_edit), 'Custom Category') # This command will be in the "undocumented" section of the help menu def do_undoc(self, arg): pass @pytest.fixture def helpcat_app(): app = HelpCategoriesApp() app.stdout = StdOut() return app def test_help_cat_base(helpcat_app): out = run_cmd(helpcat_app, 'help') expected = normalize("""Documented commands (type help ): Custom Category =============== edit squat Some Category ============= diddly Other ===== alias help history load py pyscript quit set shell shortcuts unalias Undocumented commands: ====================== undoc """) assert out == expected def test_help_cat_verbose(helpcat_app): out = run_cmd(helpcat_app, 'help --verbose') expected = normalize("""Documented commands (type help ): Custom Category ================================================================================ edit This overrides the edit command and does nothing. squat This command does diddly squat... Some Category ================================================================================ diddly This command does diddly Other ================================================================================ alias Define or display aliases help List available commands with "help" or detailed help with "help cmd". history View, run, edit, and save previously entered commands. load Runs commands in script file that is encoded as either ASCII or UTF-8 text. py Invoke python command, shell, or script pyscript Runs a python script file inside the console quit Exits this application. set Sets a settable parameter or shows current settings of parameters. shell Execute a command as if at the OS prompt. shortcuts Lists shortcuts (aliases) available. unalias Unsets aliases Undocumented commands: ====================== undoc """) assert out == expected class SelectApp(cmd2.Cmd): def do_eat(self, arg): """Eat something, with a selection of sauces to choose from.""" # Pass in a single string of space-separated selections sauce = self.select('sweet salty', 'Sauce? ') result = '{food} with {sauce} sauce, yum!' result = result.format(food=arg, sauce=sauce) self.stdout.write(result + '\n') def do_study(self, arg): """Learn something, with a selection of subjects to choose from.""" # Pass in a list of strings for selections subject = self.select(['math', 'science'], 'Subject? ') result = 'Good luck learning {}!\n'.format(subject) self.stdout.write(result) def do_procrastinate(self, arg): """Waste time in your manner of choice.""" # Pass in a list of tuples for selections leisure_activity = self.select([('Netflix and chill', 'Netflix'), ('Porn', 'WebSurfing')], 'How would you like to procrastinate? ') result = 'Have fun procrasinating with {}!\n'.format(leisure_activity) self.stdout.write(result) def do_play(self, arg): """Play your favorite musical instrument.""" # Pass in an uneven list of tuples for selections instrument = self.select([('Guitar', 'Electric Guitar'), ('Drums',)], 'Instrument? ') result = 'Charm us with the {}...\n'.format(instrument) self.stdout.write(result) @pytest.fixture def select_app(): app = SelectApp() app.stdout = StdOut() return app def test_select_options(select_app): # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='2') sm.input = m food = 'bacon' out = run_cmd(select_app, "eat {}".format(food)) expected = normalize(""" 1. sweet 2. salty {} with salty sauce, yum! """.format(food)) # Make sure our mock was called with the expected arguments m.assert_called_once_with('Sauce? ') # And verify the expected output to stdout assert out == expected def test_select_invalid_option(select_app): # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input') # If side_effect is an iterable then each call to the mock will return the next value from the iterable. m.side_effect = ['3', '1'] # First pass and invalid selection, then pass a valid one sm.input = m food = 'fish' out = run_cmd(select_app, "eat {}".format(food)) expected = normalize(""" 1. sweet 2. salty 3 isn't a valid choice. Pick a number between 1 and 2: {} with sweet sauce, yum! """.format(food)) # Make sure our mock was called exactly twice with the expected arguments arg = 'Sauce? ' calls = [mock.call(arg), mock.call(arg)] m.assert_has_calls(calls) # And verify the expected output to stdout assert out == expected def test_select_list_of_strings(select_app): # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='2') sm.input = m out = run_cmd(select_app, "study") expected = normalize(""" 1. math 2. science Good luck learning {}! """.format('science')) # Make sure our mock was called with the expected arguments m.assert_called_once_with('Subject? ') # And verify the expected output to stdout assert out == expected def test_select_list_of_tuples(select_app): # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='2') sm.input = m out = run_cmd(select_app, "procrastinate") expected = normalize(""" 1. Netflix 2. WebSurfing Have fun procrasinating with {}! """.format('Porn')) # Make sure our mock was called with the expected arguments m.assert_called_once_with('How would you like to procrastinate? ') # And verify the expected output to stdout assert out == expected def test_select_uneven_list_of_tuples(select_app): # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.MagicMock(name='input', return_value='2') sm.input = m out = run_cmd(select_app, "play") expected = normalize(""" 1. Electric Guitar 2. Drums Charm us with the {}... """.format('Drums')) # Make sure our mock was called with the expected arguments m.assert_called_once_with('Instrument? ') # And verify the expected output to stdout assert out == expected @pytest.fixture def noarglist_app(): cmd2.set_use_arg_list(False) app = cmd2.Cmd() app.stdout = StdOut() return app def test_pyscript_with_noarglist(noarglist_app, capsys, request): test_dir = os.path.dirname(request.module.__file__) python_script = os.path.join(test_dir, '..', 'examples', 'scripts', 'arg_printer.py') expected = """Running Python script 'arg_printer.py' which was called with 2 arguments arg 1: 'foo' arg 2: 'bar' """ run_cmd(noarglist_app, 'pyscript {} foo bar'.format(python_script)) out, err = capsys.readouterr() assert out == expected class OptionApp(cmd2.Cmd): @cmd2.options([make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE")]) def do_greet(self, arg, opts=None): arg = ''.join(arg) if opts.shout: arg = arg.upper() self.stdout.write(arg + '\n') def test_option_help_with_no_docstring(capsys): app = OptionApp() app.onecmd_plus_hooks('greet -h') out, err = capsys.readouterr() assert err == '' assert out == """Usage: greet [options] arg Options: -h, --help show this help message and exit -s, --shout N00B EMULATION MODE """ @pytest.mark.skipif(sys.platform.startswith('win'), reason="cmd2._which function only used on Mac and Linux") def test_which_editor_good(): editor = 'vi' path = cmd2._which(editor) # Assert that the vi editor was found because it should exist on all Mac and Linux systems assert path @pytest.mark.skipif(sys.platform.startswith('win'), reason="cmd2._which function only used on Mac and Linux") def test_which_editor_bad(): editor = 'notepad.exe' path = cmd2._which(editor) # Assert that the editor wasn't found because no notepad.exe on non-Windows systems ;-) assert path is None class MultilineApp(cmd2.Cmd): def __init__(self, *args, **kwargs): self.multilineCommands = ['orate'] # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) @cmd2.options([make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE")]) def do_orate(self, arg, opts=None): arg = ''.join(arg) if opts.shout: arg = arg.upper() self.stdout.write(arg + '\n') @pytest.fixture def multiline_app(): app = MultilineApp() app.stdout = StdOut() return app def test_multiline_complete_empty_statement_raises_exception(multiline_app): with pytest.raises(cmd2.EmptyStatement): multiline_app._complete_statement('') def test_multiline_complete_statement_without_terminator(multiline_app): # Mock out the input call so we don't actually wait for a user's response on stdin when it looks for more input m = mock.MagicMock(name='input', return_value='\n') sm.input = m command = 'orate' args = 'hello world' line = '{} {}'.format(command, args) statement = multiline_app._complete_statement(line) assert statement == args assert statement.parsed.command == command def test_clipboard_failure(capsys): # Force cmd2 clipboard to be disabled cmd2.can_clip = False app = cmd2.Cmd() # Redirect command output to the clipboard when a clipboard isn't present app.onecmd_plus_hooks('help > ') # Make sure we got the error output out, err = capsys.readouterr() assert out == '' assert 'Cannot redirect to paste buffer; install ``xclip`` and re-run to enable' in err class CmdResultApp(cmd2.Cmd): def __init__(self, *args, **kwargs): # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x cmd2.Cmd.__init__(self, *args, **kwargs) def do_affirmative(self, arg): self._last_result = cmd2.CmdResult(arg) def do_negative(self, arg): self._last_result = cmd2.CmdResult('', arg) @pytest.fixture def cmdresult_app(): app = CmdResultApp() app.stdout = StdOut() return app def test_cmdresult(cmdresult_app): arg = 'foo' run_cmd(cmdresult_app, 'affirmative {}'.format(arg)) assert cmdresult_app._last_result assert cmdresult_app._last_result == cmd2.CmdResult(arg) arg = 'bar' run_cmd(cmdresult_app, 'negative {}'.format(arg)) assert not cmdresult_app._last_result assert cmdresult_app._last_result == cmd2.CmdResult('', arg) def test_is_text_file_bad_input(base_app): # Test with a non-existent file file_is_valid = base_app.is_text_file('does_not_exist.txt') assert not file_is_valid # Test with a directory dir_is_valid = base_app.is_text_file('.') assert not dir_is_valid def test_eof(base_app): # Only thing to verify is that it returns True assert base_app.do_eof('dont care') def test_eos(base_app): sdir = 'dummy_dir' base_app._script_dir.append(sdir) assert len(base_app._script_dir) == 1 # Assert that it does NOT return true assert not base_app.do_eos('dont care') # And make sure it reduced the length of the script dir list assert len(base_app._script_dir) == 0 def test_echo(capsys): app = cmd2.Cmd() # Turn echo on and pre-stage some commands in the queue, simulating like we are in the middle of a script app.echo = True command = 'help history' app.cmdqueue = [command, 'quit', 'eos'] app._script_dir.append('some_dir') assert app._current_script_dir is not None # Run the inner _cmdloop app._cmdloop() out, err = capsys.readouterr() # Check the output assert app.cmdqueue == [] assert app._current_script_dir is None assert out.startswith('{}{}\n'.format(app.prompt, command) + HELP_HISTORY.split()[0]) def test_pseudo_raw_input_tty_rawinput_true(): # use context managers so original functions get put back when we are done # we dont use decorators because we need m_input for the assertion with mock.patch('sys.stdin.isatty', mock.MagicMock(name='isatty', return_value=True)): with mock.patch('six.moves.input', mock.MagicMock(name='input', side_effect=['set', EOFError])) as m_input: # run the cmdloop, which should pull input from our mocks app = cmd2.Cmd() app.use_rawinput = True app._cmdloop() # because we mocked the input() call, we won't get the prompt # or the name of the command in the output, so we can't check # if its there. We assume that if input got called twice, once # for the 'set' command, and once for the 'quit' command, # that the rest of it worked assert m_input.call_count == 2 def test_pseudo_raw_input_tty_rawinput_false(): # gin up some input like it's coming from a tty fakein = io.StringIO(u'{}'.format('set\n')) mtty = mock.MagicMock(name='isatty', return_value=True) fakein.isatty = mtty mreadline = mock.MagicMock(name='readline', wraps=fakein.readline) fakein.readline = mreadline # run the cmdloop, telling it where to get input from app = cmd2.Cmd(stdin=fakein) app.use_rawinput = False app._cmdloop() # because we mocked the readline() call, we won't get the prompt # or the name of the command in the output, so we can't check # if its there. We assume that if readline() got called twice, once # for the 'set' command, and once for the 'quit' command, # that the rest of it worked assert mreadline.call_count == 2 # the next helper function and two tests check for piped # input when use_rawinput is True. def piped_rawinput_true(capsys, echo, command): app = cmd2.Cmd() app.use_rawinput = True app.echo = echo # run the cmdloop, which should pull input from our mock app._cmdloop() out, err = capsys.readouterr() return (app, out) # using the decorator puts the original function at six.moves.input # back when this method returns @mock.patch('six.moves.input', mock.MagicMock(name='input', side_effect=['set', EOFError])) def test_pseudo_raw_input_piped_rawinput_true_echo_true(capsys): command = 'set' app, out = piped_rawinput_true(capsys, True, command) out = out.splitlines() assert out[0] == '{}{}'.format(app.prompt, command) assert out[1].startswith('colors:') # using the decorator puts the original function at six.moves.input # back when this method returns @mock.patch('six.moves.input', mock.MagicMock(name='input', side_effect=['set', EOFError])) def test_pseudo_raw_input_piped_rawinput_true_echo_false(capsys): command = 'set' app, out = piped_rawinput_true(capsys, False, command) firstline = out.splitlines()[0] assert firstline.startswith('colors:') assert not '{}{}'.format(app.prompt, command) in out # the next helper function and two tests check for piped # input when use_rawinput=False def piped_rawinput_false(capsys, echo, command): fakein = io.StringIO(u'{}'.format(command)) # run the cmdloop, telling it where to get input from app = cmd2.Cmd(stdin=fakein) app.use_rawinput = False app.echo = echo app._cmdloop() out, err = capsys.readouterr() return (app, out) def test_pseudo_raw_input_piped_rawinput_false_echo_true(capsys): command = 'set' app, out = piped_rawinput_false(capsys, True, command) out = out.splitlines() assert out[0] == '{}{}'.format(app.prompt, command) assert out[1].startswith('colors:') def test_pseudo_raw_input_piped_rawinput_false_echo_false(capsys): command = 'set' app, out = piped_rawinput_false(capsys, False, command) firstline = out.splitlines()[0] assert firstline.startswith('colors:') assert not '{}{}'.format(app.prompt, command) in out # # other input tests def test_raw_input(base_app): base_app.use_raw_input = True fake_input = 'quit' # Mock out the input call so we don't actually wait for a user's response on stdin m = mock.Mock(name='input', return_value=fake_input) sm.input = m line = base_app.pseudo_raw_input('(cmd2)') assert line == fake_input def test_stdin_input(): app = cmd2.Cmd() app.use_rawinput = False fake_input = 'quit' # Mock out the readline call so we don't actually read from stdin m = mock.Mock(name='readline', return_value=fake_input) app.stdin.readline = m line = app.pseudo_raw_input('(cmd2)') assert line == fake_input def test_empty_stdin_input(): app = cmd2.Cmd() app.use_rawinput = False fake_input = '' # Mock out the readline call so we don't actually read from stdin m = mock.Mock(name='readline', return_value=fake_input) app.stdin.readline = m line = app.pseudo_raw_input('(cmd2)') assert line == 'eof' def test_poutput_string(base_app): msg = 'This is a test' base_app.poutput(msg) out = base_app.stdout.buffer expected = msg + '\n' assert out == expected def test_poutput_zero(base_app): msg = 0 base_app.poutput(msg) out = base_app.stdout.buffer expected = str(msg) + '\n' assert out == expected def test_poutput_empty_string(base_app): msg = '' base_app.poutput(msg) out = base_app.stdout.buffer expected = msg assert out == expected def test_poutput_none(base_app): msg = None base_app.poutput(msg) out = base_app.stdout.buffer expected = '' assert out == expected def test_alias(base_app, capsys): # Create the alias out = run_cmd(base_app, 'alias fake pyscript') assert out == normalize("Alias 'fake' created") # Use the alias run_cmd(base_app, 'fake') out, err = capsys.readouterr() assert "pyscript command requires at least 1 argument" in err # See a list of aliases out = run_cmd(base_app, 'alias') assert out == normalize('alias fake pyscript') # Lookup the new alias out = run_cmd(base_app, 'alias fake') assert out == normalize('alias fake pyscript') def test_alias_lookup_invalid_alias(base_app, capsys): # Lookup invalid alias out = run_cmd(base_app, 'alias invalid') out, err = capsys.readouterr() assert "not found" in err def test_alias_with_invalid_name(base_app, capsys): run_cmd(base_app, 'alias @ help') out, err = capsys.readouterr() assert "can only contain the following characters" in err def test_unalias(base_app): # Create an alias run_cmd(base_app, 'alias fake pyscript') # Remove the alias out = run_cmd(base_app, 'unalias fake') assert out == normalize("Alias 'fake' cleared") def test_unalias_all(base_app): out = run_cmd(base_app, 'unalias -a') assert out == normalize("All aliases cleared") def test_unalias_non_existing(base_app, capsys): run_cmd(base_app, 'unalias fake') out, err = capsys.readouterr() assert "does not exist" in err cmd2-0.8.5/tests/scripts/0000755000076500000240000000000013264716261017170 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/tests/scripts/empty.txt0000644000076500000240000000000013125561711021047 0ustar toddleonhardtstaff00000000000000cmd2-0.8.5/tests/scripts/recursive.py0000644000076500000240000000026413126042103021533 0ustar toddleonhardtstaff00000000000000#!/usr/bin/env python # coding=utf-8 """ Example demonstrating that running a Python script recursively inside another Python script isn't allowed """ cmd('pyscript ../script.py') cmd2-0.8.5/tests/scripts/nested.txt0000644000076500000240000000010613161303433021175 0ustar toddleonhardtstaff00000000000000_relative_load precmds.txt help shortcuts _relative_load postcmds.txt cmd2-0.8.5/tests/scripts/postcmds.txt0000644000076500000240000000001713246364227021564 0ustar toddleonhardtstaff00000000000000set colors off cmd2-0.8.5/tests/scripts/utf8.txt0000644000076500000240000000002613125561711020607 0ustar toddleonhardtstaff00000000000000!echo γνωρίζω cmd2-0.8.5/tests/scripts/raises_exception.py0000644000076500000240000000020413126042103023062 0ustar toddleonhardtstaff00000000000000#!/usr/bin/env python # coding=utf-8 """ Example demonstrating what happens when a Python script raises an exception """ 1 + 'blue' cmd2-0.8.5/tests/scripts/one_down.txt0000644000076500000240000000003513125033336021526 0ustar toddleonhardtstaff00000000000000_relative_load ../script.txt cmd2-0.8.5/tests/scripts/precmds.txt0000644000076500000240000000001613246364227021364 0ustar toddleonhardtstaff00000000000000set colors on cmd2-0.8.5/tests/scripts/binary.bin0000644000076500000240000000006313125561711021137 0ustar toddleonhardtstaff00000000000000ELF>0T@0cmd2-0.8.5/tests/relative_multiple.txt0000644000076500000240000000004513125033336021756 0ustar toddleonhardtstaff00000000000000_relative_load scripts/one_down.txt cmd2-0.8.5/tests/script.txt0000644000076500000240000000001513125033336017531 0ustar toddleonhardtstaff00000000000000help history cmd2-0.8.5/tests/script.py0000644000076500000240000000025113110635531017343 0ustar toddleonhardtstaff00000000000000#!/usr/bin/env python # coding=utf-8 """ Trivial example of a Python script which can be run inside a cmd2 application. """ print("This is a python script running ...") cmd2-0.8.5/tests/test_submenu.py0000644000076500000240000001323013252331571020561 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Cmd2 testing for argument parsing """ import pytest import cmd2 from conftest import run_cmd, StdOut, normalize class SecondLevelB(cmd2.Cmd): """To be used as a second level command class. """ def __init__(self, *args, **kwargs): cmd2.Cmd.__init__(self, *args, **kwargs) self.prompt = '2ndLevel B ' def do_get_top_level_attr(self, line): self.poutput(str(self.top_level_attr)) def do_set_top_level_attr(self, line): self.top_level_attr = 987654321 class SecondLevel(cmd2.Cmd): """To be used as a second level command class. """ def __init__(self, *args, **kwargs): cmd2.Cmd.__init__(self, *args, **kwargs) self.prompt = '2ndLevel ' self.top_level_attr = None def do_say(self, line): self.poutput("You called a command in SecondLevel with '%s'. " % line) def help_say(self): self.poutput("This is a second level menu. Options are qwe, asd, zxc") def complete_say(self, text, line, begidx, endidx): return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] def do_get_top_level_attr(self, line): self.poutput(str(self.top_level_attr)) def do_get_prompt(self, line): self.poutput(self.prompt) second_level_cmd = SecondLevel() second_level_b_cmd = SecondLevelB() @cmd2.AddSubmenu(SecondLevelB(), command='should_work_with_default_kwargs') @cmd2.AddSubmenu(second_level_b_cmd, command='secondb', shared_attributes=dict(top_level_attr='top_level_attr'), require_predefined_shares=False, preserve_shares=True ) @cmd2.AddSubmenu(second_level_cmd, command='second', aliases=('second_alias',), shared_attributes=dict(top_level_attr='top_level_attr')) class SubmenuApp(cmd2.Cmd): """To be used as the main / top level command class that will contain other submenus.""" def __init__(self, *args, **kwargs): cmd2.Cmd.__init__(self, *args, **kwargs) self.prompt = 'TopLevel ' self.top_level_attr = 123456789 def do_say(self, line): self.poutput("You called a command in TopLevel with '%s'. " % line) def help_say(self): self.poutput("This is a top level submenu. Options are qwe, asd, zxc") def complete_say(self, text, line, begidx, endidx): return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] @pytest.fixture def submenu_app(): app = SubmenuApp() app.stdout = StdOut() second_level_cmd.stdout = StdOut() second_level_b_cmd.stdout = StdOut() return app @pytest.fixture def secondlevel_app(): app = SecondLevel() app.stdout = StdOut() return app @pytest.fixture def secondlevel_app_b(): app = SecondLevelB() app.stdout = StdOut() return app def run_submenu_cmd(app, second_level_app, cmd): """ Clear StdOut buffers, run the command, extract the buffer contents.""" app.stdout.clear() second_level_app.stdout.clear() app.onecmd_plus_hooks(cmd) out1 = app.stdout.buffer out2 = second_level_app.stdout.buffer app.stdout.clear() second_level_app.stdout.clear() return normalize(out1), normalize(out2) def test_submenu_say_from_top_level(submenu_app): line = 'testing' out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'say ' + line) assert len(out1) == 1 assert len(out2) == 0 assert out1[0] == "You called a command in TopLevel with {!r}.".format(line) def test_submenu_second_say_from_top_level(submenu_app): line = 'testing' out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second say ' + line) # No output expected from the top level assert out1 == [] # Output expected from the second level assert len(out2) == 1 assert out2[0] == "You called a command in SecondLevel with {!r}.".format(line) def test_submenu_say_from_second_level(secondlevel_app): line = 'testing' out = run_cmd(secondlevel_app, 'say ' + line) assert out == ["You called a command in SecondLevel with '%s'." % line] def test_submenu_help_second_say_from_top_level(submenu_app): out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say') # No output expected from the top level assert out1 == [] # Output expected from the second level assert out2 == ["This is a second level menu. Options are qwe, asd, zxc"] def test_submenu_help_say_from_second_level(secondlevel_app): out = run_cmd(secondlevel_app, 'help say') assert out == ["This is a second level menu. Options are qwe, asd, zxc"] def test_submenu_help_second(submenu_app): out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second') out3 = run_cmd(second_level_cmd, 'help') assert out2 == out3 def test_submenu_from_top_help_second_say(submenu_app): out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'help second say') out3 = run_cmd(second_level_cmd, 'help say') assert out2 == out3 def test_submenu_shared_attribute(submenu_app): out1, out2 = run_submenu_cmd(submenu_app, second_level_cmd, 'second get_top_level_attr') assert out2 == [str(submenu_app.top_level_attr)] def test_submenu_shared_attribute_preserve(submenu_app): out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr') assert out2 == [str(submenu_app.top_level_attr)] out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb set_top_level_attr') assert submenu_app.top_level_attr == 987654321 out1, out2 = run_submenu_cmd(submenu_app, second_level_b_cmd, 'secondb get_top_level_attr') assert out2 == [str(987654321)] cmd2-0.8.5/tests/test_transcript.py0000644000076500000240000002544113263556236021314 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Cmd2 functional testing based on transcript Copyright 2016 Federico Ceratto Released under MIT license, see LICENSE file """ import os import sys import re import random import mock import pytest import six from cmd2 import (Cmd, options, Cmd2TestCase, set_use_arg_list, set_posix_shlex, set_strip_quotes) from conftest import run_cmd, StdOut, normalize from optparse import make_option class CmdLineApp(Cmd): MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] def __init__(self, *args, **kwargs): self.multilineCommands = ['orate'] self.maxrepeats = 3 self.redirector = '->' # Add stuff to settable and/or shortcuts before calling base class initializer self.settable['maxrepeats'] = 'Max number of `--repeat`s allowed' # Need to use this older form of invoking super class constructor to support Python 2.x and Python 3.x Cmd.__init__(self, *args, **kwargs) self.intro = 'This is an intro banner ...' # Configure how arguments are parsed for @options commands set_posix_shlex(False) set_strip_quotes(True) set_use_arg_list(False) opts = [make_option('-p', '--piglatin', action="store_true", help="atinLay"), make_option('-s', '--shout', action="store_true", help="N00B EMULATION MODE"), make_option('-r', '--repeat', type="int", help="output [n] times")] @options(opts, arg_desc='(text to say)') def do_speak(self, arg, opts=None): """Repeats what you tell me to.""" arg = ''.join(arg) 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) # recommend using the poutput function instead of # self.stdout.write or "print", because Cmd allows the user # to redirect output do_say = do_speak # now "say" is a synonym for "speak" do_orate = do_speak # another synonym, but this one takes multi-line input @options([ make_option('-r', '--repeat', type="int", help="output [n] times") ]) def do_mumble(self, arg, opts=None): """Mumbles what you tell me to.""" repetitions = opts.repeat or 1 arg = arg.split() for i in range(min(repetitions, self.maxrepeats)): output = [] if (random.random() < .33): output.append(random.choice(self.MUMBLE_FIRST)) for word in arg: if (random.random() < .40): output.append(random.choice(self.MUMBLES)) output.append(word) if (random.random() < .25): output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) class DemoApp(Cmd): @options(make_option('-n', '--name', action="store", help="your name")) def do_hello(self, arg, opts): """Says hello.""" if opts.name: self.stdout.write('Hello {}\n'.format(opts.name)) else: self.stdout.write('Hello Nobody\n') @pytest.fixture def _cmdline_app(): c = CmdLineApp() c.stdout = StdOut() return c @pytest.fixture def _demo_app(): c = DemoApp() c.stdout = StdOut() return c def _get_transcript_blocks(transcript): cmd = None expected = '' for line in transcript.splitlines(): if line.startswith('(Cmd) '): if cmd is not None: yield cmd, normalize(expected) cmd = line[6:] expected = '' else: expected += line + '\n' yield cmd, normalize(expected) def test_base_with_transcript(_cmdline_app): app = _cmdline_app transcript = """ (Cmd) help Documented commands (type help ): ======================================== alias help load orate pyscript say shell speak edit history mumble py quit set shortcuts unalias (Cmd) help say Repeats what you tell me to. Usage: speak [options] (text to say) Options: -h, --help show this help message and exit -p, --piglatin atinLay -s, --shout N00B EMULATION MODE -r REPEAT, --repeat=REPEAT output [n] times (Cmd) say goodnight, Gracie goodnight, Gracie (Cmd) say -ps --repeat=5 goodnight, Gracie OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY (Cmd) set maxrepeats 5 maxrepeats - was: 3 now: 5 (Cmd) say -ps --repeat=5 goodnight, Gracie OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY (Cmd) history -------------------------[1] help -------------------------[2] help say -------------------------[3] say goodnight, Gracie -------------------------[4] say -ps --repeat=5 goodnight, Gracie -------------------------[5] set maxrepeats 5 -------------------------[6] say -ps --repeat=5 goodnight, Gracie (Cmd) history -r 4 OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY OODNIGHT, GRACIEGAY (Cmd) set prompt "---> " prompt - was: (Cmd) now: ---> """ for cmd, expected in _get_transcript_blocks(transcript): out = run_cmd(app, cmd) assert out == expected class TestMyAppCase(Cmd2TestCase): CmdApp = CmdLineApp CmdApp.testfiles = ['tests/transcript.txt'] def test_optparser(_cmdline_app, capsys): run_cmd(_cmdline_app, 'say -h') out, err = capsys.readouterr() expected = normalize(""" Repeats what you tell me to. Usage: speak [options] (text to say) Options: -h, --help show this help message and exit -p, --piglatin atinLay -s, --shout N00B EMULATION MODE -r REPEAT, --repeat=REPEAT output [n] times""") # NOTE: For some reason this extra cast to str is required for Python 2.7 but not 3.x assert normalize(str(out)) == expected def test_optparser_nosuchoption(_cmdline_app, capsys): run_cmd(_cmdline_app, 'say -a') out, err = capsys.readouterr() expected = normalize(""" no such option: -a Repeats what you tell me to. Usage: speak [options] (text to say) Options: -h, --help show this help message and exit -p, --piglatin atinLay -s, --shout N00B EMULATION MODE -r REPEAT, --repeat=REPEAT output [n] times""") assert normalize(str(out)) == expected def test_comment_stripping(_cmdline_app): out = run_cmd(_cmdline_app, 'speak it was /* not */ delicious! # Yuck!') expected = normalize("""it was delicious!""") assert out == expected def test_optarser_correct_args_with_quotes_and_midline_options(_cmdline_app): out = run_cmd(_cmdline_app, "speak 'This is a' -s test of the emergency broadcast system!") expected = normalize("""THIS IS A TEST OF THE EMERGENCY BROADCAST SYSTEM!""") assert out == expected def test_optarser_options_with_spaces_in_quotes(_demo_app): out = run_cmd(_demo_app, "hello foo -n 'Bugs Bunny' bar baz") expected = normalize("""Hello Bugs Bunny""") assert out == expected def test_commands_at_invocation(): testargs = ["prog", "say hello", "say Gracie", "quit"] expected = "This is an intro banner ...\nhello\nGracie\n" with mock.patch.object(sys, 'argv', testargs): app = CmdLineApp() app.stdout = StdOut() app.cmdloop() out = app.stdout.buffer assert out == expected def test_invalid_syntax(_cmdline_app, capsys): run_cmd(_cmdline_app, 'speak "') out, err = capsys.readouterr() expected = normalize("""ERROR: Invalid syntax: No closing quotation""") assert normalize(str(err)) == expected @pytest.mark.parametrize('filename, feedback_to_output', [ ('bol_eol.txt', False), ('characterclass.txt', False), ('dotstar.txt', False), ('extension_notation.txt', False), ('from_cmdloop.txt', True), ('multiline_no_regex.txt', False), ('multiline_regex.txt', False), ('regex_set.txt', False), ('singleslash.txt', False), ('slashes_escaped.txt', False), ('slashslash.txt', False), ('spaces.txt', False), ('word_boundaries.txt', False), ]) def test_transcript(request, capsys, filename, feedback_to_output): # Create a cmd2.Cmd() instance and make sure basic settings are # like we want for test app = CmdLineApp() app.feedback_to_output = feedback_to_output # Get location of the transcript test_dir = os.path.dirname(request.module.__file__) transcript_file = os.path.join(test_dir, 'transcripts', filename) # Need to patch sys.argv so cmd2 doesn't think it was called with # arguments equal to the py.test args testargs = ['prog', '-t', transcript_file] with mock.patch.object(sys, 'argv', testargs): # Run the command loop app.cmdloop() # Check for the unittest "OK" condition for the 1 test which ran expected_start = ".\n----------------------------------------------------------------------\nRan 1 test in" expected_end = "s\n\nOK\n" out, err = capsys.readouterr() if six.PY3: assert err.startswith(expected_start) assert err.endswith(expected_end) else: assert err == '' assert out == '' @pytest.mark.parametrize('expected, transformed', [ # strings with zero or one slash or with escaped slashes means no regular # expression present, so the result should just be what re.escape returns. # we don't use static strings in these tests because re.escape behaves # differently in python 3.7 than in prior versions ( 'text with no slashes', re.escape('text with no slashes') ), ( 'specials .*', re.escape('specials .*') ), ( 'use 2/3 cup', re.escape('use 2/3 cup') ), ( '/tmp is nice', re.escape('/tmp is nice') ), ( 'slash at end/', re.escape('slash at end/') ), # escaped slashes ( 'not this slash\/ or this one\/', re.escape('not this slash/ or this one/' ) ), # regexes ( '/.*/', '.*' ), ( 'specials ^ and + /[0-9]+/', re.escape('specials ^ and + ') + '[0-9]+' ), ( '/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}' + re.escape(' but not /a{6} with ') + '.*?' + re.escape(' more') ), ( 'not \/, use /\|?/, not \/', re.escape('not /, use ') + '\|?' + re.escape(', not /') ), # inception: slashes in our regex. backslashed on input, bare on output ( 'not \/, use /\/?/, not \/', re.escape('not /, use ') + '/?' + re.escape(', not /') ), ( 'lots /\/?/ more /.*/ stuff', re.escape('lots ') + '/?' + re.escape(' more ') + '.*' + re.escape(' stuff') ), ]) def test_parse_transcript_expected(expected, transformed): app = CmdLineApp() class TestMyAppCase(Cmd2TestCase): cmdapp = app testcase = TestMyAppCase() assert testcase._transform_transcript_expected(expected) == transformed cmd2-0.8.5/tests/test_completion.py0000644000076500000240000010410213264702441021254 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Unit/functional testing for readline tab-completion functions in the cmd2.py module. These are primarily tests related to readline completer functions which handle tab-completion of cmd2/cmd commands, file system paths, and shell commands. Copyright 2017 Todd Leonhardt Released under MIT license, see LICENSE file """ import argparse import os import sys import cmd2 import mock import pytest # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) try: import gnureadline as readline except ImportError: # Try to import readline, but allow failure for convenience in Windows unit testing # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows try: # noinspection PyUnresolvedReferences import readline except ImportError: pass # List of strings used with completion functions food_item_strs = ['Pizza', 'Ham', 'Ham Sandwich', 'Potato'] sport_item_strs = ['Bat', 'Basket', 'Basketball', 'Football', 'Space Ball'] delimited_strs = \ [ '/home/user/file.txt', '/home/user/file space.txt', '/home/user/prog.c', '/home/other user/maps', '/home/other user/tests' ] # Dictionary used with flag based completion functions flag_dict = \ { # Tab-complete food items after -f and --food flag in command line '-f': food_item_strs, '--food': food_item_strs, # Tab-complete sport items after -s and --sport flag in command line '-s': sport_item_strs, '--sport': sport_item_strs, } # Dictionary used with index based completion functions index_dict = \ { 1: food_item_strs, # Tab-complete food items at index 1 in command line 2: sport_item_strs, # Tab-complete sport items at index 2 in command line } class CompletionsExample(cmd2.Cmd): """ Example cmd2 application used to exercise tab-completion tests """ def __init__(self): cmd2.Cmd.__init__(self) def do_test_basic(self, args): pass def complete_test_basic(self, text, line, begidx, endidx): return self.basic_complete(text, line, begidx, endidx, food_item_strs) def do_test_delimited(self, args): pass def complete_test_delimited(self, text, line, begidx, endidx): return self.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/') @pytest.fixture def cmd2_app(): c = CompletionsExample() return c def complete_tester(text, line, begidx, endidx, app): """ This is a convenience function to test cmd2.complete() since in a unit test environment there is no actual console readline is monitoring. Therefore we use mock to provide readline data to complete(). :param text: str - the string prefix we are attempting to match :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param app: the cmd2 app that will run completions :return: The first matched string or None if there are no matches Matches are stored in app.completion_matches These matches also have been sorted by complete() """ def get_line(): return line def get_begidx(): return begidx def get_endidx(): return endidx first_match = None with mock.patch.object(readline, 'get_line_buffer', get_line): with mock.patch.object(readline, 'get_begidx', get_begidx): with mock.patch.object(readline, 'get_endidx', get_endidx): # Run the readline tab-completion function with readline mocks in place first_match = app.complete(text, 0) return first_match def test_cmd2_command_completion_single(cmd2_app): text = 'he' line = text endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] def test_complete_command_single(cmd2_app): text = 'he' line = text endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == ['help '] def test_complete_empty_arg(cmd2_app): text = '' line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected = sorted(cmd2_app.complete_help(text, line, begidx, endidx)) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ cmd2_app.completion_matches == expected def test_complete_bogus_command(cmd2_app): text = '' line = 'fizbuzz {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is None def test_cmd2_command_completion_single(cmd2_app): text = 'hel' line = text endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] def test_cmd2_command_completion_multiple(cmd2_app): text = 'h' line = text endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history'] def test_cmd2_command_completion_nomatch(cmd2_app): text = 'fakecommand' line = text endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == [] def test_cmd2_help_completion_single(cmd2_app): text = 'he' line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_help(text, line, begidx, endidx) == ['help'] def test_cmd2_help_completion_multiple(cmd2_app): text = 'h' line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(cmd2_app.complete_help(text, line, begidx, endidx)) assert matches == ['help', 'history'] def test_cmd2_help_completion_nomatch(cmd2_app): text = 'fakecommand' line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_help(text, line, begidx, endidx) == [] def test_shell_command_completion_shortcut(cmd2_app): # Made sure ! runs a shell command and all matches start with ! since there # isn't a space between ! and the shell command. Display matches won't # begin with the !. if sys.platform == "win32": text = '!calc' expected = ['!calc.exe '] expected_display = ['calc.exe'] else: text = '!egr' expected = ['!egrep '] expected_display = ['egrep'] line = text endidx = len(line) begidx = 0 first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ cmd2_app.completion_matches == expected and \ cmd2_app.display_matches == expected_display def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): if sys.platform == "win32": text = 'c*' else: text = 'e*' line = 'shell {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_shell(text, line, begidx, endidx) == [] def test_shell_command_completion_multiple(cmd2_app): if sys.platform == "win32": text = 'c' expected = 'calc.exe' else: text = 'l' expected = 'ls' line = 'shell {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert expected in cmd2_app.complete_shell(text, line, begidx, endidx) def test_shell_command_completion_nomatch(cmd2_app): text = 'zzzz' line = 'shell {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_shell(text, line, begidx, endidx) == [] def test_shell_command_completion_doesnt_complete_when_just_shell(cmd2_app): text = '' line = 'shell {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_shell(text, line, begidx, endidx) == [] def test_shell_command_completion_does_path_completion_when_after_command(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'conftest') line = 'shell cat {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_shell(text, line, begidx, endidx) == [text + '.py'] def test_path_completion_single_end(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'conftest') line = 'shell cat {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.path_complete(text, line, begidx, endidx) == [text + '.py'] def test_path_completion_multiple(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 's') line = 'shell cat {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(cmd2_app.path_complete(text, line, begidx, endidx)) expected = [text + 'cript.py', text + 'cript.txt', text + 'cripts' + os.path.sep] assert matches == expected def test_path_completion_nomatch(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'fakepath') line = 'shell cat {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.path_complete(text, line, begidx, endidx) == [] def test_default_to_shell_completion(cmd2_app, request): cmd2_app.default_to_shell = True test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'conftest') if sys.platform == "win32": command = 'calc.exe' else: command = 'egrep' # Make sure the command is on the testing system assert command in cmd2_app.get_exes_in_path(command) line = '{} {}'.format(command, text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == [text + '.py '] def test_path_completion_cwd(cmd2_app): # Run path complete with no search text text = '' line = 'shell ls {}'.format(text) endidx = len(line) begidx = endidx - len(text) completions_no_text = cmd2_app.path_complete(text, line, begidx, endidx) # Run path complete with path set to the CWD text = os.getcwd() + os.path.sep line = 'shell ls {}'.format(text) endidx = len(line) begidx = endidx - len(text) # We have to strip off the text from the beginning since the matches are entire paths completions_cwd = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)] # Verify that the first test gave results for entries in the cwd assert completions_no_text == completions_cwd assert completions_cwd def test_path_completion_doesnt_match_wildcards(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c*') line = 'shell cat {}'.format(text) endidx = len(line) begidx = endidx - len(text) # Currently path completion doesn't accept wildcards, so will always return empty results assert cmd2_app.path_complete(text, line, begidx, endidx) == [] def test_path_completion_expand_user_dir(cmd2_app): # Get the current user. We can't use getpass.getuser() since # that doesn't work when running these tests on Windows in AppVeyor. user = os.path.basename(os.path.expanduser('~')) text = '~{}'.format(user) line = 'shell fake {}'.format(text) endidx = len(line) begidx = endidx - len(text) completions = cmd2_app.path_complete(text, line, begidx, endidx) expected = text + os.path.sep assert expected in completions def test_path_completion_user_expansion(cmd2_app): # Run path with a tilde and a slash if sys.platform.startswith('win'): cmd = 'dir' else: cmd = 'ls' # Use a ~ which will be expanded into the user's home directory text = '~{}'.format(os.path.sep) line = 'shell {} {}'.format(cmd, text) endidx = len(line) begidx = endidx - len(text) completions_tilde_slash = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)] # Run path complete on the user's home directory text = os.path.expanduser('~') + os.path.sep line = 'shell {} {}'.format(cmd, text) endidx = len(line) begidx = endidx - len(text) completions_home = [match.replace(text, '', 1) for match in cmd2_app.path_complete(text, line, begidx, endidx)] assert completions_tilde_slash == completions_home def test_path_completion_directories_only(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 's') line = 'shell cat {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected = [text + 'cripts' + os.path.sep] assert cmd2_app.path_complete(text, line, begidx, endidx, dir_only=True) == expected def test_basic_completion_single(cmd2_app): text = 'Pi' line = 'list_food -f {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == ['Pizza'] def test_basic_completion_multiple(cmd2_app): text = '' line = 'list_food -f {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs)) assert matches == sorted(food_item_strs) def test_basic_completion_nomatch(cmd2_app): text = 'q' line = 'list_food -f {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.basic_complete(text, line, begidx, endidx, food_item_strs) == [] def test_delimiter_completion(cmd2_app): text = '/home/' line = 'load {}'.format(text) endidx = len(line) begidx = endidx - len(text) cmd2_app.delimiter_complete(text, line, begidx, endidx, delimited_strs, '/') # Remove duplicates from display_matches and sort it. This is typically done in complete(). display_set = set(cmd2_app.display_matches) display_list = list(display_set) display_list.sort() assert display_list == ['other user', 'user'] def test_flag_based_completion_single(cmd2_app): text = 'Pi' line = 'list_food -f {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == ['Pizza'] def test_flag_based_completion_multiple(cmd2_app): text = '' line = 'list_food -f {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict)) assert matches == sorted(food_item_strs) def test_flag_based_completion_nomatch(cmd2_app): text = 'q' line = 'list_food -f {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [] def test_flag_based_default_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') line = 'list_food {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict, cmd2_app.path_complete) == [text + 'onftest.py'] def test_flag_based_callable_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') line = 'list_food -o {}'.format(text) endidx = len(line) begidx = endidx - len(text) flag_dict['-o'] = cmd2_app.path_complete assert cmd2_app.flag_based_complete(text, line, begidx, endidx, flag_dict) == [text + 'onftest.py'] def test_index_based_completion_single(cmd2_app): text = 'Foo' line = 'command Pizza {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == ['Football'] def test_index_based_completion_multiple(cmd2_app): text = '' line = 'command Pizza {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict)) assert matches == sorted(sport_item_strs) def test_index_based_completion_nomatch(cmd2_app): text = 'q' line = 'command {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == [] def test_index_based_default_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') line = 'command Pizza Bat Computer {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict, cmd2_app.path_complete) == [text + 'onftest.py'] def test_index_based_callable_completer(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = os.path.join(test_dir, 'c') line = 'command Pizza Bat {}'.format(text) endidx = len(line) begidx = endidx - len(text) index_dict[3] = cmd2_app.path_complete assert cmd2_app.index_based_complete(text, line, begidx, endidx, index_dict) == [text + 'onftest.py'] def test_tokens_for_completion_quoted(cmd2_app): text = 'Pi' line = 'list_food "{}"'.format(text) endidx = len(line) begidx = endidx expected_tokens = ['list_food', 'Pi', ''] expected_raw_tokens = ['list_food', '"Pi"', ''] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens def test_tokens_for_completion_unclosed_quote(cmd2_app): text = 'Pi' line = 'list_food "{}'.format(text) endidx = len(line) begidx = endidx - len(text) expected_tokens = ['list_food', 'Pi'] expected_raw_tokens = ['list_food', '"Pi'] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens def test_tokens_for_completion_redirect(cmd2_app): text = '>>file' line = 'command | < {}'.format(text) endidx = len(line) begidx = endidx - len(text) cmd2_app.allow_redirection = True expected_tokens = ['command', '|', '<', '>>', 'file'] expected_raw_tokens = ['command', '|', '<', '>>', 'file'] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens def test_tokens_for_completion_quoted_redirect(cmd2_app): text = '>file' line = 'command "{}'.format(text) endidx = len(line) begidx = endidx - len(text) cmd2_app.allow_redirection = True expected_tokens = ['command', '>file'] expected_raw_tokens = ['command', '">file'] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens def test_tokens_for_completion_redirect_off(cmd2_app): text = '>file' line = 'command {}'.format(text) endidx = len(line) begidx = endidx - len(text) cmd2_app.allow_redirection = False expected_tokens = ['command', '>file'] expected_raw_tokens = ['command', '>file'] tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx) assert expected_tokens == tokens assert expected_raw_tokens == raw_tokens def test_parseline_command_and_args(cmd2_app): line = 'help history' command, args, out_line = cmd2_app.parseline(line) assert command == 'help' assert args == 'history' assert line == out_line def test_parseline_emptyline(cmd2_app): line = '' command, args, out_line = cmd2_app.parseline(line) assert command is None assert args is None assert line is out_line def test_parseline_strips_line(cmd2_app): line = ' help history ' command, args, out_line = cmd2_app.parseline(line) assert command == 'help' assert args == 'history' assert line.strip() == out_line def test_parseline_expands_alias(cmd2_app): # Create the alias cmd2_app.do_alias(['fake', 'pyscript']) line = 'fake foobar.py' command, args, out_line = cmd2_app.parseline(line) assert command == 'pyscript' assert args == 'foobar.py' assert line.replace('fake', 'pyscript') == out_line def test_parseline_expands_shortcuts(cmd2_app): line = '!cat foobar.txt' command, args, out_line = cmd2_app.parseline(line) assert command == 'shell' assert args == 'cat foobar.txt' assert line.replace('!', 'shell ') == out_line def test_add_opening_quote_basic_no_text(cmd2_app): text = '' line = 'test_basic {}'.format(text) endidx = len(line) begidx = endidx - len(text) # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == sorted(food_item_strs) def test_add_opening_quote_basic_nothing_added(cmd2_app): text = 'P' line = 'test_basic {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == ['Pizza', 'Potato'] def test_add_opening_quote_basic_quote_added(cmd2_app): text = 'Ha' line = 'test_basic {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected = sorted(['"Ham', '"Ham Sandwich']) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected def test_add_opening_quote_basic_text_is_common_prefix(cmd2_app): # This tests when the text entered is the same as the common prefix of the matches text = 'Ham' line = 'test_basic {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected = sorted(['"Ham', '"Ham Sandwich']) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == expected def test_add_opening_quote_delimited_no_text(cmd2_app): text = '' line = 'test_delimited {}'.format(text) endidx = len(line) begidx = endidx - len(text) # The whole list will be returned with no opening quotes added first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and cmd2_app.completion_matches == sorted(delimited_strs) def test_add_opening_quote_delimited_nothing_added(cmd2_app): text = '/ho' line = 'test_delimited {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected_matches = sorted(delimited_strs) expected_display = sorted(['other user', 'user']) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ cmd2_app.completion_matches == expected_matches and \ cmd2_app.display_matches == expected_display def test_add_opening_quote_delimited_quote_added(cmd2_app): text = '/home/user/fi' line = 'test_delimited {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected_common_prefix = '"/home/user/file' expected_display = sorted(['file.txt', 'file space.txt']) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ cmd2_app.display_matches == expected_display def test_add_opening_quote_delimited_text_is_common_prefix(cmd2_app): # This tests when the text entered is the same as the common prefix of the matches text = '/home/user/file' line = 'test_delimited {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected_common_prefix = '"/home/user/file' expected_display = sorted(['file.txt', 'file space.txt']) first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ cmd2_app.display_matches == expected_display def test_add_opening_quote_delimited_space_in_prefix(cmd2_app): # This test when a space appears before the part of the string that is the display match text = '/home/oth' line = 'test_delimited {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected_common_prefix = '"/home/other user/' expected_display = ['maps', 'tests'] first_match = complete_tester(text, line, begidx, endidx, cmd2_app) assert first_match is not None and \ os.path.commonprefix(cmd2_app.completion_matches) == expected_common_prefix and \ cmd2_app.display_matches == expected_display class SubcommandsExample(cmd2.Cmd): """ Example cmd2 application where we a base command which has a couple subcommands and the "sport" subcommand has tab completion enabled. """ def __init__(self): cmd2.Cmd.__init__(self) # subcommand functions for the base command def base_foo(self, args): """foo subcommand of base command""" self.poutput(args.x * args.y) def base_bar(self, args): """bar subcommand of base command""" self.poutput('((%s))' % args.z) def base_sport(self, args): """sport subcommand of base command""" self.poutput('Sport is {}'.format(args.sport)) # noinspection PyUnusedLocal def complete_base_sport(self, text, line, begidx, endidx): """ Adds tab completion to base sport subcommand """ index_dict = {1: sport_item_strs} return self.index_based_complete(text, line, begidx, endidx, index_dict) # create the top-level parser for the base command base_parser = argparse.ArgumentParser(prog='base') base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') # create the parser for the "foo" subcommand parser_foo = base_subparsers.add_parser('foo', help='foo help') parser_foo.add_argument('-x', type=int, default=1, help='integer') parser_foo.add_argument('y', type=float, help='float') parser_foo.set_defaults(func=base_foo) # create the parser for the "bar" subcommand parser_bar = base_subparsers.add_parser('bar', help='bar help') parser_bar.add_argument('z', help='string') parser_bar.set_defaults(func=base_bar) # create the parser for the "sport" subcommand parser_sport = base_subparsers.add_parser('sport', help='sport help') parser_sport.add_argument('sport', help='Enter name of a sport') # Set both a function and tab completer for the "sport" subcommand parser_sport.set_defaults(func=base_sport, completer=complete_base_sport) @cmd2.with_argparser(base_parser) def do_base(self, args): """Base command help""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('base') # Enable tab completion of base to make sure the subcommands' completers get called. complete_base = cmd2.Cmd.cmd_with_subs_completer @pytest.fixture def sc_app(): app = SubcommandsExample() return app def test_cmd2_subcommand_completion_single_end(sc_app): text = 'f' line = 'base {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) # It is at end of line, so extra space is present assert first_match is not None and sc_app.completion_matches == ['foo '] def test_cmd2_subcommand_completion_multiple(sc_app): text = '' line = 'base {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is not None and sc_app.completion_matches == ['bar', 'foo', 'sport'] def test_cmd2_subcommand_completion_nomatch(sc_app): text = 'z' line = 'base {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is None def test_cmd2_help_subcommand_completion_single(sc_app): text = 'base' line = 'help {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert sc_app.complete_help(text, line, begidx, endidx) == ['base'] def test_cmd2_help_subcommand_completion_multiple(sc_app): text = '' line = 'help base {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(sc_app.complete_help(text, line, begidx, endidx)) assert matches == ['bar', 'foo', 'sport'] def test_cmd2_help_subcommand_completion_nomatch(sc_app): text = 'z' line = 'help base {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert sc_app.complete_help(text, line, begidx, endidx) == [] def test_subcommand_tab_completion(sc_app): # This makes sure the correct completer for the sport subcommand is called text = 'Foot' line = 'base sport {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) # It is at end of line, so extra space is present assert first_match is not None and sc_app.completion_matches == ['Football '] def test_subcommand_tab_completion_with_no_completer(sc_app): # This tests what happens when a subcommand has no completer # In this case, the foo subcommand has no completer defined text = 'Foot' line = 'base foo {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is None def test_subcommand_tab_completion_space_in_text(sc_app): text = 'B' line = 'base sport "Space {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sc_app) assert first_match is not None and \ sc_app.completion_matches == ['Ball" '] and \ sc_app.display_matches == ['Space Ball'] class SecondLevel(cmd2.Cmd): """To be used as a second level command class. """ def __init__(self, *args, **kwargs): cmd2.Cmd.__init__(self, *args, **kwargs) self.prompt = '2ndLevel ' def do_foo(self, line): self.poutput("You called a command in SecondLevel with '%s'. " % line) def help_foo(self): self.poutput("This is a second level menu. Options are qwe, asd, zxc") def complete_foo(self, text, line, begidx, endidx): return [s for s in ['qwe', 'asd', 'zxc'] if s.startswith(text)] second_level_cmd = SecondLevel() @cmd2.AddSubmenu(second_level_cmd, command='second', require_predefined_shares=False) class SubmenuApp(cmd2.Cmd): """To be used as the main / top level command class that will contain other submenus.""" def __init__(self, *args, **kwargs): cmd2.Cmd.__init__(self, *args, **kwargs) self.prompt = 'TopLevel ' @pytest.fixture def sb_app(): app = SubmenuApp() return app def test_cmd2_submenu_completion_single_end(sb_app): text = 'f' line = 'second {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sb_app) # It is at end of line, so extra space is present assert first_match is not None and sb_app.completion_matches == ['foo '] def test_cmd2_submenu_completion_multiple(sb_app): text = 'e' line = 'second {}'.format(text) endidx = len(line) begidx = endidx - len(text) expected = ['edit', 'eof', 'eos'] first_match = complete_tester(text, line, begidx, endidx, sb_app) assert first_match is not None and sb_app.completion_matches == expected def test_cmd2_submenu_completion_nomatch(sb_app): text = 'z' line = 'second {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sb_app) assert first_match is None def test_cmd2_submenu_completion_after_submenu_match(sb_app): text = 'a' line = 'second foo {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sb_app) assert first_match is not None and sb_app.completion_matches == ['asd '] def test_cmd2_submenu_completion_after_submenu_nomatch(sb_app): text = 'b' line = 'second foo {}'.format(text) endidx = len(line) begidx = endidx - len(text) first_match = complete_tester(text, line, begidx, endidx, sb_app) assert first_match is None def test_cmd2_help_submenu_completion_multiple(sb_app): text = 'p' line = 'help second {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(sb_app.complete_help(text, line, begidx, endidx)) assert matches == ['py', 'pyscript'] def test_cmd2_help_submenu_completion_nomatch(sb_app): text = 'fake' line = 'help second {}'.format(text) endidx = len(line) begidx = endidx - len(text) assert sb_app.complete_help(text, line, begidx, endidx) == [] def test_cmd2_help_submenu_completion_subcommands(sb_app): text = 'p' line = 'help second {}'.format(text) endidx = len(line) begidx = endidx - len(text) matches = sorted(sb_app.complete_help(text, line, begidx, endidx)) assert matches == ['py', 'pyscript'] cmd2-0.8.5/tests/test_argparse.py0000644000076500000240000002150413257504271020716 0ustar toddleonhardtstaff00000000000000# coding=utf-8 """ Cmd2 testing for argument parsing """ import argparse import pytest import cmd2 import mock from conftest import run_cmd, StdOut # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) try: import gnureadline as readline except ImportError: # Try to import readline, but allow failure for convenience in Windows unit testing # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows try: # noinspection PyUnresolvedReferences import readline except ImportError: pass class ArgparseApp(cmd2.Cmd): def __init__(self): self.maxrepeats = 3 cmd2.Cmd.__init__(self) say_parser = argparse.ArgumentParser() say_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') say_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') say_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') say_parser.add_argument('words', nargs='+', help='words to say') @cmd2.with_argparser(say_parser) def do_say(self, args): """Repeat what you tell me to.""" words = [] for word in args.words: if word is None: word = '' 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 i in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) self.stdout.write('\n') tag_parser = argparse.ArgumentParser(description='create a html tag') tag_parser.add_argument('tag', help='tag') tag_parser.add_argument('content', nargs='+', help='content to surround with tag') @cmd2.with_argparser(tag_parser) def do_tag(self, args): self.stdout.write('<{0}>{1}'.format(args.tag, ' '.join(args.content))) self.stdout.write('\n') @cmd2.with_argument_list def do_arglist(self, arglist): if isinstance(arglist, list): self.stdout.write('True') else: self.stdout.write('False') @cmd2.with_argument_list @cmd2.with_argument_list def do_arglisttwice(self, arglist): if isinstance(arglist, list): self.stdout.write(' '.join(arglist)) else: self.stdout.write('False') known_parser = argparse.ArgumentParser() known_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay') known_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE') known_parser.add_argument('-r', '--repeat', type=int, help='output [n] times') @cmd2.with_argparser_and_unknown_args(known_parser) def do_speak(self, args, extra): """Repeat what you tell me to.""" words = [] for word in extra: if word is None: word = '' 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 i in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) self.stdout.write('\n') @cmd2.with_argparser_and_unknown_args(known_parser) def do_talk(self, args, extra): words = [] for word in extra: if word is None: word = '' 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 i in range(min(repetitions, self.maxrepeats)): self.stdout.write(' '.join(words)) self.stdout.write('\n') @pytest.fixture def argparse_app(): app = ArgparseApp() app.stdout = StdOut() return app def test_argparse_basic_command(argparse_app): out = run_cmd(argparse_app, 'say hello') assert out == ['hello'] def test_argparse_quoted_arguments(argparse_app): argparse_app.POSIX = False argparse_app.STRIP_QUOTES_FOR_NON_POSIX = True out = run_cmd(argparse_app, 'say "hello there"') assert out == ['hello there'] def test_argparse_with_list(argparse_app): out = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] def test_argparse_with_list_and_empty_doc(argparse_app): out = run_cmd(argparse_app, 'speak -s hello world!') assert out == ['HELLO WORLD!'] def test_argparse_quoted_arguments_multiple(argparse_app): argparse_app.POSIX = False argparse_app.STRIP_QUOTES_FOR_NON_POSIX = True out = run_cmd(argparse_app, 'say "hello there" "rick & morty"') assert out == ['hello there rick & morty'] def test_argparse_quoted_arguments_posix(argparse_app): argparse_app.POSIX = True out = run_cmd(argparse_app, 'tag strong this should be loud') assert out == ['this should be loud'] def test_argparse_quoted_arguments_posix_multiple(argparse_app): argparse_app.POSIX = True out = run_cmd(argparse_app, 'tag strong this "should be" loud') assert out == ['this should be loud'] def test_argparse_help_docstring(argparse_app): out = run_cmd(argparse_app, 'help say') assert out[0].startswith('usage: say') assert out[1] == '' assert out[2] == 'Repeat what you tell me to.' def test_argparse_help_description(argparse_app): out = run_cmd(argparse_app, 'help tag') assert out[0].startswith('usage: tag') assert out[1] == '' assert out[2] == 'create a html tag' def test_argparse_prog(argparse_app): out = run_cmd(argparse_app, 'help tag') progname = out[0].split(' ')[1] assert progname == 'tag' def test_arglist(argparse_app): out = run_cmd(argparse_app, 'arglist "we should" get these') assert out[0] == 'True' def test_arglist_decorator_twice(argparse_app): out = run_cmd(argparse_app, 'arglisttwice "we should" get these') assert out[0] == 'we should get these' class SubcommandApp(cmd2.Cmd): """ Example cmd2 application where we a base command which has a couple subcommands.""" def __init__(self): cmd2.Cmd.__init__(self) # subcommand functions for the base command def base_foo(self, args): """foo subcommand of base command""" self.poutput(args.x * args.y) def base_bar(self, args): """bar subcommand of base command""" self.poutput('((%s))' % args.z) # create the top-level parser for the base command base_parser = argparse.ArgumentParser(prog='base') base_subparsers = base_parser.add_subparsers(title='subcommands', help='subcommand help') # create the parser for the "foo" subcommand parser_foo = base_subparsers.add_parser('foo', help='foo help') parser_foo.add_argument('-x', type=int, default=1, help='integer') parser_foo.add_argument('y', type=float, help='float') parser_foo.set_defaults(func=base_foo) # create the parser for the "bar" subcommand parser_bar = base_subparsers.add_parser('bar', help='bar help') parser_bar.add_argument('z', help='string') parser_bar.set_defaults(func=base_bar) @cmd2.with_argparser(base_parser) def do_base(self, args): """Base command help""" func = getattr(args, 'func', None) if func is not None: # Call whatever subcommand function was selected func(self, args) else: # No subcommand was provided, so call help self.do_help('base') @pytest.fixture def subcommand_app(): app = SubcommandApp() app.stdout = StdOut() return app def test_subcommand_foo(subcommand_app): out = run_cmd(subcommand_app, 'base foo -x2 5.0') assert out == ['10.0'] def test_subcommand_bar(subcommand_app): out = run_cmd(subcommand_app, 'base bar baz') assert out == ['((baz))'] def test_subcommand_invalid(subcommand_app, capsys): run_cmd(subcommand_app, 'base baz') out, err = capsys.readouterr() err = err.splitlines() assert err[0].startswith('usage: base') assert err[1].startswith("base: error: invalid choice: 'baz'") def test_subcommand_base_help(subcommand_app): out = run_cmd(subcommand_app, 'help base') assert out[0].startswith('usage: base') assert out[1] == '' assert out[2] == 'Base command help' def test_subcommand_help(subcommand_app): out = run_cmd(subcommand_app, 'help base foo') assert out[0].startswith('usage: base foo') assert out[1] == '' assert out[2] == 'positional arguments:' def test_subcommand_invalid_help(subcommand_app): out = run_cmd(subcommand_app, 'help base baz') assert out[0].startswith('usage: base') assert out[1].startswith("base: error: invalid choice: 'baz'") cmd2-0.8.5/cmd2.egg-info/0000755000076500000240000000000013264716261016656 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/cmd2.egg-info/PKG-INFO0000644000076500000240000000647013264716261017762 0ustar toddleonhardtstaff00000000000000Metadata-Version: 1.1 Name: cmd2 Version: 0.8.5 Summary: cmd2 - a tool for building interactive command line applications in Python Home-page: https://github.com/python-cmd2/cmd2 Author: Catherine Devlin Author-email: catherine.devlin@gmail.com License: MIT Description-Content-Type: UNKNOWN Description: cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It provides a simple API which is an extension of Python's built-in cmd module. cmd2 provides a wealth of features on top of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary when using cmd. The latest documentation for cmd2 can be read online here: https://cmd2.readthedocs.io/ Main features: - Searchable command history (`history` command and `+r`) - Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`) - Python scripting of your application with ``pyscript`` - Run shell commands with ``!`` - Pipe command output to shell commands with `|` - Redirect command output to file with `>`, `>>`; input from file with `<` - Bare `>`, `>>` with no filename send output to paste buffer (clipboard) - `py` enters interactive Python console (opt-in `ipy` for IPython console) - Multi-line commands - Special-character command shortcuts (beyond cmd's `?` and `!`) - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for sub-commands - Unicode character support (*Python 3 only*) - Good tab-completion of commands, sub-commands, file system paths, and shell commands - Python 2.7 and 3.4+ support - Linux, macOS and Windows support - Trivial to provide built-in help for all commands - Built-in regression testing framework for your applications (transcript-based testing) - Transcripts for use with built-in regression can be automatically generated from `history -t` Usable without modification anywhere cmd is used; simply import cmd2.Cmd in place of cmd.Cmd. Keywords: command prompt console cmd Platform: any Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Operating System :: OS Independent Classifier: Intended Audience :: Developers Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: MIT License Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 2 Classifier: Programming Language :: Python :: 2.7 Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.4 Classifier: Programming Language :: Python :: 3.5 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules cmd2-0.8.5/cmd2.egg-info/SOURCES.txt0000644000076500000240000000352413264716261020546 0ustar toddleonhardtstaff00000000000000CHANGELOG.md CODEOWNERS CONTRIBUTING.md LICENSE MANIFEST.in README.md cmd2.py setup.cfg setup.py tox.ini cmd2.egg-info/PKG-INFO cmd2.egg-info/SOURCES.txt cmd2.egg-info/dependency_links.txt cmd2.egg-info/requires.txt cmd2.egg-info/top_level.txt examples/.cmd2rc examples/.coverage examples/alias_startup.py examples/arg_print.py examples/argparse_example.py examples/environment.py examples/event_loops.py examples/example.py examples/help_categories.py examples/paged_output.py examples/persistent_history.py examples/pirate.py examples/python_scripting.py examples/remove_unused.py examples/subcommands.py examples/submenus.py examples/tab_completion.py examples/table_display.py examples/scripts/arg_printer.py examples/scripts/conditional.py examples/scripts/nested.txt examples/scripts/script.py examples/scripts/script.txt examples/transcripts/exampleSession.txt examples/transcripts/pirate.transcript examples/transcripts/transcript_regex.txt tests/conftest.py tests/redirect.txt tests/relative_multiple.txt tests/script.py tests/script.txt tests/test_argparse.py tests/test_cmd2.py tests/test_completion.py tests/test_parsing.py tests/test_submenu.py tests/test_transcript.py tests/scripts/binary.bin tests/scripts/empty.txt tests/scripts/nested.txt tests/scripts/one_down.txt tests/scripts/postcmds.txt tests/scripts/precmds.txt tests/scripts/raises_exception.py tests/scripts/recursive.py tests/scripts/utf8.txt tests/transcripts/bol_eol.txt tests/transcripts/characterclass.txt tests/transcripts/dotstar.txt tests/transcripts/extension_notation.txt tests/transcripts/from_cmdloop.txt tests/transcripts/multiline_no_regex.txt tests/transcripts/multiline_regex.txt tests/transcripts/regex_set.txt tests/transcripts/singleslash.txt tests/transcripts/slashes_escaped.txt tests/transcripts/slashslash.txt tests/transcripts/spaces.txt tests/transcripts/word_boundaries.txtcmd2-0.8.5/cmd2.egg-info/requires.txt0000644000076500000240000000032113264716261021252 0ustar toddleonhardtstaff00000000000000pyparsing>=2.0.1 pyperclip six [:python_version<'3.0'] subprocess32 [:python_version<'3.4'] enum34 [:python_version<'3.5'] contextlib2 [:sys_platform!='win32'] wcwidth [:sys_platform=='win32'] pyreadline cmd2-0.8.5/cmd2.egg-info/top_level.txt0000644000076500000240000000000513264716261021403 0ustar toddleonhardtstaff00000000000000cmd2 cmd2-0.8.5/cmd2.egg-info/dependency_links.txt0000644000076500000240000000000113264716261022724 0ustar toddleonhardtstaff00000000000000 cmd2-0.8.5/MANIFEST.in0000644000076500000240000000025113246537516016077 0ustar toddleonhardtstaff00000000000000include LICENSE include README.md include CHANGELOG.md include CODEOWNERS include CONTRIBUTING.md include tox.ini recursive-include examples * recursive-include tests * cmd2-0.8.5/README.md0000755000076500000240000002576213264005612015624 0ustar toddleonhardtstaff00000000000000cmd2: a tool for building interactive command line apps ======================================================= [![Latest Version](https://img.shields.io/pypi/v/cmd2.svg?style=flat-square&label=latest%20stable%20version)](https://pypi.python.org/pypi/cmd2/) [![Build status](https://img.shields.io/travis/python-cmd2/cmd2.svg?style=flat-square&label=unix%20build)](https://travis-ci.org/python-cmd2/cmd2) [![Appveyor build status](https://img.shields.io/appveyor/ci/FedericoCeratto/cmd2.svg?style=flat-square&label=windows%20build)](https://ci.appveyor.com/project/FedericoCeratto/cmd2) [![codecov](https://codecov.io/gh/python-cmd2/cmd2/branch/master/graph/badge.svg)](https://codecov.io/gh/python-cmd2/cmd2) [![Documentation Status](https://readthedocs.org/projects/cmd2/badge/?version=latest)](http://cmd2.readthedocs.io/en/latest/?badge=latest) cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It provides a simple API which is an extension of Python's built-in [cmd](https://docs.python.org/3/library/cmd.html) module. cmd2 provides a wealth of features on top of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary when using cmd. [![Screenshot](cmd2.png)](https://github.com/python-cmd2/cmd2/blob/master/cmd2.png) Main Features ------------- - Searchable command history (`history` command and `+r`) - optionally persistent - Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`) - Python scripting of your application with ``pyscript`` - Run shell commands with ``!`` - Pipe command output to shell commands with `|` - Redirect command output to file with `>`, `>>`; input from file with `<` - Bare `>`, `>>` with no filename send output to paste buffer (clipboard) - `py` enters interactive Python console (opt-in `ipy` for IPython console) - Option to display long output using a pager with ``cmd2.Cmd.ppaged()`` - Multi-line commands - Special-character command shortcuts (beyond cmd's `?` and `!`) - Command aliasing similar to bash `alias` command - Ability to load commands at startup from an initialization script - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for sub-commands - Sub-menu support via the ``AddSubmenu`` decorator - Unicode character support (*Python 3 only*) - Good tab-completion of commands, sub-commands, file system paths, and shell commands - Python 2.7 and 3.4+ support - Windows, macOS, and Linux support - Trivial to provide built-in help for all commands - Built-in regression testing framework for your applications (transcript-based testing) - Transcripts for use with built-in regression can be automatically generated from `history -t` Plan for dropping Python 2.7 support ------------------------------------ Support for Python 2.7 will be discontinued on April 15, 2018. After that date, new releases of `cmd2` will only support Python 3. Older releases of `cmd2` will of course continue to support Python 2.7. Supporting Python 2 is an increasing burden on our limited resources. Switching to support only Python 3 will allow us to clean up the codebase, remove some cruft, and focus on developing new features. Installation ------------ On all operating systems, the latest stable version of `cmd2` can be installed using pip: ```bash pip install -U cmd2 ``` cmd2 works with Python 2.7 and Python 3.4+ on Windows, macOS, and Linux. It is pure Python code with the only 3rd-party dependencies being on [six](https://pypi.python.org/pypi/six), [pyparsing](http://pyparsing.wikispaces.com), and [pyperclip](https://github.com/asweigart/pyperclip). Windows has an additional dependency on [pyreadline](https://pypi.python.org/pypi/pyreadline). Non-Windows platforms have an additional dependency on [wcwidth](https://pypi.python.org/pypi/wcwidth). Finally, Python 3.4 and earlier have an additional dependency on [contextlib2](https://pypi.python.org/pypi/contextlib2). For information on other installation options, see [Installation Instructions](https://cmd2.readthedocs.io/en/latest/install.html) in the cmd2 documentation. Documentation ------------- The latest documentation for cmd2 can be read online here: https://cmd2.readthedocs.io/en/latest/ It is available in HTML, PDF, and ePub formats. Feature Overview ---------------- Instructions for implementing each feature follow. - Searchable command history All commands will automatically be tracked in the session's history, unless the command is listed in Cmd's exclude_from_history attribute. The history is accessed through the `history` command. If you wish to exclude some of your custom commands from the history, append their names to the list at `Cmd.exclude_from_history`. - Load commands from file, save to file, edit commands in file Type `help load`, `help history` for details. - Multi-line commands Any command accepts multi-line input when its name is listed in `Cmd.multilineCommands`. The program will keep expecting input until a line ends with any of the characters in `Cmd.terminators` . The default terminators are `;` and `/n` (empty newline). - Special-character shortcut commands (beyond cmd's "@" and "!") To create a single-character shortcut for a command, update `Cmd.shortcuts`. - Settable environment parameters To allow a user to change an environment parameter during program execution, append the parameter's name to `Cmd.settable`` - Parsing commands with `argparse` ```Python import argparse from cmd2 import with_argparser argparser = argparse.ArgumentParser() 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('words', nargs='+', help='words to say') @with_argparser(argparser) 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) self.stdout.write('{}\n'.format(' '.join(words))) ``` See https://cmd2.readthedocs.io/en/latest/argument_processing.html for more details Tutorials --------- A few tutorials on using cmd2 exist: * Florida PyCon 2017 talk: [slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE), [video](https://www.youtube.com/watch?v=6m0RdpITaeY) * PyCon 2010 talk by Catherine Devlin, the original author: [video](http://pyvideo.org/pycon-us-2010/pycon-2010--easy-command-line-applications-with-c.html) * A nice brief step-by-step tutorial: [blog](https://kushaldas.in/posts/developing-command-line-interpreters-using-python-cmd2.html) Example Application ------------------- Example cmd2 application (**examples/example.py**): ```python #!/usr/bin/env python # coding=utf-8 """ A sample application for cmd2. """ import random import argparse from cmd2 import Cmd, with_argparser class CmdLineApp(Cmd): """ Example cmd2 application. """ # Setting this true makes it run a shell command if a cmd2/cmd command doesn't exist # default_to_shell = True MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] def __init__(self): self.multilineCommands = ['orate'] self.maxrepeats = 3 # Add stuff to settable and shortcuts before calling base class initializer self.settable['maxrepeats'] = 'max repetitions for speak command' self.shortcuts.update({'&': 'speak'}) # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell Cmd.__init__(self, use_ipython=False) speak_parser = argparse.ArgumentParser() 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') @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 i in range(min(repetitions, self.maxrepeats)): # .poutput handles newlines, and accommodates output redirection too self.poutput(' '.join(words)) do_say = do_speak # now "say" is a synonym for "speak" do_orate = do_speak # another synonym, but this one takes multi-line input mumble_parser = argparse.ArgumentParser() mumble_parser.add_argument('-r', '--repeat', type=int, help='how many times to repeat') mumble_parser.add_argument('words', nargs='+', help='words to say') @with_argparser(mumble_parser) def do_mumble(self, args): """Mumbles what you tell me to.""" repetitions = args.repeat or 1 for i in range(min(repetitions, self.maxrepeats)): output = [] if (random.random() < .33): output.append(random.choice(self.MUMBLE_FIRST)) for word in args.words: if (random.random() < .40): output.append(random.choice(self.MUMBLES)) output.append(word) if (random.random() < .25): output.append(random.choice(self.MUMBLE_LAST)) self.poutput(' '.join(output)) if __name__ == '__main__': c = CmdLineApp() c.cmdloop() ``` The following is a sample session running example.py. Thanks to Cmd2's built-in transcript testing capability, it also serves as a test suite for example.py when saved as *transcript_regex.txt*. Running ```bash python example.py -t transcript_regex.txt ``` will run all the commands in the transcript against `example.py`, verifying that the output produced matches the transcript. example/transcript_regex.txt: ```text # Run this transcript with "python example.py -t transcript_regex.txt" # The regex for colors is because no color on Windows. # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious (Cmd) set colors: /(True|False)/ continuation_prompt: >/ / debug: False echo: False editor: /.*?/ feedback_to_output: False locals_in_py: True maxrepeats: 3 prompt: (Cmd)/ / quiet: False timing: False ``` Note how a regular expression `/(True|False)/` is used for output of the **show color** command since colored text is currently not available for cmd2 on Windows. Regular expressions can be used anywhere within a transcript file simply by enclosing them within forward slashes, `/`. cmd2-0.8.5/setup.py0000755000076500000240000001115113264702441016046 0ustar toddleonhardtstaff00000000000000#!/usr/bin/python # coding=utf-8 """ Setuptools setup file, used to install or test 'cmd2' """ import sys import setuptools from setuptools import setup VERSION = '0.8.5' DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python" LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It provides a simple API which is an extension of Python's built-in cmd module. cmd2 provides a wealth of features on top of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary when using cmd. The latest documentation for cmd2 can be read online here: https://cmd2.readthedocs.io/ Main features: - Searchable command history (`history` command and `+r`) - Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`) - Python scripting of your application with ``pyscript`` - Run shell commands with ``!`` - Pipe command output to shell commands with `|` - Redirect command output to file with `>`, `>>`; input from file with `<` - Bare `>`, `>>` with no filename send output to paste buffer (clipboard) - `py` enters interactive Python console (opt-in `ipy` for IPython console) - Multi-line commands - Special-character command shortcuts (beyond cmd's `?` and `!`) - Settable environment parameters - Parsing commands with arguments using `argparse`, including support for sub-commands - Unicode character support (*Python 3 only*) - Good tab-completion of commands, sub-commands, file system paths, and shell commands - Python 2.7 and 3.4+ support - Linux, macOS and Windows support - Trivial to provide built-in help for all commands - Built-in regression testing framework for your applications (transcript-based testing) - Transcripts for use with built-in regression can be automatically generated from `history -t` Usable without modification anywhere cmd is used; simply import cmd2.Cmd in place of cmd.Cmd. """ CLASSIFIERS = list(filter(None, map(str.strip, """ Development Status :: 5 - Production/Stable Environment :: Console Operating System :: OS Independent Intended Audience :: Developers Intended Audience :: System Administrators License :: OSI Approved :: MIT License Programming Language :: Python Programming Language :: Python :: 2 Programming Language :: Python :: 2.7 Programming Language :: Python :: 3 Programming Language :: Python :: 3.4 Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries :: Python Modules """.splitlines()))) INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'pyperclip', 'six'] EXTRAS_REQUIRE = { # Windows also requires pyreadline to ensure tab completion works ":sys_platform=='win32'": ['pyreadline'], ":sys_platform!='win32'": ['wcwidth'], # Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout ":python_version<'3.5'": ['contextlib2'], # Python 3.3 and earlier require enum34 backport of enum module from Python 3.4 ":python_version<'3.4'": ['enum34'], # Python 2.7 also requires subprocess32 ":python_version<'3.0'": ['subprocess32'], } if int(setuptools.__version__.split('.')[0]) < 18: EXTRAS_REQUIRE = {} if sys.platform.startswith('win'): INSTALL_REQUIRES.append('pyreadline') else: INSTALL_REQUIRES.append('wcwidth') if sys.version_info < (3, 5): INSTALL_REQUIRES.append('contextlib2') if sys.version_info < (3, 4): INSTALL_REQUIRES.append('enum34') if sys.version_info < (3, 0): INSTALL_REQUIRES.append('subprocess32') # unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python TESTS_REQUIRE = ['mock', 'pytest', 'pytest-xdist'] DOCS_REQUIRE = ['sphinx', 'sphinx_rtd_theme', 'pyparsing', 'pyperclip', 'six'] setup( name="cmd2", version=VERSION, description=DESCRIPTION, long_description=LONG_DESCRIPTION, classifiers=CLASSIFIERS, author='Catherine Devlin', author_email='catherine.devlin@gmail.com', url='https://github.com/python-cmd2/cmd2', license='MIT', platforms=['any'], py_modules=["cmd2"], keywords='command prompt console cmd', install_requires=INSTALL_REQUIRES, extras_require=EXTRAS_REQUIRE, tests_require=TESTS_REQUIRE, ) cmd2-0.8.5/CONTRIBUTING.md0000644000076500000240000005014013257504271016566 0ustar toddleonhardtstaff00000000000000# Contributor's Guide We welcome pull requests from cmd2 users and seasoned Python developers alike! Follow these steps to contribute: 1. Find an issue that needs assistance by searching for the [Help Wanted](https://github.com/python-cmd2/cmd2/labels/help%20wanted) tag. 2. Let us know you are working on it by posting a comment on the issue. 3. Follow the [Contribution Guidelines](#contribution-guidelines) to start working on the issue. 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 this *free* series [How to Contribute to an Open Source Project on GitHub](https://egghead.io/series/how-to-contribute-to-an-open-source-project-on-github). ###### If you've found a bug that is not on the board, [follow these steps](#found-a-bug). -------------------------------------------------------------------------------- ## Contribution Guidelines - [Prerequisites](#prerequisites) - [Forking The Project](#forking-the-project) - [Create A Branch](#create-a-branch) - [Setup Linting](#setup-linting) - [Setup for cmd2 development](#setup-for-cmd2-development) - [Make Changes](#make-changes) - [Run The Test Suite](#run-the-test-suite) - [Squash Your Commits](#squash-your-commits) - [Creating A Pull Request](#creating-a-pull-request) - [Common Steps](#common-steps) - [How We Review and Merge Pull Requests](#how-we-review-and-merge-pull-requests) - [How We Close Stale Issues](#how-we-close-stale-issues) - [Next Steps](#next-steps) - [Other resources](#other-resources) - [Advice](#advice) ### Prerequisites The tables below list all prerequisites along with the minimum required version for each. > _Updating to the latest releases for all prerequisites via pip or conda is recommended_. #### Prerequisites to run cmd2 applications | Prerequisite | Minimum Version | | --------------------------------------------------- | --------------- | | [Python](https://www.python.org/downloads/) | `3.4 or 2.7` | | [six](https://pypi.python.org/pypi/six) | `1.8` | | [pyparsing](http://pyparsing.wikispaces.com) | `2.0.3` | | [pyperclip](https://github.com/asweigart/pyperclip) | `1.6` | #### Additional prerequisites to run cmd2 unit tests | Prerequisite | Minimum Version | | ------------------------------------------- | --------------- | | [pytest](http://doc.pytest.org/en/latest/) | `2.6.3` | | [mock](https://pypi.python.org/pypi/six) | `1.0.1` | ### Additional prerequisites to build cmd2 documentation | Prerequisite | Minimum Version | | ------------------------------------------- | --------------- | | [sphinx](http://www.sphinx-doc.org) | `1.2.3` | | [sphinx-rtd-theme](https://github.com/snide/sphinx_rtd_theme) | `0.1.6` | ### Optional prerequisites for enhanced unit test features | Prerequisite | Minimum Version | | ------------------------------------------- | --------------- | | [pytest-forked](https://pypi.python.org/pypi/pytest-forked)| `0.2` | | [pytest-xdist](https://pypi.python.org/pypi/pytest-xdist)| `1.15` | | [pytest-cov](https://pypi.python.org/pypi/pytest-cov) | `1.8` | If Python is already installed in your machine, run the following commands to validate the versions: ```shell python -V pip freeze | grep six pip freeze | grep pyparsing ``` 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 the [Anaconda](https://www.continuum.io/downloads) distribution because it provides an excellent out-of-the-box install on all Platforms (Windows, Mac, or Linux) and because it supports having multiple Python environments (versions of Python) installed simultaneously. ### Forking The Project #### Setting Up Your System 1. Install [Git](https://git-scm.com/) or your favorite Git client. If you aren't comfortable with Git at the command-line, then both [SmartGit](http://www.syntevo.com/smartgit/) and [GitKraken](https://www.gitkraken.com) are excellent cross-platform graphical Git clients. 2. (Optional) [Setup an SSH Key](https://help.github.com/articles/generating-an-ssh-key/) for GitHub. 3. Create a parent projects directory on your system. For this guide, it will be assumed that it is `~/src` #### Forking cmd2 1. Go to the top level cmd2 repository: 2. Click the "Fork" Button in the upper right hand corner of the interface ([More Details Here](https://help.github.com/articles/fork-a-repo/)) 3. After the repository has been forked, you will be taken to your copy of the cmd2 repo at `yourUsername/cmd2` #### Cloning Your Fork 1. Open a Terminal / Command Line / Bash Shell in your projects directory (_i.e.: `/yourprojectdirectory/`_) 2. Clone your fork of cmd2 ```shell $ git clone https://github.com/yourUsername/cmd2.git ``` ##### (make sure to replace `yourUsername` with your GitHub Username) This will download the entire cmd2 repo to your projects directory. #### Setup Your Upstream 1. Change directory to the new cmd2 directory (`cd cmd2`) 2. Add a remote to the official cmd2 repo: ```shell $ git remote add upstream https://github.com/python-cmd2/cmd2.git ``` Congratulations, you now have a local copy of the cmd2 repo! #### Maintaining Your Fork Now that you have a copy of your fork, there is work you will need to do to keep it current. ##### **Rebasing from Upstream** Do this prior to every time you create a branch for a PR: 1. Make sure you are on the `master` branch > ```shell > $ git status > On branch master > Your branch is up-to-date with 'origin/master'. > ``` > If your aren't on `master`, resolve outstanding files / commits and checkout the `master` branch > ```shell > $ git checkout master > ``` 2. Do A Pull with Rebase Against `upstream` > ```shell > $ 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. 3. (_Optional_) Force push your updated master branch to your GitHub fork > ```shell > $ git push origin master --force > ``` > This will overwrite the master branch of your fork. ### Create A Branch Before you start working, you will need to create a separate branch specific to the issue / feature you're working on. You will push your work to this branch. #### Naming Your Branch Name the branch something like `fix/xxx` or `feature/xxx` where `xxx` is a short description of the changes or feature you are attempting to add. For example `fix/script-files` would be a branch where you fix something specific to script files. #### Adding Your Branch To create a branch on your local machine (and switch to this branch): ```shell $ git checkout -b [name_of_your_new_branch] ``` and to push to GitHub: ```shell $ git push origin [name_of_your_new_branch] ``` ##### If you need more help with branching, take a look at _[this](https://github.com/Kunena/Kunena-Forum/wiki/Create-a-new-branch-with-git-and-manage-branches)_. ### Setup Linting You should have some sort of [PEP8](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. [pylint](https://www.pylint.org) is a good Python linter which can be run at the command-line but also can integrate with many IDEs and editors. > Please do not ignore any linting errors in code you write or modify, as they are meant to **help** you and to ensure a clean and simple code base. Don't worry about linting errors in code you don't touch though - cleaning up the legacy code is a work in progress. ### Setup for cmd2 development Once you have cmd2 cloned, before you start any cmd2 application, you first need to install all of the dependencies: ```bash # Install cmd2 prerequisites pip install -U six pyparsing pyperclip # Install prerequisites for running cmd2 unit tests pip install -U pytest mock # Install prerequisites for building cmd2 documentation pip install -U sphinx sphinx-rtd-theme # Install optional prerequisites for running unit tests in parallel and doing code coverage analysis pip install -U pytest-xdist pytest-cov pytest-forked ``` For doing cmd2 development, you actually do NOT want to have cmd2 installed as a Python package. So if you have previously installed cmd2, make sure to uninstall it: ```bash pip uninstall cmd2 ``` Then you should modify your PYTHONPATH environment variable to include the directory you have cloned the cmd2 repository to. Add a line similar to the following to your .bashrc, .bashprofile, or to your Windows environment variables: ```bash # Use cmd2 Python module from GitHub clone when it isn't installed export PYTHONPATH=$PYTHONPATH:~/src/cmd2 ``` Where `~src/cmd2` is replaced by the directory you cloned your fork of the cmd2 repo to. Now navigate to your terminal to the directory you cloned your fork of the cmd2 repo to and try running the example to make sure everything is working: ```bash cd ~src/cmd2 python examples/example.py ``` If the example app loads, you should see a prompt that says "(Cmd)". You can type `help` to get help or `quit` to quit. If you see that, then congratulations – you're all set. Otherwise, refer to the cmd2 [Installation Instructions](https://cmd2.readthedocs.io/en/latest/install.html#installing). There also might be an error in the console of your Bash / Terminal / Command Line that will help identify the problem. ### Make Changes This bit is up to you! #### How to find the code in the cmd2 codebase to fix/edit? The cmd2 project directory structure is pretty simple and straightforward. All actual code for cmd2 is located in a single file, `cmd2.py`. The code to generate the documentation is in the `docs` directory. Unit tests are in the `tests` directory. The `examples` directory contains examples of how to use cmd2. There are various other files in the root directory, but these are primarily related to continuous integration and to 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: ```shell cd docs make clean html ``` In order to see the changes, use your web browser of choice to open `/docs/_build/html/index.html`. ### Run The Test Suite When you're ready to share your code, run the test suite: ```shell cd py.test ``` and ensure all tests pass. If you have the `pytest-xdist` pytest distributed testing plugin installed, then you can use it to dramatically speed up test execution by running tests in parallel on multiple cores like so: ```shell py.test -n4 ``` where `4` should be replaced by the number of parallel threads you wish to run for testing. If you have the `pytest-forked` pytest plugin (not avilable on Windows) for running tests in isolated formed processes, you can speed things up even further: ```shell py.test -nauto --forked ``` #### Measuring code coverage Code coverage can be measured as follows: ```shell py.test -nauto --cov=cmd2 --cov-report=term-missing --cov-report=html --forked ``` Then use your web browser of choice to look at the results which are in `/htmlcov/index.html`. ### Squash Your Commits When you make a pull request, it is preferable for all of your changes to be in one commit. If you have made more then one commit, then you will can _squash_ your commits. To do this, see [Squashing Your Commits](http://forum.freecodecamp.com/t/how-to-squash-multiple-commits-into-one-with-git/13231). ### Creating A Pull Request #### What is a Pull Request? A pull request (PR) is a method of submitting proposed changes to the cmd2 Repo (or any Repo, for that matter). You will make changes to copies of the files which make up cmd2 in a personal fork, then apply to have them accepted by cmd2 proper. #### Need Help? GitHub has a good guide on how to contribute to open source [here](https://opensource.guide/how-to-contribute/). #### Important: ALWAYS EDIT ON A BRANCH Take away only one thing from this document, it should be this: Never, **EVER** make edits to the `master` branch. ALWAYS make a new branch BEFORE you edit files. This is critical, because if your PR is not accepted, your copy of master will be forever sullied and the only way to fix it is to delete your fork and re-fork. #### Methods There are two methods of creating a pull request for cmd2: - Editing files on a local clone (recommended) - Editing files via the GitHub Interface ##### Method 1: Editing via your Local Fork _(Recommended)_ This is the recommended method. Read about [How to Setup and Maintain a Local Instance of cmd2](#maintaining-your-fork). 1. Perform the maintenance step of rebasing `master`. 2. Ensure you are on the `master` branch using `git status`: ```bash $ git status On branch master Your branch is up-to-date with 'origin/master'. nothing to commit, working directory clean ``` 1. If you are not on master or your working directory is not clean, resolve 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 `fix/short-fix-description` or `feature/short-feature-description`. Review the [Contribution Guidelines](#contribution-guidelines) for more detail. 3. Edit your file(s) locally with the editor of your choice 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 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. 7. Squash your commits, if there are more than one. 8. Push your commits to your GitHub Fork: `git push -u origin branch/name-here` 9. Go to [Common Steps](#common-steps) ##### Method 2: Editing via the GitHub Interface Note: Editing via the GitHub Interface is not recommended, since it is not possible to update your fork via GitHub's interface without deleting and recreating your fork. If you really want to go this route which isn't recommended, you can Google for more information on how to do it. ### Common Steps 1. Once the edits have been committed, you will be prompted to create a pull request on your fork's GitHub Page. 2. By default, all pull requests should be against the cmd2 main repo, `master` branch. 3. Submit a pull request from your branch to cmd2's `master` branch. 4. The title (also called the subject) of your PR should be descriptive of your changes and succinctly indicates what is being fixed. - **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` 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. 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) 7. Creating the PR causes our continuous integration (CI) systems to automatically run all of the unit tests on all supported OSes and all supported versions of Python. You should watch your PR to make sure that all unit tests pass on Both TravisCI (Linux) and AppVeyor (Windows). 8. If any unit tests fail, you should look at the details and fix the failures. You can then push the fix to the same branch in your fork and the PR will automatically get updated and the CI system will automatically run all of the unit tests again. ### How We Review and Merge Pull Requests cmd2 has a team of volunteer Maintainers. These Maintainers routinely go through open pull requests in a process called [Quality Assurance](https://en.wikipedia.org/wiki/Quality_assurance) (QA). We also utilize multiple continuous integration (CI) providers to automatically run all of the unit tests on multiple operating systems and versions of Python. 1. If your changes can merge without conflicts and all unit tests pass for all OSes and supported versions of Python, then your pull request (PR) will have a big green checkbox which says something like "All Checks Passed" next to it. If this is not the case, there will be a link you can click on to get details regarding what the problem is. It is your responsibility to make sure all unit tests are passing. Generally a Maintainer will not QA a pull request unless it can merge without conflicts and all unit tests pass on all supported platforms. 2. If a Maintainer QA's a pull request and confirms that the new code does what it is supposed to do without seeming to introduce any new bugs, and doesn't present any backward compatibility issues, they will merge the pull request. If you would like to apply to join our Maintainer team, message [@tleonhardt](https://github.com/tleonhardt) with links to 5 of your pull requests that have been accepted. ### How We Close Stale Issues We will close any issues that have been inactive for more than 60 days or pull requests that have been inactive for more than 30 days, except those that match the following criteria: - bugs that are confirmed - pull requests that are waiting on other pull requests to be merged - features that are part of a cmd2 GitHub Milestone or Project ### Next Steps #### If your PR is accepted Once your PR is accepted, you may delete the branch you created to submit it. This keeps your working fork clean. You can do this with a press of a button on the GitHub PR interface. You can delete the local copy of the branch with: `git branch -D branch/to-delete-name` #### If your PR is rejected Don't despair! You should receive solid feedback from the Maintainers as to why it was rejected and what changes are needed. Many Pull Requests, especially first Pull Requests, require correction or updating. If you have used the GitHub interface to create your PR, you will need to close your PR, create a new branch, and re-submit. If you have a local copy of the repo, you can make the requested changes and amend your commit with: `git commit --amend` This will update your existing commit. When you push it to your fork you will need to do a force push to overwrite your old commit: `git push --force` Be sure to post in the PR conversation that you have made the requested changes. ### Other resources - [PEP8 Style Guide for Python Code](https://www.python.org/dev/peps/pep-0008/) - [Searching for Your Issue on GitHub](https://help.github.com/articles/searching-issues/) - [Creating a New GitHub Issue](https://help.github.com/articles/creating-an-issue/) ### Advice Here is some advice regarding what makes a good pull request (PR) from the perspective of the cmd2 maintainers: - Multiple smaller PRs divided by topic are better than a single large PR containing a bunch of unrelated changes - Maintaining backward compatibility is important - Good unit/functional tests are very important - Accurate documentation is also important - Adding new features is of the lowest importance, behind bug fixes, unit test additions/improvements, code cleanup, and documentation - It's best to create a dedicated branch for a PR and use it only for that PR (and delete it once the PR has been merged) - It's good if the branch name is related to the PR contents, even if it's just "fix123" or "add_more_tests" - Code coverage of the unit tests matters, try not to decrease it - Think twice before adding dependencies to 3rd party libraries (outside of the Python standard library) because it could affect a lot of users ### Acknowledgement Thanks to the good folks at [freeCodeCamp](https://github.com/freeCodeCamp/freeCodeCamp) for creating an excellent `CONTRIBUTING` file which we have borrowed heavily from. cmd2-0.8.5/cmd2.py0000755000076500000240000055425413264702441015553 0ustar toddleonhardtstaff00000000000000#!/usr/bin/env python # coding=utf-8 """Variant on standard library's cmd with extra features. To use, simply import cmd2.Cmd instead of cmd.Cmd; use precisely as though you were using the standard library's cmd, while enjoying the extra features. Searchable command history (commands: "history") Load commands from file, save to file, edit commands in file Multi-line commands Special-character shortcut commands (beyond cmd's "@" and "!") Settable environment parameters Parsing commands with `argparse` argument parsers (flags) Redirection to file with >, >>; input from file with < Easy transcript-based testing of applications (see examples/example.py) Bash-style ``select`` available Note that redirection with > and | will only work if `self.poutput()` is used in place of `print`. - Catherine Devlin, Jan 03 2008 - catherinedevlin.blogspot.com Git repository on GitHub at https://github.com/python-cmd2/cmd2 """ import argparse import atexit import cmd import codecs import collections import copy import datetime import functools import glob import io import optparse import os import platform import re import shlex import signal import six import sys import tempfile import traceback import unittest from code import InteractiveConsole try: from enum34 import Enum except ImportError: from enum import Enum import pyparsing import pyperclip # Collection is a container that is sizable and iterable # It was introduced in Python 3.6. We will try to import it, otherwise use our implementation try: from collections.abc import Collection, Iterable except ImportError: if six.PY3: from collections.abc import Sized, Iterable, Container else: from collections import Sized, Iterable, Container # noinspection PyAbstractClass class Collection(Sized, Iterable, Container): __slots__ = () # noinspection PyPep8Naming @classmethod def __subclasshook__(cls, C): if cls is Collection: if any("__len__" in B.__dict__ for B in C.__mro__) and \ any("__iter__" in B.__dict__ for B in C.__mro__) and \ any("__contains__" in B.__dict__ for B in C.__mro__): return True return NotImplemented # Newer versions of pyperclip are released as a single file, but older versions had a more complicated structure try: from pyperclip.exceptions import PyperclipException except ImportError: # noinspection PyUnresolvedReferences from pyperclip import PyperclipException # next(it) gets next item of iterator it. This is a replacement for calling it.next() in Python 2 and next(it) in Py3 from six import next # Possible types for text data. This is basestring() in Python 2 and str in Python 3. from six import string_types # Used for sm.input: raw_input() for Python 2 or input() for Python 3 import six.moves as sm # itertools.zip() for Python 2 or zip() for Python 3 - produces an iterator in both cases from six.moves import zip # If using Python 2.7, try to use the subprocess32 package backported from Python 3.2 due to various improvements # NOTE: The feature to pipe output to a shell command won't work correctly in Python 2.7 without this try: # noinspection PyPackageRequirements import subprocess32 as subprocess except ImportError: import subprocess # Python 3.4 and earlier require contextlib2 for temporarily redirecting stderr and stdout if sys.version_info < (3, 5): from contextlib2 import redirect_stdout, redirect_stderr else: from contextlib import redirect_stdout, redirect_stderr if six.PY3: from io import StringIO # Python3 else: from io import BytesIO as StringIO # Python2 # Detect whether IPython is installed to determine if the built-in "ipy" command should be included ipython_available = True try: # noinspection PyUnresolvedReferences,PyPackageRequirements from IPython import embed except ImportError: ipython_available = False # Prefer statically linked gnureadline if available (for macOS compatibility due to issues with libedit) try: import gnureadline as readline except ImportError: # Try to import readline, but allow failure for convenience in Windows unit testing # Note: If this actually fails, you should install readline on Linux or Mac or pyreadline on Windows try: # noinspection PyUnresolvedReferences import readline except ImportError: pass # Check what implementation of readline we are using class RlType(Enum): GNU = 1 PYREADLINE = 2 NONE = 3 rl_type = RlType.NONE if 'pyreadline' in sys.modules: rl_type = RlType.PYREADLINE # Save the original pyreadline display completion function since we need to override it and restore it # noinspection PyProtectedMember orig_pyreadline_display = readline.rl.mode._display_completions elif 'gnureadline' in sys.modules or 'readline' in sys.modules: rl_type = RlType.GNU # We need wcswidth to calculate display width of tab completions from wcwidth import wcswidth # Load the readline lib so we can make changes to it import ctypes readline_lib = ctypes.CDLL(readline.__file__) # Save address that rl_basic_quote_characters is pointing to since we need to override and restore it rl_basic_quote_characters = ctypes.c_char_p.in_dll(readline_lib, "rl_basic_quote_characters") orig_rl_basic_quote_characters_addr = ctypes.cast(rl_basic_quote_characters, ctypes.c_void_p).value # BrokenPipeError and FileNotFoundError exist only in Python 3. Use IOError for Python 2. if six.PY3: BROKEN_PIPE_ERROR = BrokenPipeError FILE_NOT_FOUND_ERROR = FileNotFoundError else: BROKEN_PIPE_ERROR = FILE_NOT_FOUND_ERROR = IOError # On some systems, pyperclip will import gtk for its clipboard functionality. # The following code is a workaround for gtk interfering with printing from a background # thread while the CLI thread is blocking in raw_input() in Python 2 on Linux. if six.PY2 and sys.platform.startswith('lin'): try: # noinspection PyUnresolvedReferences import gtk gtk.set_interactive(0) except ImportError: pass __version__ = '0.8.5' # Pyparsing enablePackrat() can greatly speed up parsing, but problems have been seen in Python 3 in the past pyparsing.ParserElement.enablePackrat() # Override the default whitespace chars in Pyparsing so that newlines are not treated as whitespace pyparsing.ParserElement.setDefaultWhitespaceChars(' \t') # The next 3 variables and associated setter functions effect how arguments are parsed for decorated commands # which use one of the decorators such as @with_argument_list or @with_argparser # The defaults are sane and maximize ease of use for new applications based on cmd2. # To maximize backwards compatibility, we recommend setting USE_ARG_LIST to "False" # Use POSIX or Non-POSIX (Windows) rules for splitting a command-line string into a list of arguments via shlex.split() POSIX_SHLEX = False # Strip outer quotes for convenience if POSIX_SHLEX = False STRIP_QUOTES_FOR_NON_POSIX = True # For @options commands, pass a list of argument strings instead of a single argument string to the do_* methods USE_ARG_LIST = True # Used for tab completion and word breaks. Do not change. QUOTES = ['"', "'"] REDIRECTION_CHARS = ['|', '<', '>'] # optional attribute, when tagged on a function, allows cmd2 to categorize commands HELP_CATEGORY = 'help_category' HELP_SUMMARY = 'help_summary' def categorize(func, category): """Categorize a function. The help command output will group this function under the specified category heading :param func: Union[Callable, Iterable] - function to categorize :param category: str - category to put it in """ if isinstance(func, Iterable): for item in func: setattr(item, HELP_CATEGORY, category) else: setattr(func, HELP_CATEGORY, category) def set_posix_shlex(val): """ Allows user of cmd2 to choose between POSIX and non-POSIX splitting of args for decorated commands. :param val: bool - True => POSIX, False => Non-POSIX """ global POSIX_SHLEX POSIX_SHLEX = val def set_strip_quotes(val): """ Allows user of cmd2 to choose whether to automatically strip outer-quotes when POSIX_SHLEX is False. :param val: bool - True => strip quotes on args for decorated commands if POSIX_SHLEX is False. """ global STRIP_QUOTES_FOR_NON_POSIX STRIP_QUOTES_FOR_NON_POSIX = val def set_use_arg_list(val): """ Allows user of cmd2 to choose between passing @options commands an argument string or list of arg strings. :param val: bool - True => arg is a list of strings, False => arg is a string (for @options commands) """ global USE_ARG_LIST USE_ARG_LIST = val class OptionParser(optparse.OptionParser): """Subclass of optparse.OptionParser which stores a reference to the do_* method it is parsing options for. Used mostly for getting access to the do_* method's docstring when printing help. """ def __init__(self): # Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility optparse.OptionParser.__init__(self) # The do_* method this class is parsing options for. Used for accessing docstring help. self._func = None def exit(self, status=0, msg=None): """Called at the end of showing help when either -h is used to show help or when bad arguments are provided. We override exit so it doesn't automatically exit the application. """ if self.values is not None: self.values._exit = True if msg: print(msg) def print_help(self, *args, **kwargs): """Called when optparse encounters either -h or --help or bad arguments. It prints help for options. We override it so that before the standard optparse help, it prints the do_* method docstring, if available. """ if self._func.__doc__: print(self._func.__doc__) optparse.OptionParser.print_help(self, *args, **kwargs) def error(self, msg): """error(msg : string) Print a usage message incorporating 'msg' to stderr and exit. If you override this in a subclass, it should not return -- it should either exit or raise an exception. """ raise optparse.OptParseError(msg) def remaining_args(opts_plus_args, arg_list): """ Preserves the spacing originally in the arguments after the removal of options. :param opts_plus_args: str - original argument string, including options :param arg_list: List[str] - list of strings containing the non-option arguments :return: str - non-option arguments as a single string, with original spacing preserved """ pattern = '\s+'.join(re.escape(a) for a in arg_list) + '\s*$' match_obj = re.search(pattern, opts_plus_args) try: remaining = opts_plus_args[match_obj.start():] except AttributeError: # Don't preserve spacing, but at least we don't crash and we do preserve args and their order remaining = ' '.join(arg_list) return remaining def _which(editor): try: editor_path = subprocess.check_output(['which', editor], stderr=subprocess.STDOUT).strip() if six.PY3: editor_path = editor_path.decode() except subprocess.CalledProcessError: editor_path = None return editor_path def strip_quotes(arg): """ Strip outer quotes from a string. Applies to both single and double quotes. :param arg: str - string to strip outer quotes from :return str - same string with potentially outer quotes stripped """ quote_chars = '"' + "'" if len(arg) > 1 and arg[0] == arg[-1] and arg[0] in quote_chars: arg = arg[1:-1] return arg def parse_quoted_string(cmdline): """Parse a quoted string into a list of arguments.""" if isinstance(cmdline, list): # arguments are already a list, return the list we were passed lexed_arglist = cmdline else: # Use shlex to split the command line into a list of arguments based on shell rules lexed_arglist = shlex.split(cmdline, posix=POSIX_SHLEX) # If not using POSIX shlex, make sure to strip off outer quotes for convenience if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX: temp_arglist = [] for arg in lexed_arglist: temp_arglist.append(strip_quotes(arg)) lexed_arglist = temp_arglist return lexed_arglist def with_category(category): """A decorator to apply a category to a command function""" def cat_decorator(func): categorize(func, category) return func return cat_decorator def with_argument_list(func): """A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user typed. With this decorator, the decorated method will receive a list of arguments parsed from user input using shlex.split().""" @functools.wraps(func) def cmd_wrapper(self, cmdline): lexed_arglist = parse_quoted_string(cmdline) return func(self, lexed_arglist) cmd_wrapper.__doc__ = func.__doc__ return cmd_wrapper def with_argparser_and_unknown_args(argparser): """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given instance of argparse.ArgumentParser, but also returning unknown args as a list. :param argparser: argparse.ArgumentParser - given instance of ArgumentParser :return: function that gets passed parsed args and a list of unknown args """ # noinspection PyProtectedMember def arg_decorator(func): @functools.wraps(func) def cmd_wrapper(instance, cmdline): lexed_arglist = parse_quoted_string(cmdline) args, unknown = argparser.parse_known_args(lexed_arglist) return func(instance, args, unknown) # argparser defaults the program name to sys.argv[0] # we want it to be the name of our command argparser.prog = func.__name__[3:] # If the description has not been set, then use the method docstring if one exists if argparser.description is None and func.__doc__: argparser.description = func.__doc__ if func.__doc__: setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__) cmd_wrapper.__doc__ = argparser.format_help() # Mark this function as having an argparse ArgumentParser (used by do_help) cmd_wrapper.__dict__['has_parser'] = True # If there are subcommands, store their names in a list to support tab-completion of subcommand names if argparser._subparsers is not None: subcommand_names = argparser._subparsers._group_actions[0]._name_parser_map.keys() cmd_wrapper.__dict__['subcommand_names'] = subcommand_names return cmd_wrapper return arg_decorator def with_argparser(argparser): """A decorator to alter a cmd2 method to populate its ``args`` argument by parsing arguments with the given instance of argparse.ArgumentParser. :param argparser: argparse.ArgumentParser - given instance of ArgumentParser :return: function that gets passed parsed args """ # noinspection PyProtectedMember def arg_decorator(func): @functools.wraps(func) def cmd_wrapper(instance, cmdline): lexed_arglist = parse_quoted_string(cmdline) args = argparser.parse_args(lexed_arglist) return func(instance, args) # argparser defaults the program name to sys.argv[0] # we want it to be the name of our command argparser.prog = func.__name__[3:] # If the description has not been set, then use the method docstring if one exists if argparser.description is None and func.__doc__: argparser.description = func.__doc__ if func.__doc__: setattr(cmd_wrapper, HELP_SUMMARY, func.__doc__) cmd_wrapper.__doc__ = argparser.format_help() # Mark this function as having an argparse ArgumentParser (used by do_help) cmd_wrapper.__dict__['has_parser'] = True # If there are subcommands, store their names in a list to support tab-completion of subcommand names if argparser._subparsers is not None: # Key is subcommand name and value is completer function subcommands = collections.OrderedDict() # Get all subcommands and check if they have completer functions for name, parser in argparser._subparsers._group_actions[0]._name_parser_map.items(): if 'completer' in parser._defaults: completer = parser._defaults['completer'] else: completer = None subcommands[name] = completer cmd_wrapper.__dict__['subcommands'] = subcommands return cmd_wrapper return arg_decorator def options(option_list, arg_desc="arg"): """Used as a decorator and passed a list of optparse-style options, alters a cmd2 method to populate its ``opts`` argument from its raw text argument. Example: transform def do_something(self, arg): into @options([make_option('-q', '--quick', action="store_true", help="Makes things fast")], "source dest") def do_something(self, arg, opts): if opts.quick: self.fast_button = True """ if not isinstance(option_list, list): # If passed a single option instead of a list of options, convert it to a list with one option option_list = [option_list] def option_setup(func): """Decorator function which modifies on of the do_* methods that use the @options decorator. :param func: do_* method which uses the @options decorator :return: modified version of the do_* method """ option_parser = OptionParser() for option in option_list: option_parser.add_option(option) # Allow reasonable help for commands defined with @options and an empty list of options if len(option_list) > 0: option_parser.set_usage("%s [options] %s" % (func.__name__[3:], arg_desc)) else: option_parser.set_usage("%s %s" % (func.__name__[3:], arg_desc)) option_parser._func = func @functools.wraps(func) def new_func(instance, arg): """For @options commands this replaces the actual do_* methods in the instance __dict__. First it does all of the option/argument parsing. Then it calls the underlying do_* method. :param instance: cmd2.Cmd2 derived class application instance :param arg: str - command-line arguments provided to the command :return: bool - returns whatever the result of calling the underlying do_* method would be """ try: # Use shlex to split the command line into a list of arguments based on shell rules opts, new_arglist = option_parser.parse_args(shlex.split(arg, posix=POSIX_SHLEX)) # If not using POSIX shlex, make sure to strip off outer quotes for convenience if not POSIX_SHLEX and STRIP_QUOTES_FOR_NON_POSIX: temp_arglist = [] for arg in new_arglist: temp_arglist.append(strip_quotes(arg)) new_arglist = temp_arglist # Also strip off outer quotes on string option values for key, val in opts.__dict__.items(): if isinstance(val, str): opts.__dict__[key] = strip_quotes(val) # Must find the remaining args in the original argument list, but # mustn't include the command itself # if hasattr(arg, 'parsed') and new_arglist[0] == arg.parsed.command: # new_arglist = new_arglist[1:] if USE_ARG_LIST: arg = new_arglist else: new_args = remaining_args(arg, new_arglist) if isinstance(arg, ParsedString): arg = arg.with_args_replaced(new_args) else: arg = new_args except optparse.OptParseError as e: print(e) option_parser.print_help() return if hasattr(opts, '_exit'): return None result = func(instance, arg, opts) return result new_func.__doc__ = '%s%s' % (func.__doc__ + '\n' if func.__doc__ else '', option_parser.format_help()) return new_func return option_setup # Can we access the clipboard? Should always be true on Windows and Mac, but only sometimes on Linux # noinspection PyUnresolvedReferences try: # Get the version of the pyperclip module as a float pyperclip_ver = float('.'.join(pyperclip.__version__.split('.')[:2])) # The extraneous output bug in pyperclip on Linux using xclip was fixed in more recent versions of pyperclip if sys.platform.startswith('linux') and pyperclip_ver < 1.6: # Avoid extraneous output to stderr from xclip when clipboard is empty at cost of overwriting clipboard contents pyperclip.copy('') else: # Try getting the contents of the clipboard _ = pyperclip.paste() except PyperclipException: can_clip = False else: can_clip = True def get_paste_buffer(): """Get the contents of the clipboard / paste buffer. :return: str - contents of the clipboard """ pb_str = pyperclip.paste() # If value returned from the clipboard is unicode and this is Python 2, convert to a "normal" Python 2 string first if six.PY2 and not isinstance(pb_str, str): import unicodedata pb_str = unicodedata.normalize('NFKD', pb_str).encode('ascii', 'ignore') return pb_str def write_to_paste_buffer(txt): """Copy text to the clipboard / paste buffer. :param txt: str - text to copy to the clipboard """ pyperclip.copy(txt) class ParsedString(str): """Subclass of str which also stores a pyparsing.ParseResults object containing structured parse results.""" # pyarsing.ParseResults - structured parse results, to provide multiple means of access to the parsed data parsed = None # Function which did the parsing parser = None def full_parsed_statement(self): """Used to reconstruct the full parsed statement when a command isn't recognized.""" new = ParsedString('%s %s' % (self.parsed.command, self.parsed.args)) new.parsed = self.parsed new.parser = self.parser return new def with_args_replaced(self, newargs): """Used for @options commands when USE_ARG_LIST is False. It helps figure out what the args are after removing options. """ new = ParsedString(newargs) new.parsed = self.parsed new.parser = self.parser new.parsed['args'] = newargs new.parsed.statement['args'] = newargs return new def replace_with_file_contents(fname): """Action to perform when successfully matching parse element definition for inputFrom parser. :param fname: str - filename :return: str - contents of file "fname" """ try: # Any outer quotes are not part of the filename unquoted_file = strip_quotes(fname[0]) with open(os.path.expanduser(unquoted_file)) as source_file: result = source_file.read() except IOError: result = '< %s' % fname[0] # wasn't a file after all # TODO: IF pyparsing input parser logic gets fixed to support empty file, add support to get from paste buffer return result class EmbeddedConsoleExit(SystemExit): """Custom exception class for use with the py command.""" pass class EmptyStatement(Exception): """Custom exception class for handling behavior when the user just presses .""" pass # Regular expression to match ANSI escape codes ANSI_ESCAPE_RE = re.compile(r'\x1b[^m]*m') def strip_ansi(text): """Strip ANSI escape codes from a string. :param text: str - a string which may contain ANSI escape codes :return: str - the same string with any ANSI escape codes removed """ return ANSI_ESCAPE_RE.sub('', text) def _pop_readline_history(clear_history=True): """Returns a copy of readline's history and optionally clears it (default)""" # noinspection PyArgumentList history = [ readline.get_history_item(i) for i in range(1, 1 + readline.get_current_history_length()) ] if clear_history: readline.clear_history() return history def _push_readline_history(history, clear_history=True): """Restores readline's history and optionally clears it first (default)""" if clear_history: readline.clear_history() for line in history: readline.add_history(line) def _complete_from_cmd(cmd_obj, text, line, begidx, endidx): """Complete as though the user was typing inside cmd's cmdloop()""" from itertools import takewhile command_subcommand_params = line.split(None, 3) if len(command_subcommand_params) < (3 if text else 2): n = len(command_subcommand_params[0]) n += sum(1 for _ in takewhile(str.isspace, line[n:])) return cmd_obj.completenames(text, line[n:], begidx - n, endidx - n) command, subcommand = command_subcommand_params[:2] n = len(command) + sum(1 for _ in takewhile(str.isspace, line)) cfun = getattr(cmd_obj, 'complete_' + subcommand, cmd_obj.complete) return cfun(text, line[n:], begidx - n, endidx - n) class AddSubmenu(object): """Conveniently add a submenu (Cmd-like class) to a Cmd e.g. given "class SubMenu(Cmd): ..." then @AddSubmenu(SubMenu(), 'sub') class MyCmd(cmd.Cmd): .... will have the following effects: 1. 'sub' will interactively enter the cmdloop of a SubMenu instance 2. 'sub cmd args' will call do_cmd(args) in a SubMenu instance 3. 'sub ... [TAB]' will have the same behavior as [TAB] in a SubMenu cmdloop i.e., autocompletion works the way you think it should 4. 'help sub [cmd]' will print SubMenu's help (calls its do_help()) """ class _Nonexistent(object): """ Used to mark missing attributes. Disable __dict__ creation since this class does nothing """ __slots__ = () # def __init__(self, submenu, command, aliases=(), reformat_prompt="{super_prompt}>> {sub_prompt}", shared_attributes=None, require_predefined_shares=True, create_subclass=False, preserve_shares=False, persistent_history_file=None ): """Set up the class decorator submenu (Cmd): Instance of something cmd.Cmd-like command (str): The command the user types to access the SubMenu instance aliases (iterable): More commands that will behave like "command" reformat_prompt (str): Format str or None to disable if it's a string, it should contain one or more of: {super_prompt}: The current cmd's prompt {command}: The command in the current cmd with which it was called {sub_prompt}: The subordinate cmd's original prompt the default is "{super_prompt}{command} {sub_prompt}" shared_attributes (dict): dict of the form {'subordinate_attr': 'parent_attr'} the attributes are copied to the submenu at the last moment; the submenu's attributes are backed up before this and restored afterward require_predefined_shares: The shared attributes above must be independently defined in the subordinate Cmd (default: True) create_subclass: put the modifications in a subclass rather than modifying the existing class (default: False) """ self.submenu = submenu self.command = command self.aliases = aliases if persistent_history_file: self.persistent_history_file = os.path.expanduser(persistent_history_file) else: self.persistent_history_file = None if reformat_prompt is not None and not isinstance(reformat_prompt, str): raise ValueError("reformat_prompt should be either a format string or None") self.reformat_prompt = reformat_prompt self.shared_attributes = {} if shared_attributes is None else shared_attributes if require_predefined_shares: for attr in self.shared_attributes.keys(): if not hasattr(submenu, attr): raise AttributeError("The shared attribute '{attr}' is not defined in {cmd}. Either define {attr} " "in {cmd} or set require_predefined_shares=False." .format(cmd=submenu.__class__.__name__, attr=attr)) self.create_subclass = create_subclass self.preserve_shares = preserve_shares def _get_original_attributes(self): return { attr: getattr(self.submenu, attr, AddSubmenu._Nonexistent) for attr in self.shared_attributes.keys() } def _copy_in_shared_attrs(self, parent_cmd): for sub_attr, par_attr in self.shared_attributes.items(): setattr(self.submenu, sub_attr, getattr(parent_cmd, par_attr)) def _copy_out_shared_attrs(self, parent_cmd, original_attributes): if self.preserve_shares: for sub_attr, par_attr in self.shared_attributes.items(): setattr(parent_cmd, par_attr, getattr(self.submenu, sub_attr)) else: for attr, value in original_attributes.items(): if attr is not AddSubmenu._Nonexistent: setattr(self.submenu, attr, value) else: delattr(self.submenu, attr) def __call__(self, cmd_obj): """Creates a subclass of Cmd wherein the given submenu can be accessed via the given command""" def enter_submenu(parent_cmd, line): """ This function will be bound to do_ and will change the scope of the CLI to that of the submenu. """ submenu = self.submenu original_attributes = self._get_original_attributes() history = _pop_readline_history() if self.persistent_history_file: try: readline.read_history_file(self.persistent_history_file) except FILE_NOT_FOUND_ERROR: pass try: # copy over any shared attributes self._copy_in_shared_attrs(parent_cmd) if line.parsed.args: # Remove the menu argument and execute the command in the submenu line = submenu.parser_manager.parsed(line.parsed.args) submenu.precmd(line) ret = submenu.onecmd(line) submenu.postcmd(ret, line) else: if self.reformat_prompt is not None: prompt = submenu.prompt submenu.prompt = self.reformat_prompt.format( super_prompt=parent_cmd.prompt, command=self.command, sub_prompt=prompt, ) submenu.cmdloop() if self.reformat_prompt is not None: # noinspection PyUnboundLocalVariable self.submenu.prompt = prompt finally: # copy back original attributes self._copy_out_shared_attrs(parent_cmd, original_attributes) # write submenu history if self.persistent_history_file: readline.write_history_file(self.persistent_history_file) # reset main app history before exit _push_readline_history(history) def complete_submenu(_self, text, line, begidx, endidx): """ This function will be bound to complete_ and will perform the complete commands of the submenu. """ submenu = self.submenu original_attributes = self._get_original_attributes() try: # copy over any shared attributes self._copy_in_shared_attrs(_self) return _complete_from_cmd(submenu, text, line, begidx, endidx) finally: # copy back original attributes self._copy_out_shared_attrs(_self, original_attributes) original_do_help = cmd_obj.do_help original_complete_help = cmd_obj.complete_help def help_submenu(_self, line): """ This function will be bound to help_ and will call the help commands of the submenu. """ tokens = line.split(None, 1) if tokens and (tokens[0] == self.command or tokens[0] in self.aliases): self.submenu.do_help(tokens[1] if len(tokens) == 2 else '') else: original_do_help(_self, line) def _complete_submenu_help(_self, text, line, begidx, endidx): """autocomplete to match help_submenu()'s behavior""" tokens = line.split(None, 1) if len(tokens) == 2 and ( not (not tokens[1].startswith(self.command) and not any( tokens[1].startswith(alias) for alias in self.aliases)) ): return self.submenu.complete_help( text, tokens[1], begidx - line.index(tokens[1]), endidx - line.index(tokens[1]), ) else: return original_complete_help(_self, text, line, begidx, endidx) if self.create_subclass: class _Cmd(cmd_obj): do_help = help_submenu complete_help = _complete_submenu_help else: _Cmd = cmd_obj _Cmd.do_help = help_submenu _Cmd.complete_help = _complete_submenu_help # Create bindings in the parent command to the submenus commands. setattr(_Cmd, 'do_' + self.command, enter_submenu) setattr(_Cmd, 'complete_' + self.command, complete_submenu) # Create additional bindings for aliases for _alias in self.aliases: setattr(_Cmd, 'do_' + _alias, enter_submenu) setattr(_Cmd, 'complete_' + _alias, complete_submenu) return _Cmd class Cmd(cmd.Cmd): """An easy but powerful framework for writing line-oriented command interpreters. Extends the Python Standard Library’s cmd package by adding a lot of useful features to the out of the box configuration. Line-oriented command interpreters are often useful for test harnesses, internal tools, and rapid prototypes. """ # Attributes used to configure the ParserManager (all are not dynamically settable at runtime) blankLinesAllowed = False commentGrammars = pyparsing.Or([pyparsing.pythonStyleComment, pyparsing.cStyleComment]) commentInProgress = pyparsing.Literal('/*') + pyparsing.SkipTo(pyparsing.stringEnd ^ '*/') legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit multilineCommands = [] prefixParser = pyparsing.Empty() redirector = '>' # for sending output to file shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'} aliases = dict() terminators = [';'] # make sure your terminators are not in legalChars! # Attributes which are NOT dynamically settable at runtime allow_cli_args = True # Should arguments passed on the command-line be processed as commands? allow_redirection = True # Should output redirection and pipes be allowed default_to_shell = False # Attempt to run unrecognized commands as shell commands quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt reserved_words = [] # Attributes which ARE dynamically settable at runtime colors = (platform.system() != 'Windows') continuation_prompt = '> ' debug = False echo = False editor = os.environ.get('EDITOR') if not editor: if sys.platform[:3] == 'win': editor = 'notepad' else: # Favor command-line editors first so we don't leave the terminal to edit for editor in ['vim', 'vi', 'emacs', 'nano', 'pico', 'gedit', 'kate', 'subl', 'geany', 'atom']: if _which(editor): break feedback_to_output = False # Do not include nonessentials in >, | output by default (things like timing) locals_in_py = True quiet = False # Do not suppress nonessential output timing = False # Prints elapsed time for each command # To make an attribute settable with the "do_set" command, add it to this ... # This starts out as a dictionary but gets converted to an OrderedDict sorted alphabetically by key settable = {'colors': 'Colorized output (*nix only)', 'continuation_prompt': 'On 2nd+ line of input', 'debug': 'Show full error stack on error', 'echo': 'Echo command issued into output', 'editor': 'Program used by ``edit``', 'feedback_to_output': 'Include nonessentials in `|`, `>` results', 'locals_in_py': 'Allow access to your application in py via self', 'prompt': 'The prompt issued to solicit input', 'quiet': "Don't print nonessential feedback", 'timing': 'Report execution times'} def __init__(self, completekey='tab', stdin=None, stdout=None, persistent_history_file='', persistent_history_length=1000, startup_script=None, use_ipython=False, transcript_files=None): """An easy but powerful framework for writing line-oriented command interpreters, extends Python's cmd package. :param completekey: str - (optional) readline name of a completion key, default to Tab :param stdin: (optional) alternate input file object, if not specified, sys.stdin is used :param stdout: (optional) alternate output file object, if not specified, sys.stdout is used :param persistent_history_file: str - (optional) file path to load a persistent readline history from :param persistent_history_length: int - (optional) max number of lines which will be written to the history file :param startup_script: str - (optional) file path to a a script to load and execute at startup :param use_ipython: (optional) should the "ipy" command be included for an embedded IPython shell :param transcript_files: str - (optional) allows running transcript tests when allow_cli_args is False """ # If use_ipython is False, make sure the do_ipy() method doesn't exit if not use_ipython: try: del Cmd.do_ipy except AttributeError: pass # If persistent readline history is enabled, then read history from file and register to write to file at exit if persistent_history_file: persistent_history_file = os.path.expanduser(persistent_history_file) try: readline.read_history_file(persistent_history_file) # default history len is -1 (infinite), which may grow unruly readline.set_history_length(persistent_history_length) except FILE_NOT_FOUND_ERROR: pass atexit.register(readline.write_history_file, persistent_history_file) # Call super class constructor. Need to do it in this way for Python 2 and 3 compatibility cmd.Cmd.__init__(self, completekey=completekey, stdin=stdin, stdout=stdout) # Commands to exclude from the help menu and tab completion self.hidden_commands = ['eof', 'eos', '_relative_load'] # Commands to exclude from the history command self.exclude_from_history = '''history edit eof eos'''.split() self._finalize_app_parameters() self.initial_stdout = sys.stdout self.history = History() self.pystate = {} self.keywords = self.reserved_words + [fname[3:] for fname in dir(self) if fname.startswith('do_')] self.parser_manager = ParserManager(redirector=self.redirector, terminators=self.terminators, multilineCommands=self.multilineCommands, legalChars=self.legalChars, commentGrammars=self.commentGrammars, commentInProgress=self.commentInProgress, blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser, preparse=self.preparse, postparse=self.postparse, aliases=self.aliases, shortcuts=self.shortcuts) self._transcript_files = transcript_files # Used to enable the ability for a Python script to quit the application self._should_quit = False # True if running inside a Python script or interactive console, False otherwise self._in_py = False # 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. self._last_result = None # Used to save state during a redirection self.kept_state = None self.kept_sys = None # Codes used for exit conditions self._STOP_AND_EXIT = True # cmd convention self._colorcodes = {'bold': {True: '\x1b[1m', False: '\x1b[22m'}, 'cyan': {True: '\x1b[36m', False: '\x1b[39m'}, 'blue': {True: '\x1b[34m', False: '\x1b[39m'}, 'red': {True: '\x1b[31m', False: '\x1b[39m'}, 'magenta': {True: '\x1b[35m', False: '\x1b[39m'}, 'green': {True: '\x1b[32m', False: '\x1b[39m'}, 'underline': {True: '\x1b[4m', False: '\x1b[24m'}, 'yellow': {True: '\x1b[33m', False: '\x1b[39m'}} # Used load command to store the current script dir as a LIFO queue to support _relative_load command self._script_dir = [] # Used when piping command output to a shell command self.pipe_proc = None # Used by complete() for readline tab completion self.completion_matches = [] # Used to keep track of whether we are redirecting or piping output self.redirecting = False # If this string is non-empty, then this warning message will print if a broken pipe error occurs while printing self.broken_pipe_warning = '' # If a startup script is provided, then add it in the queue to load if startup_script is not None: startup_script = os.path.expanduser(startup_script) if os.path.exists(startup_script) and os.path.getsize(startup_script) > 0: self.cmdqueue.append('load {}'.format(startup_script)) ############################################################################################################ # The following variables are used by tab-completion functions. They are reset each time complete() is run # using set_completion_defaults() and it is up to completer functions to set them before returning results. ############################################################################################################ # If true and a single match is returned to complete(), then a space will be appended # if the match appears at the end of the line self.allow_appended_space = True # If true and a single match is returned to complete(), then a closing quote # will be added if there is an unmatched opening quote self.allow_closing_quote = True # Use this list if you are completing strings that contain a common delimiter and you only want to # display the final portion of the matches as the tab-completion suggestions. The full matches # still must be returned from your completer function. For an example, look at path_complete() # which uses this to show only the basename of paths as the suggestions. delimiter_complete() also # populates this list. self.display_matches = [] # ----- Methods related to presenting output to the user ----- @property def visible_prompt(self): """Read-only property to get the visible prompt with any ANSI escape codes stripped. Used by transcript testing to make it easier and more reliable when users are doing things like coloring the prompt using ANSI color codes. :return: str - prompt stripped of any ANSI escape codes """ return strip_ansi(self.prompt) def _finalize_app_parameters(self): self.commentGrammars.ignore(pyparsing.quotedString).setParseAction(lambda x: '') # noinspection PyUnresolvedReferences self.shortcuts = sorted(self.shortcuts.items(), reverse=True) # Make sure settable parameters are sorted alphabetically by key self.settable = collections.OrderedDict(sorted(self.settable.items(), key=lambda t: t[0])) def poutput(self, msg, end='\n'): """Convenient shortcut for self.stdout.write(); by default adds newline to end if not already present. Also handles BrokenPipeError exceptions for when a commands's output has been piped to another process and that process terminates before the cmd2 command is finished executing. :param msg: str - message to print to current stdout - anything convertible to a str with '{}'.format() is OK :param end: str - string appended after the end of the message if not already present, default a newline """ if msg is not None and msg != '': try: msg_str = '{}'.format(msg) self.stdout.write(msg_str) if not msg_str.endswith(end): self.stdout.write(end) except BROKEN_PIPE_ERROR: # This occurs if a command's output is being piped to another process and that process closes before the # command is finished. If you would like your application to print a warning message, then set the # broken_pipe_warning attribute to the message you want printed. if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) def perror(self, errmsg, exception_type=None, traceback_war=True): """ Print error message to sys.stderr and if debug is true, print an exception Traceback if one exists. :param errmsg: str - error message to print out :param exception_type: str - (optional) type of exception which precipitated this error message :param traceback_war: bool - (optional) if True, print a message to let user know they can enable debug :return: """ if self.debug: traceback.print_exc() if exception_type is None: err = self.colorize("ERROR: {}\n".format(errmsg), 'red') sys.stderr.write(err) else: err = "EXCEPTION of type '{}' occurred with message: '{}'\n".format(exception_type, errmsg) sys.stderr.write(self.colorize(err, 'red')) if traceback_war: war = "To enable full traceback, run the following command: 'set debug true'\n" sys.stderr.write(self.colorize(war, 'yellow')) def pfeedback(self, msg): """For printing nonessential feedback. Can be silenced with `quiet`. Inclusion in redirected output is controlled by `feedback_to_output`.""" if not self.quiet: if self.feedback_to_output: self.poutput(msg) else: sys.stderr.write("{}\n".format(msg)) def ppaged(self, msg, end='\n'): """Print output using a pager if it would go off screen and stdout isn't currently being redirected. Never uses a pager inside of a script (Python or text) or when output is being redirected or piped or when stdout or stdin are not a fully functional terminal. :param msg: str - message to print to current stdout - anything convertible to a str with '{}'.format() is OK :param end: str - string appended after the end of the message if not already present, default a newline """ if msg is not None and msg != '': try: msg_str = '{}'.format(msg) if not msg_str.endswith(end): msg_str += end # Attempt to detect if we are not running within a fully functional terminal. # Don't try to use the pager when being run by a continuous integration system like Jenkins + pexpect. functional_terminal = False if self.stdin.isatty() and self.stdout.isatty(): if sys.platform.startswith('win') or os.environ.get('TERM') is not None: functional_terminal = True # Don't attempt to use a pager that can block if redirecting or running a script (either text or Python) # Also only attempt to use a pager if actually running in a real fully functional terminal if functional_terminal and not self.redirecting and not self._in_py and not self._script_dir: if sys.platform.startswith('win'): pager_cmd = 'more' else: # Here is the meaning of the various flags we are using with the less command: # -S causes lines longer than the screen width to be chopped (truncated) rather than wrapped # -R causes ANSI "color" escape sequences to be output in raw form (i.e. colors are displayed) # -X disables sending the termcap initialization and deinitialization strings to the terminal # -F causes less to automatically exit if the entire file can be displayed on the first screen pager_cmd = 'less -SRXF' self.pipe_proc = subprocess.Popen(pager_cmd, shell=True, stdin=subprocess.PIPE) try: self.pipe_proc.stdin.write(msg_str.encode('utf-8', 'replace')) self.pipe_proc.stdin.close() except (IOError, KeyboardInterrupt): pass # Less doesn't respect ^C, but catches it for its own UI purposes (aborting search etc. inside less) while True: try: self.pipe_proc.wait() except KeyboardInterrupt: pass else: break self.pipe_proc = None else: self.stdout.write(msg_str) except BROKEN_PIPE_ERROR: # This occurs if a command's output is being piped to another process and that process closes before the # command is finished. If you would like your application to print a warning message, then set the # broken_pipe_warning attribute to the message you want printed. if self.broken_pipe_warning: sys.stderr.write(self.broken_pipe_warning) def colorize(self, val, color): """Given a string (``val``), returns that string wrapped in UNIX-style special characters that turn on (and then off) text color and style. If the ``colors`` environment parameter is ``False``, or the application is running on Windows, will return ``val`` unchanged. ``color`` should be one of the supported strings (or styles): red/blue/green/cyan/magenta, bold, underline""" if self.colors and (self.stdout == self.initial_stdout): return self._colorcodes[color][True] + val + self._colorcodes[color][False] return val def get_subcommands(self, command): """ Returns a list of a command's subcommand names if they exist :param command: the command we are querying :return: A subcommand list or None """ subcommand_names = None # Check if is a valid command funcname = self._func_named(command) if funcname: # Check to see if this function was decorated with an argparse ArgumentParser func = getattr(self, funcname) subcommands = func.__dict__.get('subcommands', None) if subcommands is not None: subcommand_names = subcommands.keys() return subcommand_names def get_subcommand_completer(self, command, subcommand): """ Returns a subcommand's tab completion function if one exists :param command: command which owns the subcommand :param subcommand: the subcommand we are querying :return: A completer or None """ completer = None # Check if is a valid command funcname = self._func_named(command) if funcname: # Check to see if this function was decorated with an argparse ArgumentParser func = getattr(self, funcname) subcommands = func.__dict__.get('subcommands', None) if subcommands is not None: completer = subcommands[subcommand] return completer # ----- Methods related to tab completion ----- def set_completion_defaults(self): """ Resets tab completion settings Needs to be called each time readline runs tab completion """ self.allow_appended_space = True self.allow_closing_quote = True self.display_matches = [] def tokens_for_completion(self, line, begidx, endidx): """ Used by tab completion functions to get all tokens through the one being completed :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :return: A 2 item tuple where the items are On Success tokens: list of unquoted tokens this is generally the list needed for tab completion functions raw_tokens: list of tokens with any quotes preserved this can be used to know if a token was quoted or is missing a closing quote Both lists are guaranteed to have at least 1 item The last item in both lists is the token being tab completed On Failure Both items are None """ unclosed_quote = '' quotes_to_try = copy.copy(QUOTES) tmp_line = line[:endidx] tmp_endidx = endidx # Parse the line into tokens while True: try: # Use non-POSIX parsing to keep the quotes around the tokens initial_tokens = shlex.split(tmp_line[:tmp_endidx], posix=False) # If the cursor is at an empty token outside of a quoted string, # then that is the token being completed. Add it to the list. if not unclosed_quote and begidx == tmp_endidx: initial_tokens.append('') break except ValueError: # ValueError can be caused by missing closing quote if not quotes_to_try: # Since we have no more quotes to try, something else # is causing the parsing error. Return None since # this means the line is malformed. return None, None # Add a closing quote and try to parse again unclosed_quote = quotes_to_try[0] quotes_to_try = quotes_to_try[1:] tmp_line = line[:endidx] tmp_line += unclosed_quote tmp_endidx = endidx + 1 if self.allow_redirection: # Since redirection is enabled, we need to treat redirection characters (|, <, >) # as word breaks when they are in unquoted strings. Go through each token # and further split them on these characters. Each run of redirect characters # is treated as a single token. raw_tokens = [] for cur_initial_token in initial_tokens: # Save tokens up to 1 character in length or quoted tokens. No need to parse these. if len(cur_initial_token) <= 1 or cur_initial_token[0] in QUOTES: raw_tokens.append(cur_initial_token) continue # Iterate over each character in this token cur_index = 0 cur_char = cur_initial_token[cur_index] # Keep track of the token we are building cur_raw_token = '' while True: if cur_char not in REDIRECTION_CHARS: # Keep appending to cur_raw_token until we hit a redirect char while cur_char not in REDIRECTION_CHARS: cur_raw_token += cur_char cur_index += 1 if cur_index < len(cur_initial_token): cur_char = cur_initial_token[cur_index] else: break else: redirect_char = cur_char # Keep appending to cur_raw_token until we hit something other than redirect_char while cur_char == redirect_char: cur_raw_token += cur_char cur_index += 1 if cur_index < len(cur_initial_token): cur_char = cur_initial_token[cur_index] else: break # Save the current token raw_tokens.append(cur_raw_token) cur_raw_token = '' # Check if we've viewed all characters if cur_index >= len(cur_initial_token): break else: raw_tokens = initial_tokens # Save the unquoted tokens tokens = [strip_quotes(cur_token) for cur_token in raw_tokens] # If the token being completed had an unclosed quote, we need # to remove the closing quote that was added in order for it # to match what was on the command line. if unclosed_quote: raw_tokens[-1] = raw_tokens[-1][:-1] return tokens, raw_tokens # noinspection PyUnusedLocal @staticmethod def basic_complete(text, line, begidx, endidx, match_against): """ Performs tab completion against a list :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param match_against: Collection - the list being matched against :return: List[str] - a list of possible tab completions """ return [cur_match for cur_match in match_against if cur_match.startswith(text)] def delimiter_complete(self, text, line, begidx, endidx, match_against, delimiter): """ Performs tab completion against a list but each match is split on a delimiter and only the portion of the match being tab completed is shown as the completion suggestions. This is useful if you match against strings that are hierarchical in nature and have a common delimiter. An easy way to illustrate this concept is path completion since paths are just directories/files delimited by a slash. If you are tab completing items in /home/user you don't get the following as suggestions: /home/user/file.txt /home/user/program.c /home/user/maps/ /home/user/cmd2.py Instead you are shown: file.txt program.c maps/ cmd2.py For a large set of data, this can be visually more pleasing and easier to search. Another example would be strings formatted with the following syntax: company::department::name In this case the delimiter would be :: and the user could easily narrow down what they are looking for if they were only shown suggestions in the category they are at in the string. :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param match_against: Collection - the list being matched against :param delimiter: str - what delimits each portion of the matches (ex: paths are delimited by a slash) :return: List[str] - a list of possible tab completions """ matches = self.basic_complete(text, line, begidx, endidx, match_against) # Display only the portion of the match that's being completed based on delimiter if matches: # Get the common beginning for the matches common_prefix = os.path.commonprefix(matches) prefix_tokens = common_prefix.split(delimiter) # Calculate what portion of the match we are completing display_token_index = 0 if prefix_tokens: display_token_index = len(prefix_tokens) - 1 # Get this portion for each match and store them in self.display_matches for cur_match in matches: match_tokens = cur_match.split(delimiter) display_token = match_tokens[display_token_index] if not display_token: display_token = delimiter self.display_matches.append(display_token) return matches def flag_based_complete(self, text, line, begidx, endidx, flag_dict, all_else=None): """ Tab completes based on a particular flag preceding the token being completed :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param flag_dict: dict - dictionary whose structure is the following: keys - flags (ex: -c, --create) that result in tab completion for the next argument in the command line values - there are two types of values 1. iterable list of strings to match against (dictionaries, lists, etc.) 2. function that performs tab completion (ex: path_complete) :param all_else: Collection or function - an optional parameter for tab completing any token that isn't preceded by a flag in flag_dict :return: List[str] - a list of possible tab completions """ # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) if tokens is None: return [] completions_matches = [] match_against = all_else # Must have at least 2 args for a flag to precede the token being completed if len(tokens) > 1: flag = tokens[-2] if flag in flag_dict: match_against = flag_dict[flag] # Perform tab completion using a Collection if isinstance(match_against, Collection): completions_matches = self.basic_complete(text, line, begidx, endidx, match_against) # Perform tab completion using a function elif callable(match_against): completions_matches = match_against(text, line, begidx, endidx) return completions_matches def index_based_complete(self, text, line, begidx, endidx, index_dict, all_else=None): """ Tab completes based on a fixed position in the input string :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param index_dict: dict - dictionary whose structure is the following: keys - 0-based token indexes into command line that determine which tokens perform tab completion values - there are two types of values 1. iterable list of strings to match against (dictionaries, lists, etc.) 2. function that performs tab completion (ex: path_complete) :param all_else: Collection or function - an optional parameter for tab completing any token that isn't at an index in index_dict :return: List[str] - a list of possible tab completions """ # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) if tokens is None: return [] matches = [] # Get the index of the token being completed index = len(tokens) - 1 # Check if token is at an index in the dictionary if index in index_dict: match_against = index_dict[index] else: match_against = all_else # Perform tab completion using a Collection if isinstance(match_against, Collection): matches = self.basic_complete(text, line, begidx, endidx, match_against) # Perform tab completion using a function elif callable(match_against): matches = match_against(text, line, begidx, endidx) return matches # noinspection PyUnusedLocal def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only=False): """Performs completion of local file system paths :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param dir_exe_only: bool - only return directories and executables, not non-executable files :param dir_only: bool - only return directories :return: List[str] - a list of possible tab completions """ # Used to complete ~ and ~user strings def complete_users(): # We are returning ~user strings that resolve to directories, # so don't append a space or quote in the case of a single result. self.allow_appended_space = False self.allow_closing_quote = False users = [] # Windows lacks the pwd module so we can't get a list of users. # Instead we will add a slash once the user enters text that # resolves to an existing home directory. if sys.platform.startswith('win'): expanded_path = os.path.expanduser(text) if os.path.isdir(expanded_path): users.append(text + os.path.sep) else: import pwd # Iterate through a list of users from the password database for cur_pw in pwd.getpwall(): # Check if the user has an existing home dir if os.path.isdir(cur_pw.pw_dir): # Add a ~ to the user to match against text cur_user = '~' + cur_pw.pw_name if cur_user.startswith(text): if add_trailing_sep_if_dir: cur_user += os.path.sep users.append(cur_user) return users # Determine if a trailing separator should be appended to directory completions add_trailing_sep_if_dir = False if endidx == len(line) or (endidx < len(line) and line[endidx] != os.path.sep): add_trailing_sep_if_dir = True # Used to replace cwd in the final results cwd = os.getcwd() cwd_added = False # Used to replace expanded user path in final result orig_tilde_path = '' expanded_tilde_path = '' # If the search text is blank, then search in the CWD for * if not text: search_str = os.path.join(os.getcwd(), '*') cwd_added = True else: # Purposely don't match any path containing wildcards - what we are doing is complicated enough! wildcards = ['*', '?'] for wildcard in wildcards: if wildcard in text: return [] # Start the search string search_str = text + '*' # Handle tilde expansion and completion if text.startswith('~'): sep_index = text.find(os.path.sep, 1) # If there is no slash, then the user is still completing the user after the tilde if sep_index == -1: return complete_users() # Otherwise expand the user dir else: search_str = os.path.expanduser(search_str) # Get what we need to restore the original tilde path later orig_tilde_path = text[:sep_index] expanded_tilde_path = os.path.expanduser(orig_tilde_path) # If the search text does not have a directory, then use the cwd elif not os.path.dirname(text): search_str = os.path.join(os.getcwd(), search_str) cwd_added = True # Find all matching path completions matches = glob.glob(search_str) # Filter based on type if dir_exe_only: matches = [c for c in matches if os.path.isdir(c) or os.access(c, os.X_OK)] elif dir_only: matches = [c for c in matches if os.path.isdir(c)] # Don't append a space or closing quote to directory if len(matches) == 1 and os.path.isdir(matches[0]): self.allow_appended_space = False self.allow_closing_quote = False # Build display_matches and add a slash to directories for index, cur_match in enumerate(matches): # Display only the basename of this path in the tab-completion suggestions self.display_matches.append(os.path.basename(cur_match)) # Add a separator after directories if the next character isn't already a separator if os.path.isdir(cur_match) and add_trailing_sep_if_dir: matches[index] += os.path.sep self.display_matches[index] += os.path.sep # Remove cwd if it was added to match the text readline expects if cwd_added: matches = [cur_path.replace(cwd + os.path.sep, '', 1) for cur_path in matches] # Restore the tilde string if we expanded one to match the text readline expects if expanded_tilde_path: matches = [cur_path.replace(expanded_tilde_path, orig_tilde_path, 1) for cur_path in matches] return matches @staticmethod def get_exes_in_path(starts_with): """ Returns names of executables in a user's path :param starts_with: str - what the exes should start with. leave blank for all exes in path. :return: List[str] - a list of matching exe names """ # Purposely don't match any executable containing wildcards wildcards = ['*', '?'] for wildcard in wildcards: if wildcard in starts_with: return [] # Get a list of every directory in the PATH environment variable and ignore symbolic links paths = [p for p in os.getenv('PATH').split(os.path.pathsep) if not os.path.islink(p)] # Use a set to store exe names since there can be duplicates exes_set = set() # Find every executable file in the user's path that matches the pattern for path in paths: full_path = os.path.join(path, starts_with) matches = [f for f in glob.glob(full_path + '*') if os.path.isfile(f) and os.access(f, os.X_OK)] for match in matches: exes_set.add(os.path.basename(match)) return list(exes_set) def shell_cmd_complete(self, text, line, begidx, endidx, complete_blank=False): """Performs completion of executables either in a user's path or a given path :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param complete_blank: bool - If True, then a blank will complete all shell commands in a user's path If False, then no completion is performed Defaults to False to match Bash shell behavior :return: List[str] - a list of possible tab completions """ # Don't tab complete anything if no shell command has been started if not complete_blank and not text: return [] # If there are no path characters in the search text, then do shell command completion in the user's path if not text.startswith('~') and os.path.sep not in text: return self.get_exes_in_path(text) # Otherwise look for executables in the given path else: return self.path_complete(text, line, begidx, endidx, dir_exe_only=True) def _redirect_complete(self, text, line, begidx, endidx, compfunc): """ Called by complete() as the first tab completion function for all commands It determines if it should tab complete for redirection (|, <, >, >>) or use the completer function for the current command :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :param compfunc: Callable - the completer function for the current command this will be called if we aren't completing for redirection :return: List[str] - a list of possible tab completions """ if self.allow_redirection: # Get all tokens through the one being completed. We want the raw tokens # so we can tell if redirection strings are quoted and ignore them. _, raw_tokens = self.tokens_for_completion(line, begidx, endidx) if raw_tokens is None: return [] if len(raw_tokens) > 1: # Build a list of all redirection tokens all_redirects = REDIRECTION_CHARS + ['>>'] # Check if there are redirection strings prior to the token being completed seen_pipe = False has_redirection = False for cur_token in raw_tokens[:-1]: if cur_token in all_redirects: has_redirection = True if cur_token == '|': seen_pipe = True # Get token prior to the one being completed prior_token = raw_tokens[-2] # If a pipe is right before the token being completed, complete a shell command as the piped process if prior_token == '|': return self.shell_cmd_complete(text, line, begidx, endidx) # Otherwise do path completion either as files to redirectors or arguments to the piped process elif prior_token in all_redirects or seen_pipe: return self.path_complete(text, line, begidx, endidx) # If there were redirection strings anywhere on the command line, then we # are no longer tab completing for the current command elif has_redirection: return [] # Call the command's completer function return compfunc(text, line, begidx, endidx) @staticmethod def _pad_matches_to_display(matches_to_display): """ Adds padding to the matches being displayed as tab completion suggestions. The default padding of readline/pyreadine is small and not visually appealing especially if matches have spaces. It appears very squished together. :param matches_to_display: the matches being padded :return: the padded matches and length of padding that was added """ if rl_type == RlType.GNU: # Add 2 to the padding of 2 that readline uses for a total of 4. padding = 2 * ' ' elif rl_type == RlType.PYREADLINE: # Add 3 to the padding of 1 that pyreadline uses for a total of 4. padding = 3 * ' ' else: return matches_to_display, 0 return [cur_match + padding for cur_match in matches_to_display], len(padding) def _display_matches_gnu_readline(self, substitution, matches, longest_match_length): """ Prints a match list using GNU readline's rl_display_match_list() This exists to print self.display_matches if it has data. Otherwise matches prints. :param substitution: str - the substitution written to the command line :param matches: list[str] - the tab completion matches to display :param longest_match_length: int - longest printed length of the matches """ if rl_type == RlType.GNU: # Check if we should show display_matches if self.display_matches: matches_to_display = self.display_matches # Recalculate longest_match_length for display_matches longest_match_length = 0 for cur_match in matches_to_display: cur_length = wcswidth(cur_match) if cur_length > longest_match_length: longest_match_length = cur_length else: matches_to_display = matches # Add padding for visual appeal matches_to_display, padding_length = self._pad_matches_to_display(matches_to_display) longest_match_length += padding_length # We will use readline's display function (rl_display_match_list()), so we # need to encode our string as bytes to place in a C array. if six.PY3: encoded_substitution = bytes(substitution, encoding='utf-8') encoded_matches = [bytes(cur_match, encoding='utf-8') for cur_match in matches_to_display] else: encoded_substitution = bytes(substitution) encoded_matches = [bytes(cur_match) for cur_match in matches_to_display] # rl_display_match_list() expects matches to be in argv format where # substitution is the first element, followed by the matches, and then a NULL. # noinspection PyCallingNonCallable,PyTypeChecker strings_array = (ctypes.c_char_p * (1 + len(encoded_matches) + 1))() # Copy in the encoded strings and add a NULL to the end strings_array[0] = encoded_substitution strings_array[1:-1] = encoded_matches strings_array[-1] = None # Call readline's display function # rl_display_match_list(strings_array, number of completion matches, longest match length) readline_lib.rl_display_match_list(strings_array, len(encoded_matches), longest_match_length) # rl_forced_update_display() is the proper way to redraw the prompt and line, but we # have to use ctypes to do it since Python's readline API does not wrap the function readline_lib.rl_forced_update_display() # Since we updated the display, readline asks that rl_display_fixed be set for efficiency display_fixed = ctypes.c_int.in_dll(readline_lib, "rl_display_fixed") display_fixed.value = 1 def _display_matches_pyreadline(self, matches): """ Prints a match list using pyreadline's _display_completions() This exists to print self.display_matches if it has data. Otherwise matches prints. :param matches: list[str] - the tab completion matches to display """ if rl_type == RlType.PYREADLINE: # Check if we should show display_matches if self.display_matches: matches_to_display = self.display_matches else: matches_to_display = matches # Add padding for visual appeal matches_to_display, _ = self._pad_matches_to_display(matches_to_display) # Display the matches orig_pyreadline_display(matches_to_display) # ----- Methods which override stuff in cmd ----- def complete(self, text, state): """Override of command method which returns the next possible completion for 'text'. If a command has not been entered, then complete against command list. Otherwise try to call complete_ to get list of completions. This method gets called directly by readline because it is set as the tab-completion function. This completer function is called as complete(text, state), for state in 0, 1, 2, …, until it returns a non-string value. It should return the next possible completion starting with text. :param text: str - the current word that user is typing :param state: int - non-negative integer """ if state == 0: unclosed_quote = '' self.set_completion_defaults() # lstrip the original line orig_line = readline.get_line_buffer() line = orig_line.lstrip() stripped = len(orig_line) - len(line) # Calculate new indexes for the stripped line. If the cursor is at a position before the end of a # line of spaces, then the following math could result in negative indexes. Enforce a max of 0. begidx = max(readline.get_begidx() - stripped, 0) endidx = max(readline.get_endidx() - stripped, 0) # Shortcuts are not word break characters when tab completing. Therefore shortcuts become part # of the text variable if there isn't a word break, like a space, after it. We need to remove it # from text and update the indexes. This only applies if we are at the the beginning of the line. shortcut_to_restore = '' if begidx == 0: for (shortcut, expansion) in self.shortcuts: if text.startswith(shortcut): # Save the shortcut to restore later shortcut_to_restore = shortcut # Adjust text and where it begins text = text[len(shortcut_to_restore):] begidx += len(shortcut_to_restore) break # If begidx is greater than 0, then we are no longer completing the command if begidx > 0: # Parse the command line command, args, expanded_line = self.parseline(line) # We overwrote line with a properly formatted but fully stripped version # Restore the end spaces since line is only supposed to be lstripped when # passed to completer functions according to Python docs rstripped_len = len(line) - len(line.rstrip()) expanded_line += ' ' * rstripped_len # Fix the index values if expanded_line has a different size than line if len(expanded_line) != len(line): diff = len(expanded_line) - len(line) begidx += diff endidx += diff # Overwrite line to pass into completers line = expanded_line # Get all tokens through the one being completed tokens, raw_tokens = self.tokens_for_completion(line, begidx, endidx) # Either had a parsing error or are trying to complete the command token # The latter can happen if default_to_shell is True and parseline() allowed # assumed something like " or ' was a command. if tokens is None or len(tokens) == 1: self.completion_matches = [] return None # Text we need to remove from completions later text_to_remove = '' # Get the token being completed with any opening quote preserved raw_completion_token = raw_tokens[-1] # Check if the token being completed has an opening quote if raw_completion_token and raw_completion_token[0] in QUOTES: # Since the token is still being completed, we know the opening quote is unclosed unclosed_quote = raw_completion_token[0] # readline still performs word breaks after a quote. Therefore something like quoted search # text with a space would have resulted in begidx pointing to the middle of the token we # we want to complete. Figure out where that token actually begins and save the beginning # portion of it that was not part of the text readline gave us. We will remove it from the # completions later since readline expects them to start with the original text. actual_begidx = line[:endidx].rfind(tokens[-1]) if actual_begidx != begidx: text_to_remove = line[actual_begidx:begidx] # Adjust text and where it begins so the completer routines # get unbroken search text to complete on. text = text_to_remove + text begidx = actual_begidx # Check if a valid command was entered if command in self.get_all_commands(): # Get the completer function for this command try: compfunc = getattr(self, 'complete_' + command) except AttributeError: compfunc = self.completedefault subcommands = self.get_subcommands(command) if subcommands is not None: # Since there are subcommands, then try completing those if the cursor is in # the token at index 1, otherwise default to using compfunc index_dict = {1: subcommands} compfunc = functools.partial(self.index_based_complete, index_dict=index_dict, all_else=compfunc) # A valid command was not entered else: # Check if this command should be run as a shell command if self.default_to_shell and command in self.get_exes_in_path(command): compfunc = self.path_complete else: compfunc = self.completedefault # Attempt tab completion for redirection first, and if that isn't occurring, # call the completer function for the current command self.completion_matches = self._redirect_complete(text, line, begidx, endidx, compfunc) if self.completion_matches: # Eliminate duplicates matches_set = set(self.completion_matches) self.completion_matches = list(matches_set) display_matches_set = set(self.display_matches) self.display_matches = list(display_matches_set) # Check if display_matches has been used. If so, then matches # on delimited strings like paths was done. if self.display_matches: matches_delimited = True else: matches_delimited = False # Since self.display_matches is empty, set it to self.completion_matches # before we alter them. That way the suggestions will reflect how we parsed # the token being completed and not how readline did. self.display_matches = copy.copy(self.completion_matches) # Check if we need to add an opening quote if not unclosed_quote: add_quote = False # This is the tab completion text that will appear on the command line. common_prefix = os.path.commonprefix(self.completion_matches) if matches_delimited: # Check if any portion of the display matches appears in the tab completion display_prefix = os.path.commonprefix(self.display_matches) # For delimited matches, we check what appears before the display # matches (common_prefix) as well as the display matches themselves. if (' ' in common_prefix) or (display_prefix and ' ' in ''.join(self.display_matches)): add_quote = True # If there is a tab completion and any match has a space, then add an opening quote elif common_prefix and ' ' in ''.join(self.completion_matches): add_quote = True if add_quote: # Figure out what kind of quote to add and save it as the unclosed_quote if '"' in ''.join(self.completion_matches): unclosed_quote = "'" else: unclosed_quote = '"' self.completion_matches = [unclosed_quote + match for match in self.completion_matches] # Check if we need to remove text from the beginning of tab completions elif text_to_remove: self.completion_matches = \ [m.replace(text_to_remove, '', 1) for m in self.completion_matches] # Check if we need to restore a shortcut in the tab completions # so it doesn't get erased from the command line if shortcut_to_restore: self.completion_matches = \ [shortcut_to_restore + match for match in self.completion_matches] else: # Complete token against aliases and command names alias_names = set(self.aliases.keys()) visible_commands = set(self.get_visible_commands()) strs_to_match = list(alias_names | visible_commands) self.completion_matches = self.basic_complete(text, line, begidx, endidx, strs_to_match) # Handle single result if len(self.completion_matches) == 1: str_to_append = '' # Add a closing quote if needed and allowed if self.allow_closing_quote and unclosed_quote: str_to_append += unclosed_quote # If we are at the end of the line, then add a space if allowed if self.allow_appended_space and endidx == len(line): str_to_append += ' ' self.completion_matches[0] += str_to_append # Otherwise sort matches elif self.completion_matches: self.completion_matches.sort() self.display_matches.sort() try: return self.completion_matches[state] except IndexError: return None def get_all_commands(self): """ Returns a list of all commands """ return [cur_name[3:] for cur_name in self.get_names() if cur_name.startswith('do_')] def get_visible_commands(self): """ Returns a list of commands that have not been hidden """ commands = self.get_all_commands() # Remove the hidden commands for name in self.hidden_commands: if name in commands: commands.remove(name) return commands def get_help_topics(self): """ Returns a list of help topics """ return [name[5:] for name in self.get_names() if name.startswith('help_')] def complete_help(self, text, line, begidx, endidx): """ Override of parent class method to handle tab completing subcommands and not showing hidden commands Returns a list of possible tab completions """ # The command is the token at index 1 in the command line cmd_index = 1 # The subcommand is the token at index 2 in the command line subcmd_index = 2 # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) if tokens is None: return [] matches = [] # Get the index of the token being completed index = len(tokens) - 1 # Check if we are completing a command or help topic if index == cmd_index: # Complete token against topics and visible commands topics = set(self.get_help_topics()) visible_commands = set(self.get_visible_commands()) strs_to_match = list(topics | visible_commands) matches = self.basic_complete(text, line, begidx, endidx, strs_to_match) # Check if we are completing a subcommand elif index == subcmd_index: # Match subcommands if any exist command = tokens[cmd_index] matches = self.basic_complete(text, line, begidx, endidx, self.get_subcommands(command)) return matches # noinspection PyUnusedLocal def sigint_handler(self, signum, frame): """Signal handler for SIGINTs which typically come from Ctrl-C events. If you need custom SIGINT behavior, then override this function. :param signum: int - signal number :param frame """ # Save copy of pipe_proc since it could theoretically change while this is running pipe_proc = self.pipe_proc if pipe_proc is not None: pipe_proc.terminate() # Re-raise a KeyboardInterrupt so other parts of the code can catch it raise KeyboardInterrupt("Got a keyboard interrupt") def preloop(self): """"Hook method executed once when the cmdloop() method is called.""" # Register a default SIGINT signal handler for Ctrl+C signal.signal(signal.SIGINT, self.sigint_handler) def precmd(self, statement): """Hook method executed just before the command is processed by ``onecmd()`` and after adding it to the history. :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance :return: ParsedString - a potentially modified version of the input ParsedString statement """ return statement # ----- Methods which are cmd2-specific lifecycle hooks which are not present in cmd ----- # noinspection PyMethodMayBeStatic def preparse(self, raw): """Hook method executed just before the command line is interpreted, but after the input prompt is generated. :param raw: str - raw command line input :return: str - potentially modified raw command line input """ return raw # noinspection PyMethodMayBeStatic def postparse(self, parse_result): """Hook that runs immediately after parsing the command-line but before ``parsed()`` returns a ParsedString. :param parse_result: pyparsing.ParseResults - parsing results output by the pyparsing parser :return: pyparsing.ParseResults - potentially modified ParseResults object """ return parse_result # noinspection PyMethodMayBeStatic def postparsing_precmd(self, statement): """This runs after parsing the command-line, but before anything else; even before adding cmd to history. NOTE: This runs before precmd() and prior to any potential output redirection or piping. If you wish to fatally fail this command and exit the application entirely, set stop = True. If you wish to just fail this command you can do so by raising an exception: - raise EmptyStatement - will silently fail and do nothing - raise - will fail and print an error message :param statement: - the parsed command-line statement :return: (bool, statement) - (stop, statement) containing a potentially modified version of the statement """ stop = False return stop, statement # noinspection PyMethodMayBeStatic def postparsing_postcmd(self, stop): """This runs after everything else, including after postcmd(). It even runs when an empty line is entered. Thus, if you need to do something like update the prompt due to notifications from a background thread, then this is the method you want to override to do it. :param stop: bool - True implies the entire application should exit. :return: bool - True implies the entire application should exit. """ if not sys.platform.startswith('win'): # Fix those annoying problems that occur with terminal programs like "less" when you pipe to them if self.stdin.isatty(): proc = subprocess.Popen(shlex.split('stty sane')) proc.communicate() return stop def parseline(self, line): """Parse the line into a command name and a string containing the arguments. NOTE: This is an override of a parent class method. It is only used by other parent class methods. But we do need to override it here so that the additional shortcuts present in cmd2 get properly expanded for purposes of tab completion. Used for command tab completion. Returns a tuple containing (command, args, line). 'command' and 'args' may be None if the line couldn't be parsed. :param line: str - line read by readline :return: (str, str, str) - tuple containing (command, args, line) """ line = line.strip() if not line: # Deal with empty line or all whitespace line return None, None, line # Make a copy of aliases so we can edit it tmp_aliases = list(self.aliases.keys()) keep_expanding = len(tmp_aliases) > 0 # Expand aliases while keep_expanding: for cur_alias in tmp_aliases: keep_expanding = False if line == cur_alias or line.startswith(cur_alias + ' '): line = line.replace(cur_alias, self.aliases[cur_alias], 1) # Do not expand the same alias more than once tmp_aliases.remove(cur_alias) keep_expanding = len(tmp_aliases) > 0 break # Expand command shortcut to its full command name for (shortcut, expansion) in self.shortcuts: if line.startswith(shortcut): # If the next character after the shortcut isn't a space, then insert one shortcut_len = len(shortcut) if len(line) == shortcut_len or line[shortcut_len] != ' ': expansion += ' ' # Expand the shortcut line = line.replace(shortcut, expansion, 1) break i, n = 0, len(line) # If we are allowing shell commands, then allow any character in the command if self.default_to_shell: while i < n and line[i] != ' ': i += 1 # Otherwise only allow those in identchars else: while i < n and line[i] in self.identchars: i += 1 command, arg = line[:i], line[i:].strip() return command, arg, line def onecmd_plus_hooks(self, line): """Top-level function called by cmdloop() to handle parsing a line and running the command and all of its hooks. :param line: str - line of text read from input :return: bool - True if cmdloop() should exit, False otherwise """ stop = 0 try: statement = self._complete_statement(line) (stop, statement) = self.postparsing_precmd(statement) if stop: return self.postparsing_postcmd(stop) try: if self.allow_redirection: self._redirect_output(statement) timestart = datetime.datetime.now() statement = self.precmd(statement) stop = self.onecmd(statement) stop = self.postcmd(stop, statement) if self.timing: self.pfeedback('Elapsed: %s' % str(datetime.datetime.now() - timestart)) finally: if self.allow_redirection: self._restore_output(statement) except EmptyStatement: pass except ValueError as ex: # If shlex.split failed on syntax, let user know whats going on self.perror("Invalid syntax: {}".format(ex), traceback_war=False) except Exception as ex: self.perror(ex, type(ex).__name__) finally: return self.postparsing_postcmd(stop) def runcmds_plus_hooks(self, cmds): """Convenience method to run multiple commands by onecmd_plus_hooks. This method adds the given cmds to the command queue and processes the queue until completion or an error causes it to abort. Scripts that are loaded will have their commands added to the queue. Scripts may even load other scripts recursively. This means, however, that you should not use this method if there is a running cmdloop or some other event-loop. This method is only intended to be used in "one-off" scenarios. NOTE: You may need this method even if you only have one command. If that command is a load, then you will need this command to fully process all the subsequent commands that are loaded from the script file. This is an improvement over onecmd_plus_hooks, which expects to be used inside of a command loop which does the processing of loaded commands. Example: cmd_obj.runcmds_plus_hooks(['load myscript.txt']) :param cmds: list - Command strings suitable for onecmd_plus_hooks. :return: bool - True implies the entire application should exit. """ stop = False self.cmdqueue = list(cmds) + self.cmdqueue try: while self.cmdqueue and not stop: line = self.cmdqueue.pop(0) if self.echo and line != 'eos': self.poutput('{}{}'.format(self.prompt, line)) stop = self.onecmd_plus_hooks(line) finally: # Clear out the command queue and script directory stack, just in # case we hit an error and they were not completed. self.cmdqueue = [] self._script_dir = [] # NOTE: placing this return here inside the finally block will # swallow exceptions. This is consistent with what is done in # onecmd_plus_hooks and _cmdloop, although it may not be # necessary/desired here. return stop def _complete_statement(self, line): """Keep accepting lines of input until the command is complete.""" if not line or (not pyparsing.Or(self.commentGrammars).setParseAction(lambda x: '').transformString(line)): raise EmptyStatement() statement = self.parser_manager.parsed(line) while statement.parsed.multilineCommand and (statement.parsed.terminator == ''): statement = '%s\n%s' % (statement.parsed.raw, self.pseudo_raw_input(self.continuation_prompt)) statement = self.parser_manager.parsed(statement) if not statement.parsed.command: raise EmptyStatement() return statement def _redirect_output(self, statement): """Handles output redirection for >, >>, and |. :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance """ if statement.parsed.pipeTo: self.kept_state = Statekeeper(self, ('stdout',)) # Create a pipe with read and write sides read_fd, write_fd = os.pipe() # Make sure that self.poutput() expects unicode strings in Python 3 and byte strings in Python 2 write_mode = 'w' read_mode = 'r' if six.PY2: write_mode = 'wb' read_mode = 'rb' # Open each side of the pipe and set stdout accordingly # noinspection PyTypeChecker self.stdout = io.open(write_fd, write_mode) self.redirecting = True # noinspection PyTypeChecker subproc_stdin = io.open(read_fd, read_mode) # We want Popen to raise an exception if it fails to open the process. Thus we don't set shell to True. try: self.pipe_proc = subprocess.Popen(shlex.split(statement.parsed.pipeTo), stdin=subproc_stdin) except Exception as ex: # Restore stdout to what it was and close the pipe self.stdout.close() subproc_stdin.close() self.pipe_proc = None self.kept_state.restore() self.kept_state = None self.redirecting = False # Re-raise the exception raise ex elif statement.parsed.output: if (not statement.parsed.outputTo) and (not can_clip): raise EnvironmentError('Cannot redirect to paste buffer; install ``xclip`` and re-run to enable') self.kept_state = Statekeeper(self, ('stdout',)) self.kept_sys = Statekeeper(sys, ('stdout',)) self.redirecting = True if statement.parsed.outputTo: mode = 'w' if statement.parsed.output == 2 * self.redirector: mode = 'a' sys.stdout = self.stdout = open(os.path.expanduser(statement.parsed.outputTo), mode) else: sys.stdout = self.stdout = tempfile.TemporaryFile(mode="w+") if statement.parsed.output == '>>': self.poutput(get_paste_buffer()) def _restore_output(self, statement): """Handles restoring state after output redirection as well as the actual pipe operation if present. :param statement: ParsedString - subclass of str which also contains pyparsing ParseResults instance """ # If we have redirected output to a file or the clipboard or piped it to a shell command, then restore state if self.kept_state is not None: # If we redirected output to the clipboard if statement.parsed.output and not statement.parsed.outputTo: self.stdout.seek(0) write_to_paste_buffer(self.stdout.read()) try: # Close the file or pipe that stdout was redirected to self.stdout.close() except BROKEN_PIPE_ERROR: pass finally: # Restore self.stdout self.kept_state.restore() self.kept_state = None # If we were piping output to a shell command, then close the subprocess the shell command was running in if self.pipe_proc is not None: self.pipe_proc.communicate() self.pipe_proc = None # Restore sys.stdout if need be if self.kept_sys is not None: self.kept_sys.restore() self.kept_sys = None self.redirecting = False def _func_named(self, arg): """Gets the method name associated with a given command. :param arg: str - command to look up method name which implements it :return: str - method name which implements the given command """ result = None target = 'do_' + arg if target in dir(self): result = target return result def onecmd(self, line): """ This executes the actual do_* method for a command. If the command provided doesn't exist, then it executes _default() instead. :param line: ParsedString - subclass of string including the pyparsing ParseResults :return: bool - a flag indicating whether the interpretation of commands should stop """ statement = self.parser_manager.parsed(line) funcname = self._func_named(statement.parsed.command) if not funcname: return self.default(statement) # Since we have a valid command store it in the history if statement.parsed.command not in self.exclude_from_history: self.history.append(statement.parsed.raw) try: func = getattr(self, funcname) except AttributeError: return self.default(statement) stop = func(statement) return stop def default(self, statement): """Executed when the command given isn't a recognized command implemented by a do_* method. :param statement: ParsedString - subclass of string including the pyparsing ParseResults :return: """ arg = statement.full_parsed_statement() if self.default_to_shell: result = os.system(arg) # If os.system() succeeded, then don't print warning about unknown command if not result: return # Print out a message stating this is an unknown command self.poutput('*** Unknown syntax: {}\n'.format(arg)) @staticmethod def _surround_ansi_escapes(prompt, start="\x01", end="\x02"): """Overcome bug in GNU Readline in relation to calculation of prompt length in presence of ANSI escape codes. :param prompt: str - original prompt :param start: str - start code to tell GNU Readline about beginning of invisible characters :param end: str - end code to tell GNU Readline about end of invisible characters :return: str - prompt safe to pass to GNU Readline """ # Windows terminals don't use ANSI escape codes and Windows readline isn't based on GNU Readline if sys.platform == "win32": return prompt escaped = False result = "" for c in prompt: if c == "\x1b" and not escaped: result += start + c escaped = True elif c.isalpha() and escaped: result += c + end escaped = False else: result += c return result def pseudo_raw_input(self, prompt): """ began life as a copy of cmd's cmdloop; like raw_input but - accounts for changed stdin, stdout - if input is a pipe (instead of a tty), look at self.echo to decide whether to print the prompt and the input """ # Deal with the vagaries of readline and ANSI escape codes safe_prompt = self._surround_ansi_escapes(prompt) if self.use_rawinput: try: if sys.stdin.isatty(): line = sm.input(safe_prompt) else: line = sm.input() if self.echo: sys.stdout.write('{}{}\n'.format(safe_prompt, line)) except EOFError: line = 'eof' else: if self.stdin.isatty(): # on a tty, print the prompt first, then read the line self.poutput(safe_prompt, end='') self.stdout.flush() line = self.stdin.readline() if len(line) == 0: line = 'eof' else: # we are reading from a pipe, read the line to see if there is # anything there, if so, then decide whether to print the # prompt or not line = self.stdin.readline() if len(line): # we read something, output the prompt and the something if self.echo: self.poutput('{}{}'.format(safe_prompt, line)) else: line = 'eof' return line.strip() def _cmdloop(self): """Repeatedly issue a prompt, accept input, parse an initial prefix off the received input, and dispatch to action methods, passing them the remainder of the line as argument. This serves the same role as cmd.cmdloop(). :return: bool - True implies the entire application should exit. """ # An almost perfect copy from Cmd; however, the pseudo_raw_input portion # has been split out so that it can be called separately if self.use_rawinput and self.completekey: # Set up readline for our tab completion needs if rl_type == RlType.GNU: readline.set_completion_display_matches_hook(self._display_matches_gnu_readline) # Set GNU readline's rl_basic_quote_characters to NULL so it won't automatically add a closing quote # We don't need to worry about setting rl_completion_suppress_quote since we never declared # rl_completer_quote_characters. rl_basic_quote_characters.value = None elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = self._display_matches_pyreadline try: self.old_completer = readline.get_completer() self.old_delims = readline.get_completer_delims() readline.set_completer(self.complete) # Break words on whitespace and quotes when tab completing completer_delims = " \t\n" + ''.join(QUOTES) if self.allow_redirection: # If redirection is allowed, then break words on those characters too completer_delims += ''.join(REDIRECTION_CHARS) readline.set_completer_delims(completer_delims) # Enable tab completion readline.parse_and_bind(self.completekey + ": complete") except NameError: pass stop = None try: while not stop: if self.cmdqueue: # Run command out of cmdqueue if nonempty (populated by load command or commands at invocation) line = self.cmdqueue.pop(0) if self.echo and line != 'eos': self.poutput('{}{}'.format(self.prompt, line)) else: # Otherwise, read a command from stdin if not self.quit_on_sigint: try: line = self.pseudo_raw_input(self.prompt) except KeyboardInterrupt: self.poutput('^C') line = '' else: line = self.pseudo_raw_input(self.prompt) # Run the command along with all associated pre and post hooks stop = self.onecmd_plus_hooks(line) finally: if self.use_rawinput and self.completekey: # Restore what we changed in readline try: readline.set_completer(self.old_completer) readline.set_completer_delims(self.old_delims) except NameError: pass if rl_type == RlType.GNU: readline.set_completion_display_matches_hook(None) rl_basic_quote_characters.value = orig_rl_basic_quote_characters_addr elif rl_type == RlType.PYREADLINE: readline.rl.mode._display_completions = orig_pyreadline_display # Need to set empty list this way because Python 2 doesn't support the clear() method on lists self.cmdqueue = [] self._script_dir = [] return stop @with_argument_list def do_alias(self, arglist): """Define or display aliases Usage: Usage: alias [name] | [ ] Where: name - name of the alias being looked up, added, or replaced value - what the alias will be resolved to (if adding or replacing) this can contain spaces and does not need to be quoted Without arguments, 'alias' prints a list of all aliases in a reusable form which can be outputted to a startup_script to preserve aliases across sessions. With one argument, 'alias' shows the value of the specified alias. Example: alias ls (Prints the value of the alias called 'ls' if it exists) With two or more arguments, 'alias' creates or replaces an alias. Example: alias ls !ls -lF If you want to use redirection or pipes in the alias, then either quote the tokens with these characters or quote the entire alias value. Examples: alias save_results print_results ">" out.txt alias save_results print_results "> out.txt" alias save_results "print_results > out.txt" """ # If no args were given, then print a list of current aliases if not arglist: for cur_alias in self.aliases: self.poutput("alias {} {}".format(cur_alias, self.aliases[cur_alias])) # The user is looking up an alias elif len(arglist) == 1: name = arglist[0] if name in self.aliases: self.poutput("alias {} {}".format(name, self.aliases[name])) else: self.perror("Alias {!r} not found".format(name), traceback_war=False) # The user is creating an alias else: name = arglist[0] value = ' '.join(arglist[1:]) # Check for a valid name for cur_char in name: if cur_char not in self.identchars: self.perror("Alias names can only contain the following characters: {}".format(self.identchars), traceback_war=False) return # Set the alias self.aliases[name] = value self.poutput("Alias {!r} created".format(name)) def complete_alias(self, text, line, begidx, endidx): """ Tab completion for alias """ index_dict = \ { 1: self.aliases, 2: self.get_visible_commands() } return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete) @with_argument_list def do_unalias(self, arglist): """Unsets aliases Usage: Usage: unalias [-a] name [name ...] Where: name - name of the alias being unset Options: -a remove all alias definitions """ if not arglist: self.do_help('unalias') if '-a' in arglist: self.aliases.clear() self.poutput("All aliases cleared") else: # Get rid of duplicates arglist = list(set(arglist)) for cur_arg in arglist: if cur_arg in self.aliases: del self.aliases[cur_arg] self.poutput("Alias {!r} cleared".format(cur_arg)) else: self.perror("Alias {!r} does not exist".format(cur_arg), traceback_war=False) def complete_unalias(self, text, line, begidx, endidx): """ Tab completion for unalias """ return self.basic_complete(text, line, begidx, endidx, self.aliases) @with_argument_list def do_help(self, arglist): """List available commands with "help" or detailed help with "help cmd".""" if not arglist or (len(arglist) == 1 and arglist[0] in ('--verbose', '-v')): verbose = len(arglist) == 1 and arglist[0] in ('--verbose', '-v') self._help_menu(verbose) else: # Getting help for a specific command funcname = self._func_named(arglist[0]) if funcname: # Check to see if this function was decorated with an argparse ArgumentParser func = getattr(self, funcname) if func.__dict__.get('has_parser', False): # Function has an argparser, so get help based on all the arguments in case there are sub-commands new_arglist = arglist[1:] new_arglist.append('-h') # Temporarily redirect all argparse output to both sys.stdout and sys.stderr to self.stdout with redirect_stdout(self.stdout): with redirect_stderr(self.stdout): func(new_arglist) else: # No special behavior needed, delegate to cmd base class do_help() cmd.Cmd.do_help(self, funcname[3:]) else: # This could be a help topic cmd.Cmd.do_help(self, arglist[0]) def _help_menu(self, verbose=False): """Show a list of commands which help can be displayed for. """ # Get a sorted list of help topics help_topics = self.get_help_topics() help_topics.sort() # Get a sorted list of visible command names visible_commands = self.get_visible_commands() visible_commands.sort() cmds_doc = [] cmds_undoc = [] cmds_cats = {} for command in visible_commands: if command in help_topics or getattr(self, self._func_named(command)).__doc__: if command in help_topics: help_topics.remove(command) if hasattr(getattr(self, self._func_named(command)), HELP_CATEGORY): category = getattr(getattr(self, self._func_named(command)), HELP_CATEGORY) cmds_cats.setdefault(category, []) cmds_cats[category].append(command) else: cmds_doc.append(command) else: cmds_undoc.append(command) if len(cmds_cats) == 0: # No categories found, fall back to standard behavior self.poutput("{}\n".format(str(self.doc_leader))) self._print_topics(self.doc_header, cmds_doc, verbose) else: # Categories found, Organize all commands by category self.poutput('{}\n'.format(str(self.doc_leader))) self.poutput('{}\n\n'.format(str(self.doc_header))) for category in sorted(cmds_cats.keys()): self._print_topics(category, cmds_cats[category], verbose) self._print_topics('Other', cmds_doc, verbose) self.print_topics(self.misc_header, help_topics, 15, 80) self.print_topics(self.undoc_header, cmds_undoc, 15, 80) def _print_topics(self, header, cmds, verbose): """Customized version of print_topics that can switch between verbose or traditional output""" if cmds: if not verbose: self.print_topics(header, cmds, 15, 80) else: self.stdout.write('{}\n'.format(str(header))) widest = 0 # measure the commands for command in cmds: width = len(command) if width > widest: widest = width # add a 4-space pad widest += 4 if widest < 20: widest = 20 if self.ruler: self.stdout.write('{:{ruler}<{width}}\n'.format('', ruler=self.ruler, width=80)) for command in cmds: # Try to get the documentation string try: # first see if there's a help function implemented func = getattr(self, 'help_' + command) except AttributeError: # Couldn't find a help function try: # Now see if help_summary has been set doc = getattr(self, self._func_named(command)).help_summary except AttributeError: # Last, try to directly access the function's doc-string doc = getattr(self, self._func_named(command)).__doc__ else: # we found the help function result = StringIO() # try to redirect system stdout with redirect_stdout(result): # save our internal stdout stdout_orig = self.stdout try: # redirect our internal stdout self.stdout = result func() finally: # restore internal stdout self.stdout = stdout_orig doc = result.getvalue() # Attempt to locate the first documentation block doc_block = [] found_first = False for doc_line in doc.splitlines(): str(doc_line).strip() if len(doc_line.strip()) > 0: doc_block.append(doc_line.strip()) found_first = True else: if found_first: break for doc_line in doc_block: self.stdout.write('{: <{col_width}}{doc}\n'.format(command, col_width=widest, doc=doc_line)) command = '' self.stdout.write("\n") def do_shortcuts(self, _): """Lists shortcuts (aliases) available.""" result = "\n".join('%s: %s' % (sc[0], sc[1]) for sc in sorted(self.shortcuts)) self.poutput("Shortcuts for other commands:\n{}\n".format(result)) def do_eof(self, _): """Called when -D is pressed.""" # End of script should not exit app, but -D should. print('') # Required for clearing line when exiting submenu return self._STOP_AND_EXIT def do_quit(self, _): """Exits this application.""" self._should_quit = True return self._STOP_AND_EXIT def select(self, opts, prompt='Your choice? '): """Presents a numbered menu to the user. Modelled after the bash shell's SELECT. Returns the item chosen. Argument ``opts`` can be: | a single string -> will be split into one-word options | a list of strings -> will be offered as options | a list of tuples -> interpreted as (value, text), so that the return value can differ from the text advertised to the user """ local_opts = opts if isinstance(opts, string_types): local_opts = list(zip(opts.split(), opts.split())) fulloptions = [] for opt in local_opts: if isinstance(opt, string_types): fulloptions.append((opt, opt)) else: try: fulloptions.append((opt[0], opt[1])) except IndexError: fulloptions.append((opt[0], opt[0])) for (idx, (value, text)) in enumerate(fulloptions): self.poutput(' %2d. %s\n' % (idx + 1, text)) while True: response = sm.input(prompt) hlen = readline.get_current_history_length() if hlen >= 1 and response != '': readline.remove_history_item(hlen - 1) try: response = int(response) result = fulloptions[response - 1][0] break except (ValueError, IndexError): self.poutput("{!r} isn't a valid choice. Pick a number between 1 and {}:\n".format(response, len(fulloptions))) return result def cmdenvironment(self): """Get a summary report of read-only settings which the user cannot modify at runtime. :return: str - summary report of read-only settings which the user cannot modify at runtime """ read_only_settings = """ Commands may be terminated with: {} Arguments at invocation allowed: {} Output redirection and pipes allowed: {} Parsing of @options commands: Shell lexer mode for command argument splitting: {} Strip Quotes after splitting arguments: {} Argument type: {} """.format(str(self.terminators), self.allow_cli_args, self.allow_redirection, "POSIX" if POSIX_SHLEX else "non-POSIX", "True" if STRIP_QUOTES_FOR_NON_POSIX and not POSIX_SHLEX else "False", "List of argument strings" if USE_ARG_LIST else "string of space-separated arguments") return read_only_settings def show(self, args, parameter): param = '' if parameter: param = parameter.strip().lower() result = {} maxlen = 0 for p in self.settable: if (not param) or p.startswith(param): result[p] = '%s: %s' % (p, str(getattr(self, p))) maxlen = max(maxlen, len(result[p])) if result: for p in sorted(result): if args.long: self.poutput('{} # {}'.format(result[p].ljust(maxlen), self.settable[p])) else: self.poutput(result[p]) # If user has requested to see all settings, also show read-only settings if args.all: self.poutput('\nRead only settings:{}'.format(self.cmdenvironment())) else: raise LookupError("Parameter '%s' not supported (type 'show' for list of parameters)." % param) set_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) set_parser.add_argument('-a', '--all', action='store_true', help='display read-only settings as well') set_parser.add_argument('-l', '--long', action='store_true', help='describe function of parameter') set_parser.add_argument('settable', nargs='*', help='[param_name] [value]') @with_argparser(set_parser) def do_set(self, args): """Sets a settable parameter or shows current settings of parameters. Accepts abbreviated parameter names so long as there is no ambiguity. Call without arguments for a list of settable parameters with their values. """ try: param_name, val = args.settable val = val.strip() param_name = param_name.strip().lower() if param_name not in self.settable: hits = [p for p in self.settable if p.startswith(param_name)] if len(hits) == 1: param_name = hits[0] else: return self.show(args, param_name) current_val = getattr(self, param_name) if (val[0] == val[-1]) and val[0] in ("'", '"'): val = val[1:-1] else: val = cast(current_val, val) setattr(self, param_name, val) self.poutput('%s - was: %s\nnow: %s\n' % (param_name, current_val, val)) if current_val != val: try: onchange_hook = getattr(self, '_onchange_%s' % param_name) onchange_hook(old=current_val, new=val) except AttributeError: pass except (ValueError, AttributeError): param = '' if args.settable: param = args.settable[0] self.show(args, param) def do_shell(self, command): """Execute a command as if at the OS prompt. Usage: shell [arguments]""" try: # Use non-POSIX parsing to keep the quotes around the tokens tokens = shlex.split(command, posix=False) except ValueError as err: self.perror(err, traceback_war=False) return # Support expanding ~ in quoted paths for index, _ in enumerate(tokens): if tokens[index]: # Check if the token is quoted. Since shlex.split() passed, there isn't # an unclosed quote, so we only need to check the first character. first_char = tokens[index][0] if first_char in QUOTES: tokens[index] = strip_quotes(tokens[index]) tokens[index] = os.path.expanduser(tokens[index]) # Restore the quotes if first_char in QUOTES: tokens[index] = first_char + tokens[index] + first_char expanded_command = ' '.join(tokens) proc = subprocess.Popen(expanded_command, stdout=self.stdout, shell=True) proc.communicate() def complete_shell(self, text, line, begidx, endidx): """Handles tab completion of executable commands and local file system paths for the shell command :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :return: List[str] - a list of possible tab completions """ index_dict = {1: self.shell_cmd_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict, self.path_complete) def cmd_with_subs_completer(self, text, line, begidx, endidx): """ This is a function provided for convenience to those who want an easy way to add tab completion to functions that implement subcommands. By setting this as the completer of the base command function, the correct completer for the chosen subcommand will be called. The use of this function requires assigning a completer function to the subcommand's parser Example: A command called print has a subcommands called 'names' that needs a tab completer When you create the parser for names, include the completer function in the parser's defaults. names_parser.set_defaults(func=print_names, completer=complete_print_names) To make sure the names completer gets called, set the completer for the print function in a similar fashion to what follows. complete_print = cmd2.Cmd.cmd_with_subs_completer When the subcommand's completer is called, this function will have stripped off all content from the beginning of the command line before the subcommand, meaning the line parameter always starts with the subcommand name and the index parameters reflect this change. For instance, the command "print names -d 2" becomes "names -d 2" begidx and endidx are incremented accordingly :param text: str - the string prefix we are attempting to match (all returned matches must begin with it) :param line: str - the current input line with leading whitespace removed :param begidx: int - the beginning index of the prefix text :param endidx: int - the ending index of the prefix text :return: List[str] - a list of possible tab completions """ # The command is the token at index 0 in the command line cmd_index = 0 # The subcommand is the token at index 1 in the command line subcmd_index = 1 # Get all tokens through the one being completed tokens, _ = self.tokens_for_completion(line, begidx, endidx) if tokens is None: return [] matches = [] # Get the index of the token being completed index = len(tokens) - 1 # If the token being completed is past the subcommand name, then do subcommand specific tab-completion if index > subcmd_index: # Get the command name command = tokens[cmd_index] # Get the subcommand name subcommand = tokens[subcmd_index] # Find the offset into line where the subcommand name begins subcmd_start = 0 for cur_index in range(0, subcmd_index + 1): cur_token = tokens[cur_index] subcmd_start = line.find(cur_token, subcmd_start) if cur_index != subcmd_index: subcmd_start += len(cur_token) # Strip off everything before subcommand name orig_line = line line = line[subcmd_start:] # Update the indexes diff = len(orig_line) - len(line) begidx -= diff endidx -= diff # Call the subcommand specific completer if it exists compfunc = self.get_subcommand_completer(command, subcommand) if compfunc is not None: matches = compfunc(self, text, line, begidx, endidx) return matches # noinspection PyBroadException def do_py(self, arg): """ Invoke python command, shell, or script py : Executes a Python command. py: Enters interactive Python mode. End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``. Non-python commands can be issued with ``cmd("your command")``. Run python code from external script files with ``run("script.py")`` """ if self._in_py: self.perror("Recursively entering interactive Python consoles is not allowed.", traceback_war=False) return self._in_py = True try: self.pystate['self'] = self arg = arg.strip() # Support the run command even if called prior to invoking an interactive interpreter def run(filename): """Run a Python script file in the interactive console. :param filename: str - filename of *.py script file to run """ try: with open(filename) as f: interp.runcode(f.read()) except IOError as e: self.perror(e) def onecmd_plus_hooks(cmd_plus_args): """Run a cmd2.Cmd command from a Python script or the interactive Python console. :param cmd_plus_args: str - command line including command and arguments to run :return: bool - True if cmdloop() should exit once leaving the interactive Python console """ return self.onecmd_plus_hooks(cmd_plus_args + '\n') self.pystate['run'] = run self.pystate['cmd'] = onecmd_plus_hooks localvars = (self.locals_in_py and self.pystate) or {} interp = InteractiveConsole(locals=localvars) interp.runcode('import sys, os;sys.path.insert(0, os.getcwd())') if arg: interp.runcode(arg) else: # noinspection PyShadowingBuiltins def quit(): """Function callable from the interactive Python console to exit that environment""" raise EmbeddedConsoleExit self.pystate['quit'] = quit self.pystate['exit'] = quit keepstate = None try: cprt = 'Type "help", "copyright", "credits" or "license" for more information.' keepstate = Statekeeper(sys, ('stdin', 'stdout')) sys.stdout = self.stdout sys.stdin = self.stdin interp.interact(banner="Python %s on %s\n%s\n(%s)\n%s" % (sys.version, sys.platform, cprt, self.__class__.__name__, self.do_py.__doc__)) except EmbeddedConsoleExit: pass if keepstate is not None: keepstate.restore() except Exception: pass finally: self._in_py = False return self._should_quit @with_argument_list def do_pyscript(self, arglist): """\nRuns a python script file inside the console Usage: pyscript [script_arguments] Console commands can be executed inside this script with cmd("your command") However, you cannot run nested "py" or "pyscript" commands from within this script Paths or arguments that contain spaces must be enclosed in quotes """ if not arglist: self.perror("pyscript command requires at least 1 argument ...", traceback_war=False) self.do_help('pyscript') return # Get the absolute path of the script script_path = os.path.expanduser(arglist[0]) # Save current command line arguments orig_args = sys.argv # Overwrite sys.argv to allow the script to take command line arguments sys.argv = [script_path] sys.argv.extend(arglist[1:]) # Run the script - use repr formatting to escape things which need to be escaped to prevent issues on Windows self.do_py("run({!r})".format(script_path)) # Restore command line arguments to original state sys.argv = orig_args # Enable tab-completion for pyscript command def complete_pyscript(self, text, line, begidx, endidx): index_dict = {1: self.path_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict) # Only include the do_ipy() method if IPython is available on the system if ipython_available: # noinspection PyMethodMayBeStatic,PyUnusedLocal def do_ipy(self, arg): """Enters an interactive IPython shell. Run python code from external files with ``run filename.py`` End with ``Ctrl-D`` (Unix) / ``Ctrl-Z`` (Windows), ``quit()``, '`exit()``. """ banner = 'Entering an embedded IPython shell type quit() or -d to exit ...' exit_msg = 'Leaving IPython, back to {}'.format(sys.argv[0]) embed(banner1=banner, exit_msg=exit_msg) history_parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) history_parser_group = history_parser.add_mutually_exclusive_group() history_parser_group.add_argument('-r', '--run', action='store_true', help='run selected history items') history_parser_group.add_argument('-e', '--edit', action='store_true', help='edit and then run selected history items') history_parser_group.add_argument('-s', '--script', action='store_true', help='script format; no separation lines') history_parser_group.add_argument('-o', '--output-file', metavar='FILE', help='output commands to a script file') history_parser_group.add_argument('-t', '--transcript', help='output commands and results to a transcript file') _history_arg_help = """empty all history items a one history item by number a..b, a:b, a:, ..b items by indices (inclusive) [string] items containing string /regex/ items matching regular expression""" history_parser.add_argument('arg', nargs='?', help=_history_arg_help) @with_argparser(history_parser) def do_history(self, args): """View, run, edit, and save previously entered commands.""" # If an argument was supplied, then retrieve partial contents of the history cowardly_refuse_to_run = False if args.arg: # If a character indicating a slice is present, retrieve # a slice of the history arg = args.arg if '..' in arg or ':' in arg: try: # Get a slice of history history = self.history.span(arg) except IndexError: history = self.history.get(arg) else: # Get item(s) from history by index or string search history = self.history.get(arg) else: # If no arg given, then retrieve the entire history cowardly_refuse_to_run = True # Get a copy of the history so it doesn't get mutated while we are using it history = self.history[:] if args.run: if cowardly_refuse_to_run: self.perror("Cowardly refusing to run all previously entered commands.", traceback_war=False) self.perror("If this is what you want to do, specify '1:' as the range of history.", traceback_war=False) else: for runme in history: self.pfeedback(runme) if runme: self.onecmd_plus_hooks(runme) elif args.edit: fd, fname = tempfile.mkstemp(suffix='.txt', text=True) with os.fdopen(fd, 'w') as fobj: for command in history: fobj.write('{}\n'.format(command)) try: os.system('"{}" "{}"'.format(self.editor, fname)) self.do_load(fname) except Exception: raise finally: os.remove(fname) elif args.output_file: try: with open(os.path.expanduser(args.output_file), 'w') as fobj: for command in history: fobj.write('{}\n'.format(command)) plural = 's' if len(history) > 1 else '' self.pfeedback('{} command{} saved to {}'.format(len(history), plural, args.output_file)) except Exception as e: self.perror('Saving {!r} - {}'.format(args.output_file, e), traceback_war=False) elif args.transcript: # Make sure echo is on so commands print to standard out saved_echo = self.echo self.echo = True # Redirect stdout to the transcript file saved_self_stdout = self.stdout self.stdout = open(args.transcript, 'w') # Run all of the commands in the history with output redirected to transcript and echo on self.runcmds_plus_hooks(history) # Restore stdout to its original state self.stdout.close() self.stdout = saved_self_stdout # Set echo back to its original state self.echo = saved_echo # Post-process the file to escape un-escaped "/" regex escapes with open(args.transcript, 'r') as fin: data = fin.read() post_processed_data = data.replace('/', '\/') with open(args.transcript, 'w') as fout: fout.write(post_processed_data) plural = 's' if len(history) > 1 else '' self.pfeedback('{} command{} and outputs saved to transcript file {!r}'.format(len(history), plural, args.transcript)) else: # Display the history items retrieved for hi in history: if args.script: self.poutput(hi) else: self.poutput(hi.pr()) @with_argument_list def do_edit(self, arglist): """Edit a file in a text editor. Usage: edit [file_path] Where: * file_path - path to a file to open in editor The editor used is determined by the ``editor`` settable parameter. "set editor (program-name)" to change or set the EDITOR environment variable. """ if not self.editor: raise EnvironmentError("Please use 'set editor' to specify your text editing program of choice.") filename = arglist[0] if arglist else '' if filename: os.system('"{}" "{}"'.format(self.editor, filename)) else: os.system('"{}"'.format(self.editor)) # Enable tab-completion for edit command def complete_edit(self, text, line, begidx, endidx): index_dict = {1: self.path_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict) @property def _current_script_dir(self): """Accessor to get the current script directory from the _script_dir LIFO queue.""" if self._script_dir: return self._script_dir[-1] else: return None @with_argument_list def do__relative_load(self, arglist): """Runs commands in script file that is encoded as either ASCII or UTF-8 text. Usage: _relative_load optional argument: file_path a file path pointing to a script Script should contain one command per line, just like command would be typed in console. If this is called from within an already-running script, the filename will be interpreted relative to the already-running script's directory. NOTE: This command is intended to only be used within text file scripts. """ # If arg is None or arg is an empty string this is an error if not arglist: self.perror('_relative_load command requires a file path:', traceback_war=False) return file_path = arglist[0].strip() # NOTE: Relative path is an absolute path, it is just relative to the current script directory relative_path = os.path.join(self._current_script_dir or '', file_path) self.do_load(relative_path) def do_eos(self, _): """Handles cleanup when a script has finished executing.""" if self._script_dir: self._script_dir.pop() @with_argument_list def do_load(self, arglist): """Runs commands in script file that is encoded as either ASCII or UTF-8 text. Usage: load * file_path - a file path pointing to a script Script should contain one command per line, just like command would be typed in console. """ # If arg is None or arg is an empty string this is an error if not arglist: self.perror('load command requires a file path:', traceback_war=False) return file_path = arglist[0].strip() expanded_path = os.path.abspath(os.path.expanduser(file_path)) # Make sure expanded_path points to a file if not os.path.isfile(expanded_path): self.perror('{} does not exist or is not a file'.format(expanded_path), traceback_war=False) return # Make sure the file is not empty if os.path.getsize(expanded_path) == 0: self.perror('{} is empty'.format(expanded_path), traceback_war=False) return # Make sure the file is ASCII or UTF-8 encoded text if not self.is_text_file(expanded_path): self.perror('{} is not an ASCII or UTF-8 encoded text file'.format(expanded_path), traceback_war=False) return try: # Read all lines of the script and insert into the head of the # command queue. Add an "end of script (eos)" command to cleanup the # self._script_dir list when done. Specify file encoding in Python # 3, but Python 2 doesn't allow that argument to open(). kwargs = {'encoding': 'utf-8'} if six.PY3 else {} with open(expanded_path, **kwargs) as target: self.cmdqueue = target.read().splitlines() + ['eos'] + self.cmdqueue except IOError as e: self.perror('Problem accessing script from {}:\n{}'.format(expanded_path, e)) return self._script_dir.append(os.path.dirname(expanded_path)) # Enable tab-completion for load command def complete_load(self, text, line, begidx, endidx): index_dict = {1: self.path_complete} return self.index_based_complete(text, line, begidx, endidx, index_dict) @staticmethod def is_text_file(file_path): """ Returns if a file contains only ASCII or UTF-8 encoded text :param file_path: path to the file being checked """ expanded_path = os.path.abspath(os.path.expanduser(file_path.strip())) valid_text_file = False # Check if the file is ASCII try: with codecs.open(expanded_path, encoding='ascii', errors='strict') as f: # Make sure the file has at least one line of text # noinspection PyUnusedLocal if sum(1 for line in f) > 0: valid_text_file = True except IOError: pass except UnicodeDecodeError: # The file is not ASCII. Check if it is UTF-8. try: with codecs.open(expanded_path, encoding='utf-8', errors='strict') as f: # Make sure the file has at least one line of text # noinspection PyUnusedLocal if sum(1 for line in f) > 0: valid_text_file = True except IOError: pass except UnicodeDecodeError: # Not UTF-8 pass return valid_text_file def run_transcript_tests(self, callargs): """Runs transcript tests for provided file(s). This is called when either -t is provided on the command line or the transcript_files argument is provided during construction of the cmd2.Cmd instance. :param callargs: List[str] - list of transcript test file names """ class TestMyAppCase(Cmd2TestCase): cmdapp = self self.__class__.testfiles = callargs sys.argv = [sys.argv[0]] # the --test argument upsets unittest.main() testcase = TestMyAppCase() runner = unittest.TextTestRunner() runner.run(testcase) def cmdloop(self, intro=None): """This is an outer wrapper around _cmdloop() which deals with extra features provided by cmd2. _cmdloop() provides the main loop equivalent to cmd.cmdloop(). This is a wrapper around that which deals with the following extra features provided by cmd2: - commands at invocation - transcript testing - intro banner :param intro: str - if provided this overrides self.intro and serves as the intro banner printed once at start """ if self.allow_cli_args: parser = optparse.OptionParser() parser.add_option('-t', '--test', dest='test', action="store_true", help='Test against transcript(s) in FILE (wildcards OK)') (callopts, callargs) = parser.parse_args() # If transcript testing was called for, use other arguments as transcript files if callopts.test: self._transcript_files = callargs # If commands were supplied at invocation, then add them to the command queue if callargs: self.cmdqueue.extend(callargs) # Always run the preloop first self.preloop() # If transcript-based regression testing was requested, then do that instead of the main loop if self._transcript_files is not None: self.run_transcript_tests(self._transcript_files) else: # If an intro was supplied in the method call, allow it to override the default if intro is not None: self.intro = intro # Print the intro, if there is one, right after the preloop if self.intro is not None: self.poutput(str(self.intro) + "\n") # And then call _cmdloop() to enter the main loop self._cmdloop() # Run the postloop() no matter what self.postloop() # noinspection PyPep8Naming class ParserManager: """ Class which encapsulates all of the pyparsing parser functionality for cmd2 in a single location. """ def __init__(self, redirector, terminators, multilineCommands, legalChars, commentGrammars, commentInProgress, blankLinesAllowed, prefixParser, preparse, postparse, aliases, shortcuts): """Creates and uses parsers for user input according to app's parameters.""" self.commentGrammars = commentGrammars self.preparse = preparse self.postparse = postparse self.aliases = aliases self.shortcuts = shortcuts self.main_parser = self._build_main_parser(redirector=redirector, terminators=terminators, multilineCommands=multilineCommands, legalChars=legalChars, commentInProgress=commentInProgress, blankLinesAllowed=blankLinesAllowed, prefixParser=prefixParser) self.input_source_parser = self._build_input_source_parser(legalChars=legalChars, commentInProgress=commentInProgress) def _build_main_parser(self, redirector, terminators, multilineCommands, legalChars, commentInProgress, blankLinesAllowed, prefixParser): """Builds a PyParsing parser for interpreting user commands.""" # Build several parsing components that are eventually compiled into overall parser output_destination_parser = (pyparsing.Literal(redirector * 2) | (pyparsing.WordStart() + redirector) | pyparsing.Regex('[^=]' + redirector))('output') terminator_parser = pyparsing.Or( [(hasattr(t, 'parseString') and t) or pyparsing.Literal(t) for t in terminators])('terminator') string_end = pyparsing.stringEnd ^ '\nEOF' multilineCommand = pyparsing.Or( [pyparsing.Keyword(c, caseless=False) for c in multilineCommands])('multilineCommand') oneline_command = (~multilineCommand + pyparsing.Word(legalChars))('command') pipe = pyparsing.Keyword('|', identChars='|') do_not_parse = self.commentGrammars | commentInProgress | pyparsing.quotedString after_elements = \ pyparsing.Optional(pipe + pyparsing.SkipTo(output_destination_parser ^ string_end, ignore=do_not_parse)('pipeTo')) + \ pyparsing.Optional(output_destination_parser + pyparsing.SkipTo(string_end, ignore=do_not_parse). setParseAction(lambda x: strip_quotes(x[0].strip()))('outputTo')) multilineCommand.setParseAction(lambda x: x[0]) oneline_command.setParseAction(lambda x: x[0]) if blankLinesAllowed: blankLineTerminationParser = pyparsing.NoMatch else: blankLineTerminator = (pyparsing.lineEnd + pyparsing.lineEnd)('terminator') blankLineTerminator.setResultsName('terminator') blankLineTerminationParser = ((multilineCommand ^ oneline_command) + pyparsing.SkipTo(blankLineTerminator, ignore=do_not_parse).setParseAction( lambda x: x[0].strip())('args') + blankLineTerminator)('statement') multilineParser = (((multilineCommand ^ oneline_command) + pyparsing.SkipTo(terminator_parser, ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('args') + terminator_parser)('statement') + pyparsing.SkipTo(output_destination_parser ^ pipe ^ string_end, ignore=do_not_parse).setParseAction(lambda x: x[0].strip())('suffix') + after_elements) multilineParser.ignore(commentInProgress) singleLineParser = ((oneline_command + pyparsing.SkipTo(terminator_parser ^ string_end ^ pipe ^ output_destination_parser, ignore=do_not_parse).setParseAction( lambda x: x[0].strip())('args'))('statement') + pyparsing.Optional(terminator_parser) + after_elements) blankLineTerminationParser = blankLineTerminationParser.setResultsName('statement') parser = prefixParser + ( string_end | multilineParser | singleLineParser | blankLineTerminationParser | multilineCommand + pyparsing.SkipTo(string_end, ignore=do_not_parse) ) parser.ignore(self.commentGrammars) return parser @staticmethod def _build_input_source_parser(legalChars, commentInProgress): """Builds a PyParsing parser for alternate user input sources (from file, pipe, etc.)""" input_mark = pyparsing.Literal('<') input_mark.setParseAction(lambda x: '') # Also allow spaces, slashes, and quotes file_name = pyparsing.Word(legalChars + ' /\\"\'') input_from = file_name('inputFrom') input_from.setParseAction(replace_with_file_contents) # a not-entirely-satisfactory way of distinguishing < as in "import from" from < # as in "lesser than" inputParser = input_mark + pyparsing.Optional(input_from) + pyparsing.Optional('>') + \ pyparsing.Optional(file_name) + (pyparsing.stringEnd | '|') inputParser.ignore(commentInProgress) return inputParser def parsed(self, raw): """ This function is where the actual parsing of each line occurs. :param raw: str - the line of text as it was entered :return: ParsedString - custom subclass of str with extra attributes """ if isinstance(raw, ParsedString): p = raw else: # preparse is an overridable hook; default makes no changes s = self.preparse(raw) s = self.input_source_parser.transformString(s.lstrip()) s = self.commentGrammars.transformString(s) # Make a copy of aliases so we can edit it tmp_aliases = list(self.aliases.keys()) keep_expanding = len(tmp_aliases) > 0 # Expand aliases while keep_expanding: for cur_alias in tmp_aliases: keep_expanding = False if s == cur_alias or s.startswith(cur_alias + ' '): s = s.replace(cur_alias, self.aliases[cur_alias], 1) # Do not expand the same alias more than once tmp_aliases.remove(cur_alias) keep_expanding = len(tmp_aliases) > 0 break # Expand command shortcut to its full command name for (shortcut, expansion) in self.shortcuts: if s.startswith(shortcut): # If the next character after the shortcut isn't a space, then insert one shortcut_len = len(shortcut) if len(s) == shortcut_len or s[shortcut_len] != ' ': expansion += ' ' # Expand the shortcut s = s.replace(shortcut, expansion, 1) break try: result = self.main_parser.parseString(s) except pyparsing.ParseException: # If we have a parsing failure, treat it is an empty command and move to next prompt result = self.main_parser.parseString('') result['raw'] = raw result['command'] = result.multilineCommand or result.command result = self.postparse(result) p = ParsedString(result.args) p.parsed = result p.parser = self.parsed return p class HistoryItem(str): """Class used to represent an item in the History list. Thin wrapper around str class which adds a custom format for printing. It also keeps track of its index in the list as well as a lowercase representation of itself for convenience/efficiency. """ listformat = '-------------------------[{}]\n{}\n' # noinspection PyUnusedLocal def __init__(self, instr): str.__init__(self) self.lowercase = self.lower() self.idx = None def pr(self): """Represent a HistoryItem in a pretty fashion suitable for printing. :return: str - pretty print string version of a HistoryItem """ return self.listformat.format(self.idx, str(self).rstrip()) class History(list): """ A list of HistoryItems that knows how to respond to user requests. """ # noinspection PyMethodMayBeStatic def _zero_based_index(self, onebased): result = onebased if result > 0: result -= 1 return result def _to_index(self, raw): if raw: result = self._zero_based_index(int(raw)) else: result = None return result spanpattern = re.compile(r'^\s*(?P-?\d+)?\s*(?P:|(\.{2,}))?\s*(?P-?\d+)?\s*$') def span(self, raw): """Parses the input string search for a span pattern and if if found, returns a slice from the History list. :param raw: str - string potentially containing a span of the forms a..b, a:b, a:, ..b :return: List[HistoryItem] - slice from the History list """ if raw.lower() in ('*', '-', 'all'): raw = ':' results = self.spanpattern.search(raw) if not results: raise IndexError if not results.group('separator'): return [self[self._to_index(results.group('start'))]] start = self._to_index(results.group('start')) or 0 # Ensure start is not None end = self._to_index(results.group('end')) reverse = False if end is not None: if end < start: (start, end) = (end, start) reverse = True end += 1 result = self[start:end] if reverse: result.reverse() return result rangePattern = re.compile(r'^\s*(?P[\d]+)?\s*-\s*(?P[\d]+)?\s*$') def append(self, new): """Append a HistoryItem to end of the History list :param new: str - command line to convert to HistoryItem and add to the end of the History list """ new = HistoryItem(new) list.append(self, new) new.idx = len(self) def get(self, getme=None): """Get an item or items from the History list using 1-based indexing. :param getme: int or str - item(s) to get - either an integer index or string to search for :return: List[str] - list of HistoryItems matching the retrieval criteria """ if not getme: return self try: getme = int(getme) if getme < 0: return self[:(-1 * getme)] else: return [self[getme - 1]] except IndexError: return [] except ValueError: range_result = self.rangePattern.search(getme) if range_result: start = range_result.group('start') or None end = range_result.group('start') or None if start: start = int(start) - 1 if end: end = int(end) return self[start:end] # noinspection PyUnresolvedReferences getme = getme.strip() if getme.startswith(r'/') and getme.endswith(r'/'): finder = re.compile(getme[1:-1], re.DOTALL | re.MULTILINE | re.IGNORECASE) def isin(hi): """Listcomp filter function for doing a regular expression search of History. :param hi: HistoryItem :return: bool - True if search matches """ return finder.search(hi) else: def isin(hi): """Listcomp filter function for doing a case-insensitive string search of History. :param hi: HistoryItem :return: bool - True if search matches """ return getme.lower() in hi.lowercase return [itm for itm in self if isin(itm)] def cast(current, new): """Tries to force a new value into the same type as the current when trying to set the value for a parameter. :param current: current value for the parameter, type varies :param new: str - new value :return: new value with same type as current, or the current value if there was an error casting """ typ = type(current) if typ == bool: try: return bool(int(new)) except (ValueError, TypeError): pass try: new = new.lower() except AttributeError: pass if (new == 'on') or (new[0] in ('y', 't')): return True if (new == 'off') or (new[0] in ('n', 'f')): return False else: try: return typ(new) except (ValueError, TypeError): pass print("Problem setting parameter (now %s) to %s; incorrect type?" % (current, new)) return current class Statekeeper(object): """Class used to save and restore state during load and py commands as well as when redirecting output or pipes.""" def __init__(self, obj, attribs): """Use the instance attributes as a generic key-value store to copy instance attributes from outer object. :param obj: instance of cmd2.Cmd derived class (your application instance) :param attribs: Tuple[str] - tuple of strings listing attributes of obj to save a copy of """ self.obj = obj self.attribs = attribs if self.obj: self._save() def _save(self): """Create copies of attributes from self.obj inside this Statekeeper instance.""" for attrib in self.attribs: setattr(self, attrib, getattr(self.obj, attrib)) def restore(self): """Overwrite attributes in self.obj with the saved values stored in this Statekeeper instance.""" if self.obj: for attrib in self.attribs: setattr(self.obj, attrib, getattr(self, attrib)) class OutputTrap(object): """Instantiate an OutputTrap to divert/capture ALL stdout output. For use in transcript testing.""" def __init__(self): self.contents = '' def write(self, txt): """Add text to the internal contents. :param txt: str """ self.contents += txt def read(self): """Read from the internal contents and then clear them out. :return: str - text from the internal contents """ result = self.contents self.contents = '' return result class Cmd2TestCase(unittest.TestCase): """Subclass this, setting CmdApp, to make a unittest.TestCase class that will execute the commands in a transcript file and expect the results shown. See example.py""" cmdapp = None def fetchTranscripts(self): self.transcripts = {} for fileset in self.cmdapp.testfiles: for fname in glob.glob(fileset): tfile = open(fname) self.transcripts[fname] = iter(tfile.readlines()) tfile.close() if not len(self.transcripts): raise Exception("No test files found - nothing to test.") def setUp(self): if self.cmdapp: self.fetchTranscripts() # Trap stdout self._orig_stdout = self.cmdapp.stdout self.cmdapp.stdout = OutputTrap() def runTest(self): # was testall if self.cmdapp: its = sorted(self.transcripts.items()) for (fname, transcript) in its: self._test_transcript(fname, transcript) def _test_transcript(self, fname, transcript): line_num = 0 finished = False line = strip_ansi(next(transcript)) line_num += 1 while not finished: # Scroll forward to where actual commands begin while not line.startswith(self.cmdapp.visible_prompt): try: line = strip_ansi(next(transcript)) except StopIteration: finished = True break line_num += 1 command = [line[len(self.cmdapp.visible_prompt):]] line = next(transcript) # Read the entirety of a multi-line command while line.startswith(self.cmdapp.continuation_prompt): command.append(line[len(self.cmdapp.continuation_prompt):]) try: line = next(transcript) except StopIteration: raise (StopIteration, 'Transcript broke off while reading command beginning at line {} with\n{}'.format(line_num, command[0]) ) line_num += 1 command = ''.join(command) # Send the command into the application and capture the resulting output # TODO: Should we get the return value and act if stop == True? self.cmdapp.onecmd_plus_hooks(command) result = self.cmdapp.stdout.read() # Read the expected result from transcript if strip_ansi(line).startswith(self.cmdapp.visible_prompt): message = '\nFile {}, line {}\nCommand was:\n{}\nExpected: (nothing)\nGot:\n{}\n'.format( fname, line_num, command, result) self.assert_(not (result.strip()), message) continue expected = [] while not strip_ansi(line).startswith(self.cmdapp.visible_prompt): expected.append(line) try: line = next(transcript) except StopIteration: finished = True break line_num += 1 expected = ''.join(expected) # transform the expected text into a valid regular expression expected = self._transform_transcript_expected(expected) message = '\nFile {}, line {}\nCommand was:\n{}\nExpected:\n{}\nGot:\n{}\n'.format( fname, line_num, command, expected, result) self.assertTrue(re.match(expected, result, re.MULTILINE | re.DOTALL), message) def _transform_transcript_expected(self, s): """parse the string with slashed regexes into a valid regex Given a string like: Match a 10 digit phone number: /\d{3}-\d{3}-\d{4}/ Turn it into a valid regular expression which matches the literal text of the string and the regular expression. We have to remove the slashes because they differentiate between plain text and a regular expression. Unless the slashes are escaped, in which case they are interpreted as plain text, or there is only one slash, which is treated as plain text also. Check the tests in tests/test_transcript.py to see all the edge cases. """ regex = '' start = 0 while True: (regex, first_slash_pos, start) = self._escaped_find(regex, s, start, False) if first_slash_pos == -1: # no more slashes, add the rest of the string and bail regex += re.escape(s[start:]) break else: # there is a slash, add everything we have found so far # add stuff before the first slash as plain text regex += re.escape(s[start:first_slash_pos]) start = first_slash_pos+1 # and go find the next one (regex, second_slash_pos, start) = self._escaped_find(regex, s, start, True) if second_slash_pos > 0: # add everything between the slashes (but not the slashes) # as a regular expression regex += s[start:second_slash_pos] # and change where we start looking for slashed on the # turn through the loop start = second_slash_pos + 1 else: # No closing slash, we have to add the first slash, # and the rest of the text regex += re.escape(s[start-1:]) break return regex @staticmethod def _escaped_find(regex, s, start, in_regex): """ Find the next slash in {s} after {start} that is not preceded by a backslash. If we find an escaped slash, add everything up to and including it to regex, updating {start}. {start} therefore serves two purposes, tells us where to start looking for the next thing, and also tells us where in {s} we have already added things to {regex} {in_regex} specifies whether we are currently searching in a regex, we behave differently if we are or if we aren't. """ while True: pos = s.find('/', start) if pos == -1: # no match, return to caller break elif pos == 0: # slash at the beginning of the string, so it can't be # escaped. We found it. break else: # check if the slash is preceeded by a backslash if s[pos-1:pos] == '\\': # it is. if in_regex: # add everything up to the backslash as a # regular expression regex += s[start:pos-1] # skip the backslash, and add the slash regex += s[pos] else: # add everything up to the backslash as escaped # plain text regex += re.escape(s[start:pos-1]) # and then add the slash as escaped # plain text regex += re.escape(s[pos]) # update start to show we have handled everything # before it start = pos+1 # and continue to look else: # slash is not escaped, this is what we are looking for break return regex, pos, start def tearDown(self): if self.cmdapp: # Restore stdout self.cmdapp.stdout = self._orig_stdout def namedtuple_with_two_defaults(typename, field_names, default_values=('', '')): """Wrapper around namedtuple which lets you treat the last value as optional. :param typename: str - type name for the Named tuple :param field_names: List[str] or space-separated string of field names :param default_values: (optional) 2-element tuple containing the default values for last 2 parameters in named tuple Defaults to an empty string for both of them :return: namedtuple type """ T = collections.namedtuple(typename, field_names) # noinspection PyUnresolvedReferences T.__new__.__defaults__ = default_values return T class CmdResult(namedtuple_with_two_defaults('CmdResult', ['out', 'err', 'war'])): """Derive a class to store results from a named tuple so we can tweak dunder methods for convenience. This is provided as a convenience and an example for one possible way for end users to store results in the self._last_result attribute of cmd2.Cmd class instances. See the "python_scripting.py" example for how it can be used to enable conditional control flow. Named tuple attributes ---------------------- out - this is intended to store normal output data from the command and can be of any type that makes sense err: str - (optional) this is intended to store an error message and it being non-empty indicates there was an error Defaults to an empty string war: str - (optional) this is intended to store a warning message which isn't quite an error, but of note Defaults to an empty string. NOTE: Named tuples are immutable. So the contents are there for access, not for modification. """ def __bool__(self): """If err is an empty string, treat the result as a success; otherwise treat it as a failure.""" return not self.err def __nonzero__(self): """Python 2 uses this method for determining Truthiness""" return self.__bool__() if __name__ == '__main__': # If run as the main application, simply start a bare-bones cmd2 application with only built-in functionality. # Set "use_ipython" to True to include the ipy command if IPython is installed, which supports advanced interactive # debugging of your application via introspection on self. app = Cmd(use_ipython=False) app.cmdloop() cmd2-0.8.5/examples/0000755000076500000240000000000013264716261016155 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/examples/event_loops.py0000755000076500000240000000157113110635531021061 0ustar toddleonhardtstaff00000000000000#!/usr/bin/env python # coding=utf-8 """A sample application for integrating cmd2 with external event loops. This is an example of how to use cmd2 in a way so that cmd2 doesn't own the inner event loop of your application. This opens up the possibility of registering cmd2 input with event loops, like asyncio, without occupying the main loop. """ import cmd2 class Cmd2EventBased(cmd2.Cmd): """Basic example of how to run cmd2 without it controlling the main loop.""" 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. # In this case, no prompt is generated, so you need to provide one and read the user's input. app.onecmd_plus_hooks("help history") app.postloop() cmd2-0.8.5/examples/transcripts/0000755000076500000240000000000013264716261020531 5ustar toddleonhardtstaff00000000000000cmd2-0.8.5/examples/transcripts/transcript_regex.txt0000644000076500000240000000067513246364227024666 0ustar toddleonhardtstaff00000000000000# Run this transcript with "python example.py -t transcript_regex.txt" # The regex for colors is because no color on Windows. # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious (Cmd) set colors: /(True|False)/ continuation_prompt: >/ / debug: False echo: False editor: /.*?/ feedback_to_output: False locals_in_py: True maxrepeats: 3 prompt: (Cmd)/ / quiet: False timing: False cmd2-0.8.5/examples/transcripts/exampleSession.txt0000644000076500000240000000070413246364227024273 0ustar toddleonhardtstaff00000000000000# Run this transcript with "python argparse_example.py -t exampleSession.txt" # The regex for colors is because no color on Windows. # The regex for editor will match whatever program you use. # regexes on prompts just make the trailing space obvious (Cmd) set colors: /(True|False)/ continuation_prompt: >/ / debug: False echo: False editor: /.*?/ feedback_to_output: False locals_in_py: True maxrepeats: 3 prompt: (Cmd)/ / quiet: False timing: False cmd2-0.8.5/examples/transcripts/pirate.transcript0000644000076500000240000000030313231514340024110 0ustar toddleonhardtstaff00000000000000arrr> loot Now we gots 1 doubloons arrr> loot Now we gots 2 doubloons arrr> loot Now we gots 3 doubloons arrr> drink 3 Now we gots 0 doubloons arrr> yo --ho 3 rum yo ho ho ho and a bottle of rum cmd2-0.8.5/examples/python_scripting.py0000755000076500000240000001104313257504271022132 0ustar toddleonhardtstaff00000000000000#!/usr/bin/env python # coding=utf-8 """A sample application for how Python scripting can provide conditional control flow of a cmd2 application. cmd2's built-in scripting capability which can be invoked via the "@" shortcut or "load" command and uses basic ASCII text scripts is very easy to use. Moreover, the trivial syntax of the script files where there is one command per line and the line is exactly what the user would type inside the application makes it so non-technical end users can quickly learn to create scripts. However, there comes a time when technical end users want more capability and power. In particular it is common that users will want to create a script with conditional control flow - where the next command run will depend on the results from the previous command. This is where the ability to run Python scripts inside a cmd2 application via the pyscript command and the "pyscript