cmd2-0.7.9/0000755000076500000240000000000013223503124014325 5ustar toddleonhardtstaff00000000000000cmd2-0.7.9/PKG-INFO0000644000076500000240000000616613223503124015433 0ustar toddleonhardtstaff00000000000000Metadata-Version: 1.1 Name: cmd2 Version: 0.7.9 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, case-insensitive, and abbreviated commands - Special-character command shortcuts (beyond cmd's `@` and `!`) - Settable environment parameters - Parsing commands with flags - Unicode character support (*Python 3 only*) - Good tab-completion of 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) 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules cmd2-0.7.9/CODE_OF_CONDUCT.md0000644000076500000240000000623713110635531017137 0ustar toddleonhardtstaff00000000000000# cmd2 Contributor Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at todd.leonhardt at gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ cmd2-0.7.9/LICENSE0000644000076500000240000000211313043433273015336 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.7.9/CHANGELOG.md0000644000076500000240000002135113223501503016137 0ustar toddleonhardtstaff00000000000000## 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.7.9/tests/0000755000076500000240000000000013223503124015467 5ustar toddleonhardtstaff00000000000000cmd2-0.7.9/tests/test_parsing.py0000644000076500000240000003323513142444534020562 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-insensitive parser @pytest.fixture def parser(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] c.case_insensitive = True c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts) return c.parser_manager.main_parser # Case-insensitive ParserManager @pytest.fixture def ci_pm(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] c.case_insensitive = True c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, preparse=c.preparse, postparse=c.postparse, shortcuts=c.shortcuts) return c.parser_manager # Case-sensitive ParserManager @pytest.fixture def cs_pm(): c = cmd2.Cmd() c.multilineCommands = ['multiline'] c.case_insensitive = False c.parser_manager = cmd2.ParserManager(redirector=c.redirector, terminators=c.terminators, multilineCommands=c.multilineCommands, legalChars=c.legalChars, commentGrammars=c.commentGrammars, commentInProgress=c.commentInProgress, case_insensitive=c.case_insensitive, blankLinesAllowed=c.blankLinesAllowed, prefixParser=c.prefixParser, preparse=c.preparse, postparse=c.postparse, 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_insensitive_parsed_single_word(ci_pm): line = 'HeLp' statement = ci_pm.parsed(line) assert statement.parsed.command == line.lower() 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' def test_parse_abbreviated_multiline_not_allowed(parser): line = 'multilin command\n' results = parser.parseString(line) assert results.command == 'multilin' assert results.multilineCommand == '' # 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.7.9/tests/conftest.py0000644000076500000240000000654413146306504017706 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 ): ======================================== _relative_load edit history py quit save shell show cmdenvironment help load pyscript run set shortcuts """ # Help text for the history command HELP_HISTORY = """history [arg]: lists past commands issued | no arg: list all | arg is integer: list one history item, by index | a..b, a:b, a:, ..b -> list history items by a span of indices (inclusive) | arg is string: list all commands matching string search | arg is /enclosed in forward-slashes/: regular expression search Usage: history [options] (limit on which commands to include) Options: -h, --help show this help message and exit -s, --script Script format; no separation lines """ # 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 = """abbrev: False autorun_on_edit: False 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 = """ abbrev: False # Accept abbreviated commands autorun_on_edit: False # Automatically run files after editing 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.7.9/tests/redirect.txt0000644000076500000240000000001013110635531020023 0ustar toddleonhardtstaff00000000000000history cmd2-0.7.9/tests/test_cmd2.py0000644000076500000240000015321013223501503017726 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 # 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, HELP_HISTORY, SHORTCUTS_TXT, SHOW_TXT, SHOW_LONG, StdOut def test_ver(): assert cmd2.__version__ == '0.7.9' 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_history(base_app): out = run_cmd(base_app, 'help history') expected = normalize(HELP_HISTORY) assert out == expected def test_base_options_help(base_app, capsys): run_cmd(base_app, 'show -h') out, err = capsys.readouterr() expected = run_cmd(base_app, 'help show') # 'show -h' is the same as 'help show', other than whitespace differences of an extra newline present in 'help show' assert normalize(str(out)) == expected def test_base_invalid_option(base_app, capsys): run_cmd(base_app, 'show -z') out, err = capsys.readouterr() show_help = run_cmd(base_app, 'help show') expected = ['no such option: -z'] expected.extend(show_help) # 'show -h' is the same as 'help show', other than whitespace differences of an extra newline present in 'help show' assert normalize(str(out)) == 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, 'show') 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, 'show -l') expected = normalize(SHOW_LONG) 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, 'show 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, 'show 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_base_cmdenvironment(base_app): out = run_cmd(base_app, 'cmdenvironment') expected = normalize(""" Commands are case-sensitive: {} 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(not base_app.case_insensitive, 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_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 abbrev on set colors on help shortcuts _relative_load postcmds.txt set abbrev off 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 abbrev on set colors on help shortcuts load %s set abbrev off 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_base_save(base_app): # TODO: Use a temporary directory for the file filename = 'deleteme.txt' base_app.feedback_to_output = True run_cmd(base_app, 'help') run_cmd(base_app, 'help save') # Test the * form of save which saves all commands from history out = run_cmd(base_app, 'save * {}'.format(filename)) assert out == normalize('Saved to {}\n'.format(filename)) expected = normalize(""" help help save save * deleteme.txt """) with open(filename) as f: content = normalize(f.read()) assert content == expected # Test the N form of save which saves a numbered command from history out = run_cmd(base_app, 'save 1 {}'.format(filename)) assert out == normalize('Saved to {}\n'.format(filename)) expected = normalize('help') with open(filename) as f: content = normalize(f.read()) assert content == expected # Test the blank form of save which saves the most recent command from history out = run_cmd(base_app, 'save {}'.format(filename)) assert out == normalize('Saved to {}\n'.format(filename)) expected = normalize('save 1 {}'.format(filename)) with open(filename) as f: content = normalize(f.read()) assert content == expected # Delete file that was created os.remove(filename) def test_save_parse_error(base_app, capsys): invalid_file = '~!@' run_cmd(base_app, 'save {}'.format(invalid_file)) out, err = capsys.readouterr() assert out == '' assert err.startswith('ERROR: Could not understand save target {}\n'.format(invalid_file)) def test_save_tempfile(base_app): # Just run help to make sure there is something in the history base_app.feedback_to_output = True run_cmd(base_app, 'help') out = run_cmd(base_app, 'save *') output = out[0] assert output.startswith('Saved to ') # Delete the tempfile which was created temp_file = output.split('Saved to ')[1].strip() os.remove(temp_file) def test_save_invalid_history_index(base_app, capsys): run_cmd(base_app, 'save 5') out, err = capsys.readouterr() assert out == '' assert err.startswith("EXCEPTION of type 'IndexError' occurred with message: 'list index out of range'\n") def test_save_empty_history_and_index(base_app, capsys): run_cmd(base_app, 'save') out, err = capsys.readouterr() assert out == '' assert err.startswith("ERROR: History is empty, nothing to save.\n") def test_save_invalid_path(base_app, capsys): # Just run help to make sure there is something in the history run_cmd(base_app, 'help') invalid_path = '/no_such_path/foobar.txt' run_cmd(base_app, 'save {}'.format(invalid_path)) out, err = capsys.readouterr() assert out == '' assert err.startswith("ERROR: Saving '{}' - ".format(invalid_path)) 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)) expected = normalize(HELP_HISTORY) assert out == expected def test_pipe_to_shell(base_app, capsys): if sys.platform == "win32": # Windows command = 'help | sort' # Get help menu and pipe it's output to the sort shell command # expected = ['', '', '_relative_load edit history py quit save shell show', # '========================================', # 'cmdenvironment help load pyscript run set shortcuts', # 'Documented commands (type help ):'] # assert out == expected 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 help command just so we have a command in history run_cmd(base_app, 'help') run_cmd(base_app, 'edit') # We have an editor, so should expect a system call m.assert_called_once() def test_edit_empty_history(base_app, capsys): run_cmd(base_app, 'edit') out, err = capsys.readouterr() assert out == '' assert err == 'ERROR: edit must be called with argument if history is empty\n' def test_edit_valid_positive_number(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, 'edit 1') # We have an editor, so should expect a system call m.assert_called_once() def test_edit_valid_negative_number(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, 'edit "-1"') # We have an editor, so should expect a system call m.assert_called_once() def test_edit_invalid_positive_number(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, 'edit 23') # History index is invalid, so should expect a system call m.assert_not_called() def test_edit_invalid_negative_number(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, 'edit "-23"') # History index is invalid, so should expect a system call m.assert_not_called() 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_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 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 ): ======================================== _relative_load edit history py quit save shell show cmdenvironment help load pyscript run set shortcuts 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 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([cmd2.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([cmd2.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 def test_run_command_with_empty_arg(base_app): command = 'help' base_app.feedback_to_output = True run_cmd(base_app, command) out = run_cmd(base_app, 'run') expected = normalize('{}\n\n'.format(command) + BASE_HELP) assert out == expected def test_run_command_with_empty_history(base_app): base_app.feedback_to_output = True out = run_cmd(base_app, 'run') assert out == [] 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) @pytest.fixture def abbrev_app(): app = cmd2.Cmd() app.abbrev = True app.stdout = StdOut() return app def test_exclude_from_history(abbrev_app, monkeypatch): # Run all variants of run run_cmd(abbrev_app, 'run') run_cmd(abbrev_app, 'ru') run_cmd(abbrev_app, 'r') # 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 all variants of edit run_cmd(abbrev_app, 'edit') run_cmd(abbrev_app, 'edi') run_cmd(abbrev_app, 'ed') # Run all variants of history run_cmd(abbrev_app, 'history') run_cmd(abbrev_app, 'histor') run_cmd(abbrev_app, 'histo') run_cmd(abbrev_app, 'hist') run_cmd(abbrev_app, 'his') run_cmd(abbrev_app, 'hi') # Verify that the history is empty out = run_cmd(abbrev_app, 'history') assert out == [] # Now run a command which isn't excluded from the history run_cmd(abbrev_app, 'help') # And verify we have a history now ... out = run_cmd(abbrev_app, 'history') expected = normalize("""-------------------------[1] help""") assert out == expected 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) + 'history [arg]: lists past commands issued') 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] == 'abbrev: False' # 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 == 'abbrev: False' 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.abbrev = False 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] == 'abbrev: False' 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 == 'abbrev: False' 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 cmd2-0.7.9/tests/script.txt0000644000076500000240000000001513125033336017534 0ustar toddleonhardtstaff00000000000000help history cmd2-0.7.9/tests/script.py0000644000076500000240000000025113110635531017346 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.7.9/tests/test_transcript.py0000644000076500000240000002474413147151335021314 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 random import mock import pytest import six # Used for sm.input: raw_input() for Python 2 or input() for Python 3 import six.moves as sm from cmd2 import (Cmd, make_option, options, Cmd2TestCase, set_use_arg_list, set_posix_shlex, set_strip_quotes) from conftest import run_cmd, StdOut, normalize class CmdLineApp(Cmd): MUMBLES = ['like', '...', 'um', 'er', 'hmmm', 'ahh'] MUMBLE_FIRST = ['so', 'like', 'well'] MUMBLE_LAST = ['right?'] def __init__(self, *args, **kwargs): self.abbrev = True 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 ): ======================================== _relative_load help mumble pyscript save shell speak cmdenvironment history orate quit say shortcuts edit load py run set show (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) hi -------------------------[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) run 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', [ ( 'text with no slashes', 'text\ with\ no\ slashes' ), # stuff with just one slash ( 'use 2/3 cup', 'use\ 2\/3\ cup' ), ( '/tmp is nice', '\/tmp\ is\ nice'), ( 'slash at end/', 'slash\ at\ end\/'), # regexes ( 'specials .*', 'specials\ \.\*' ), ( '/.*/', '.*' ), ( 'specials ^ and + /[0-9]+/', 'specials\ \^\ and\ \+\ [0-9]+' ), ( '/a{6}/ but not \/a{6} with /.*?/ more', 'a{6}\ but\ not\ \/a\{6\}\ with\ .*?\ more' ), ( 'not this slash\/ or this one\/', 'not\ this\ slash\\/\ or\ this\ one\\/' ), ( 'not \/, use /\|?/, not \/', 'not\ \\/\,\ use\ \|?\,\ not\ \\/' ), # inception: slashes in our regex. backslashed on input, bare on output ( 'not \/, use /\/?/, not \/', 'not\ \\/\,\ use\ /?\,\ not\ \\/' ), ( 'the /\/?/ more /.*/ stuff', 'the\ /?\ more\ .*\ 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.7.9/tests/test_completion.py0000644000076500000240000002335513142444534021272 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 os import sys import cmd2 import pytest @pytest.fixture def cmd2_app(): c = cmd2.Cmd() return c @pytest.fixture def cs_app(): cmd2.Cmd.case_insensitive = False c = cmd2.Cmd() return c def test_cmd2_command_completion_single_end(cmd2_app): text = 'he' line = 'he' endidx = len(line) begidx = endidx - len(text) # It is at end of line, so extra space is present assert cmd2_app.completenames(text, line, begidx, endidx) == ['help '] def test_cmd2_command_completion_is_case_insensitive_by_default(cmd2_app): text = 'HE' line = 'HE' endidx = len(line) begidx = endidx - len(text) # It is at end of line, so extra space is present assert cmd2_app.completenames(text, line, begidx, endidx) == ['help '] def test_cmd2_case_sensitive_command_completion(cs_app): text = 'HE' line = 'HE' endidx = len(line) begidx = endidx - len(text) # It is at end of line, so extra space is present assert cs_app.completenames(text, line, begidx, endidx) == [] def test_cmd2_command_completion_single_mid(cmd2_app): text = 'he' line = 'he' begidx = 0 endidx = 1 # It is not at end of line, so no extra space assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] def test_cmd2_command_completion_multiple(cmd2_app): text = 'h' line = 'h' endidx = len(line) begidx = endidx - len(text) # It is not at end of line, so no extra space assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history'] def test_cmd2_command_completion_nomatch(cmd2_app): text = 'z' line = 'z' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == [] def test_cmd2_help_completion_single_end(cmd2_app): text = 'he' line = 'help he' endidx = len(line) begidx = endidx - len(text) # Even though it is at end of line, no extra space is present when tab completing a command name to get help on assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] def test_cmd2_help_completion_single_mid(cmd2_app): text = 'he' line = 'help he' begidx = 5 endidx = 6 assert cmd2_app.completenames(text, line, begidx, endidx) == ['help'] def test_cmd2_help_completion_multiple(cmd2_app): text = 'h' line = 'help h' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == ['help', 'history'] def test_cmd2_help_completion_nomatch(cmd2_app): text = 'z' line = 'help z' endidx = len(line) begidx = endidx - len(text) assert cmd2_app.completenames(text, line, begidx, endidx) == [] def test_shell_command_completion(cmd2_app): if sys.platform == "win32": text = 'calc' line = 'shell {}'.format(text) expected = ['calc.exe '] else: text = 'egr' line = '!{}'.format(text) expected = ['egrep '] endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_shell(text, line, begidx, endidx) == expected def test_shell_command_completion_doesnt_match_wildcards(cmd2_app): if sys.platform == "win32": text = 'c*' line = 'shell {}'.format(text) else: text = 'e*' line = '!{}'.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' line = 'shell {}'.format(text) expected = 'calc.exe' else: text = 'l' line = '!{}'.format(text) expected = 'ls' 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 zzzz' 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 = '' if sys.platform == "win32": line = 'shell'.format(text) else: line = '!'.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 = 'c' path = os.path.join(test_dir, text) line = 'shell cat {}'.format(path) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.complete_shell(text, line, begidx, endidx) == ['conftest.py '] def test_path_completion_single_end(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = 'c' path = os.path.join(test_dir, text) line = '!cat {}'.format(path) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.path_complete(text, line, begidx, endidx) == ['conftest.py '] def test_path_completion_single_mid(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = 'tes' path = os.path.join(test_dir, 'c') line = '!cat {}'.format(path) begidx = line.find(text) endidx = begidx + len(text) assert cmd2_app.path_complete(text, line, begidx, endidx) == ['tests' + os.path.sep] def test_path_completion_multiple(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = 's' path = os.path.join(test_dir, text) line = '!cat {}'.format(path) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.path_complete(text, line, begidx, endidx) == ['script.py', 'script.txt', 'scripts' + os.path.sep] def test_path_completion_nomatch(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = 'z' path = os.path.join(test_dir, text) line = '!cat {}'.format(path) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.path_complete(text, line, begidx, endidx) == [] def test_path_completion_cwd(cmd2_app): # Run path complete with no path and no search text text = '' line = '!ls {}'.format(text) endidx = len(line) begidx = endidx - len(text) completions_empty = cmd2_app.path_complete(text, line, begidx, endidx) # Run path complete with path set to the CWD cwd = os.getcwd() line = '!ls {}'.format(cwd) endidx = len(line) begidx = endidx - len(text) completions_cwd = cmd2_app.path_complete(text, line, begidx, endidx) # Verify that the results are the same in both cases and that there is something there assert completions_empty == completions_cwd assert completions_cwd def test_path_completion_doesnt_match_wildcards(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = 'c*' path = os.path.join(test_dir, text) line = '!cat {}'.format(path) 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_user_expansion(cmd2_app): # Run path with just a tilde text = '' if sys.platform.startswith('win'): line = '!dir ~\{}'.format(text) else: line = '!ls ~/{}'.format(text) endidx = len(line) begidx = endidx - len(text) completions_tilde = cmd2_app.path_complete(text, line, begidx, endidx) # Run path complete on the user's home directory user_dir = os.path.expanduser('~') if sys.platform.startswith('win'): line = '!dir {}'.format(user_dir) else: line = '!ls {}'.format(user_dir) endidx = len(line) begidx = endidx - len(text) completions_home = cmd2_app.path_complete(text, line, begidx, endidx) # Verify that the results are the same in both cases assert completions_tilde == completions_home # This next assert fails on AppVeyor Windows containers, but works fine on my Windows 10 VM if not sys.platform.startswith('win'): # Verify that there is something there assert completions_tilde def test_path_completion_directories_only(cmd2_app, request): test_dir = os.path.dirname(request.module.__file__) text = 's' path = os.path.join(test_dir, text) line = '!cat {}'.format(path) endidx = len(line) begidx = endidx - len(text) assert cmd2_app.path_complete(text, line, begidx, endidx, dir_only=True) == ['scripts' + os.path.sep] 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 == None assert args == None assert line == 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_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 cmd2-0.7.9/cmd2.egg-info/0000755000076500000240000000000013223503124016644 5ustar toddleonhardtstaff00000000000000cmd2-0.7.9/cmd2.egg-info/PKG-INFO0000644000076500000240000000616613223503124017752 0ustar toddleonhardtstaff00000000000000Metadata-Version: 1.1 Name: cmd2 Version: 0.7.9 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, case-insensitive, and abbreviated commands - Special-character command shortcuts (beyond cmd's `@` and `!`) - Settable environment parameters - Parsing commands with flags - Unicode character support (*Python 3 only*) - Good tab-completion of 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) 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: Implementation :: PyPy Classifier: Topic :: Software Development :: Libraries :: Python Modules cmd2-0.7.9/cmd2.egg-info/SOURCES.txt0000644000076500000240000000126413223503124020533 0ustar toddleonhardtstaff00000000000000CHANGELOG.md CODE_OF_CONDUCT.md CONTRIBUTING.md LICENSE MANIFEST.in README.md cmd2.py setup.py 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/argparse_example.py examples/event_loops.py examples/example.py examples/exampleSession.txt examples/pirate.py examples/python_scripting.py examples/transcript_regex.txt examples/scripts/arg_printer.py examples/scripts/conditional.py examples/scripts/script.py examples/scripts/script.txt tests/conftest.py tests/redirect.txt tests/script.py tests/script.txt tests/test_cmd2.py tests/test_completion.py tests/test_parsing.py tests/test_transcript.pycmd2-0.7.9/cmd2.egg-info/requires.txt0000644000076500000240000000003713223503124021244 0ustar toddleonhardtstaff00000000000000pyparsing>=2.0.1 pyperclip six cmd2-0.7.9/cmd2.egg-info/top_level.txt0000644000076500000240000000000513223503124021371 0ustar toddleonhardtstaff00000000000000cmd2 cmd2-0.7.9/cmd2.egg-info/dependency_links.txt0000644000076500000240000000000113223503124022712 0ustar toddleonhardtstaff00000000000000 cmd2-0.7.9/MANIFEST.in0000644000076500000240000000006713142602335016072 0ustar toddleonhardtstaff00000000000000include LICENSE include README.md include CHANGELOG.md cmd2-0.7.9/README.md0000755000076500000240000002065713223501503015620 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`) - 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, case-insensitive, and abbreviated commands - Special-character command shortcuts (beyond cmd's `@` and `!`) - Settable environment parameters - Parsing commands with flags - Unicode character support (*Python 3 only*) - Good tab-completion of 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) 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) (on Windows, [pyreadline](https://pypi.python.org/pypi/pyreadline) is an additional dependency). 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 excludeFromHistory attribute. The history is accessed through the `history`, `list`, and `run` commands. If you wish to exclude some of your custom commands from the history, append their names to the list at `Cmd.ExcludeFromHistory`. - Load commands from file, save to file, edit commands in file Type `help load`, `help save`, `help edit` 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). - Case-insensitive commands All commands are case-insensitive, unless ``Cmd.caseInsensitive`` is set to ``False``. - 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 `optparse` options (flags) ```python @options([make_option('-m', '--myoption', action="store_true", help="all about my option")]) def do_myfunc(self, arg, opts): if opts.myoption: #TODO: Do something useful pass ``` See Python standard library's `optparse` documentation: https://docs.python.org/3/library/optparse.html Tutorials --------- A few tutorials on using cmd2 exist: * Florida PyCon 2017 talk: [slides](https://docs.google.com/presentation/d/1LRmpfBt3V-pYQfgQHdczf16F3hcXmhK83tl77R6IJtE) * 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 '''A sample application for cmd2.''' from cmd2 import Cmd, make_option, options, set_use_arg_list class CmdLineApp(Cmd): def __init__(self): self.multilineCommands = ['orate'] self.maxrepeats = 3 # Add stuff to settable and shortcutgs 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) # For option commands, pass a single argument string instead of a list of argument strings to the do_* methods set_use_arg_list(False) @options([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") ]) 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.stdout.write(arg) self.stdout.write('\n') # self.stdout.write is better than "print", because Cmd can be # initialized with a non-standard output destination do_say = do_speak # now "say" is a synonym for "speak" do_orate = do_speak # another synonym, but this one takes multi-line input 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 abbrev: True autorun_on_edit: False 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 embedding them within two forward slashes, `/`. cmd2-0.7.9/setup.py0000755000076500000240000000674013223501503016050 0ustar toddleonhardtstaff00000000000000#!/usr/bin/python # coding=utf-8 """ Setuptools setup file, used to install or test 'cmd2' """ import sys from setuptools import setup VERSION = '0.7.9' 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, case-insensitive, and abbreviated commands - Special-character command shortcuts (beyond cmd's `@` and `!`) - Settable environment parameters - Parsing commands with flags - Unicode character support (*Python 3 only*) - Good tab-completion of 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) 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 :: Implementation :: CPython Programming Language :: Python :: Implementation :: PyPy Topic :: Software Development :: Libraries :: Python Modules """.splitlines()))) INSTALL_REQUIRES = ['pyparsing >= 2.0.1', 'pyperclip', 'six'] if sys.platform.startswith('win'): INSTALL_REQUIRES += ['pyreadline'] # unittest.mock was added in Python 3.3. mock is a backport of unittest.mock to all versions of Python TESTS_REQUIRE = ['mock', 'pytest'] 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, tests_require=TESTS_REQUIRE, ) cmd2-0.7.9/CONTRIBUTING.md0000644000076500000240000004750413223501503016567 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-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 ``` 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. #### Measuring code coverage Code coverage can be measured as follows: ```shell py.test -nauto --cov=cmd2 --cov-report=term-missing --cov-report=html ``` 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.7.9/cmd2.py0000755000076500000240000032215413223501503015535 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", "list", "run") Load commands from file, save to file, edit commands in file Multi-line commands Case-insensitive commands Special-character shortcut commands (beyond cmd's "@" and "!") Settable environment parameters Optional _onchange_{paramname} called when environment parameter changes Parsing commands with `optparse` options (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 cmd import codecs import collections import datetime import glob import io import optparse import os import platform import re import shlex import six import sys import tempfile import traceback import unittest from code import InteractiveConsole from optparse import make_option import pyparsing import pyperclip # 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 # 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 # 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 # BrokenPipeError is only in Python 3. Use IOError for Python 2. if six.PY3: BROKEN_PIPE_ERROR = BrokenPipeError else: BROKEN_PIPE_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.7.9' # 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 commands using @options. # 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 option commands, pass a list of argument strings instead of a single argument string to the do_* methods USE_ARG_LIST = True def set_posix_shlex(val): """ Allows user of cmd2 to choose between POSIX and non-POSIX splitting of args for @options 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 and option args for @option 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 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 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: with open(os.path.expanduser(fname[0])) 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) 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 case_insensitive = True # Commands recognized regardless of case commentGrammars = pyparsing.Or([pyparsing.pythonStyleComment, pyparsing.cStyleComment]) commentInProgress = pyparsing.Literal('/*') + pyparsing.SkipTo(pyparsing.stringEnd ^ '*/') legalChars = u'!#$%.:?@_-' + pyparsing.alphanums + pyparsing.alphas8bit multilineCommands = [] # NOTE: Multiline commands can never be abbreviated, even if abbrev is True prefixParser = pyparsing.Empty() redirector = '>' # for sending output to file shortcuts = {'?': 'help', '!': 'shell', '@': 'load', '@@': '_relative_load'} 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 excludeFromHistory = '''run ru r history histor histo hist his hi h edit edi ed e eof eo eos'''.split() exclude_from_help = ['do_eof', 'do_eos'] # Commands to exclude from the help menu reserved_words = [] # Attributes which ARE dynamically settable at runtime abbrev = False # Abbreviated commands recognized autorun_on_edit = False # Should files automatically run after editing (doesn't apply to commands) 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 = {'abbrev': 'Accept abbreviated commands', 'autorun_on_edit': 'Automatically run files after editing', '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, 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 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 # 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) 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, case_insensitive=self.case_insensitive, blankLinesAllowed=self.blankLinesAllowed, prefixParser=self.prefixParser, preparse=self.preparse, postparse=self.postparse, 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 # ----- 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. We intentionally don't print a warning message here since we know that stdout # will be restored by the _restore_output() method. If you would like your application to print a # warning message, then override this method. pass 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 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 # ----- Methods which override stuff in cmd ----- # noinspection PyMethodOverriding def completenames(self, text, line, begidx, endidx): """Override of cmd2 method which completes command names both for command completion and help.""" command = text if self.case_insensitive: command = text.lower() # Call super class method. Need to do it this way for Python 2 and 3 compatibility cmd_completion = cmd.Cmd.completenames(self, command) # If we are completing the initial command name and get exactly 1 result and are at end of line, add a space if begidx == 0 and len(cmd_completion) == 1 and endidx == len(line): cmd_completion[0] += ' ' return cmd_completion 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 # Expand command shortcuts to the full command name for (shortcut, expansion) in self.shortcuts: if line.startswith(shortcut): line = line.replace(shortcut, expansion + ' ', 1) break i, n = 0, len(line) 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) if statement.parsed.command not in self.excludeFromHistory: self.history.append(statement.parsed.raw) 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: stop = self.onecmd_plus_hooks(self.cmdqueue.pop(0)) 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) # 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 # 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',)) 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 def _func_named(self, arg): """Gets the method name associated with a given command. If self.abbrev is False, it is always just looks for do_arg. However, if self.abbrev is True, it allows abbreviated command names and looks for any commands which start with do_arg. :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 else: if self.abbrev: # accept shortened versions of commands funcs = [func for func in self.keywords if func.startswith(arg) and func not in self.multilineCommands] if len(funcs) == 1: result = 'do_' + funcs[0] 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) 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: try: self.old_completer = readline.get_completer() self.old_delims = readline.get_completer_delims() readline.set_completer(self.complete) # Don't treat "-" as a readline delimiter since it is commonly used in filesystem paths readline.set_completer_delims(self.old_delims.replace('-', '')) 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 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: try: readline.set_completer(self.old_completer) readline.set_completer_delims(self.old_delims) except NameError: pass # 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 # noinspection PyUnusedLocal def do_cmdenvironment(self, args): """Summary report of interactive parameters.""" self.poutput(""" Commands are case-sensitive: {} 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: {} \n""".format(not self.case_insensitive, 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")) def do_help(self, arg): """List available commands with "help" or detailed help with "help cmd".""" if arg: # Getting help for a specific command funcname = self._func_named(arg) if funcname: # No special behavior needed, delegate to cmd base class do_help() cmd.Cmd.do_help(self, funcname[3:]) else: # Show a menu of what commands help can be gotten for self._help_menu() def _help_menu(self): """Show a list of commands which help can be displayed for. """ # Get a list of all method names names = self.get_names() # Remove any command names which are explicitly excluded from the help menu for name in self.exclude_from_help: names.remove(name) cmds_doc = [] cmds_undoc = [] help_dict = {} for name in names: if name[:5] == 'help_': help_dict[name[5:]] = 1 names.sort() # There can be duplicates if routines overridden prevname = '' for name in names: if name[:3] == 'do_': if name == prevname: continue prevname = name command = name[3:] if command in help_dict: cmds_doc.append(command) del help_dict[command] elif getattr(self, name).__doc__: cmds_doc.append(command) else: cmds_undoc.append(command) self.poutput("%s\n" % str(self.doc_leader)) self.print_topics(self.doc_header, cmds_doc, 15, 80) self.print_topics(self.misc_header, list(help_dict.keys()), 15, 80) self.print_topics(self.undoc_header, cmds_undoc, 15, 80) # noinspection PyUnusedLocal def do_shortcuts(self, args): """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)) # noinspection PyUnusedLocal def do_eof(self, arg): """Called when -D is pressed.""" # End of script should not exit app, but -D should. return self._STOP_AND_EXIT def do_quit(self, arg): """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) 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 @options([make_option('-l', '--long', action="store_true", help="describe function of parameter")]) def do_show(self, arg, opts): """Shows value of a parameter.""" # If arguments are being passed as a list instead of as a string if USE_ARG_LIST: if arg: arg = arg[0] else: arg = '' param = arg.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 opts.long: self.poutput('{} # {}'.format(result[p].ljust(maxlen), self.settable[p])) else: self.poutput(result[p]) else: raise LookupError("Parameter '%s' not supported (type 'show' for list of parameters)." % param) def do_set(self, arg): """Sets a settable parameter. Accepts abbreviated parameter names so long as there is no ambiguity. Call without arguments for a list of settable parameters with their values. """ try: statement, param_name, val = arg.parsed.raw.split(None, 2) 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.do_show(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): self.do_show(arg) def do_shell(self, command): """Execute a command as if at the OS prompt. Usage: shell [arguments]""" proc = subprocess.Popen(command, stdout=self.stdout, shell=True) proc.communicate() def path_complete(self, text, line, begidx, endidx, dir_exe_only=False, dir_only=False): """Method called to complete an input line by local file system path completion. :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 indexe 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 """ # Deal with cases like load command and @ key when path completion is immediately after a shortcut for (shortcut, expansion) in self.shortcuts: if line.startswith(shortcut): # If the next character after the shortcut isn't a space, then insert one and adjust indices shortcut_len = len(shortcut) if len(line) == shortcut_len or line[shortcut_len] != ' ': line = line.replace(shortcut, shortcut + ' ', 1) begidx += 1 endidx += 1 break # 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 add_sep_after_tilde = False # If no path and no search text has been entered, then search in the CWD for * if not text and line[begidx - 1] == ' ' and (begidx >= len(line) or line[begidx] == ' '): search_str = os.path.join(os.getcwd(), '*') else: # Parse out the path being searched prev_space_index = line.rfind(' ', 0, begidx) dirname = line[prev_space_index + 1:begidx] # Purposely don't match any path containing wildcards - what we are doing is complicated enough! wildcards = ['*', '?'] for wildcard in wildcards: if wildcard in dirname or wildcard in text: return [] if not dirname: dirname = os.getcwd() elif dirname == '~': # If tilde was used without separator, add a separator after the tilde in the completions add_sep_after_tilde = True # Build the search string search_str = os.path.join(dirname, text + '*') # Expand "~" to the real user directory search_str = os.path.expanduser(search_str) # Find all matching path completions path_completions = glob.glob(search_str) # If we only want directories and executables, filter everything else out first if dir_exe_only: path_completions = [c for c in path_completions if os.path.isdir(c) or os.access(c, os.X_OK)] elif dir_only: path_completions = [c for c in path_completions if os.path.isdir(c)] # Get the basename of the paths completions = [] for c in path_completions: basename = os.path.basename(c) # Add a separator after directories if the next character isn't already a separator if os.path.isdir(c) and add_trailing_sep_if_dir: basename += os.path.sep completions.append(basename) # If there is a single completion if len(completions) == 1: # If it is a file and we are at the end of the line, then add a space for convenience if os.path.isfile(path_completions[0]) and endidx == len(line): completions[0] += ' ' # If tilde was expanded without a separator, prepend one elif os.path.isdir(path_completions[0]) and add_sep_after_tilde: completions[0] = os.path.sep + completions[0] # If there are multiple completions, then sort them alphabetically return sorted(completions) # Enable tab completion of paths for relevant commands complete_edit = path_complete complete_load = path_complete complete_save = path_complete # noinspection PyUnusedLocal @staticmethod def _shell_command_complete(text, line, begidx, endidx): """Method called to complete an input line by environment PATH executable completion. :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 """ # Purposely don't match any executable containing wildcards wildcards = ['*', '?'] for wildcard in wildcards: if wildcard in text: 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)] # Find every executable file in the PATH that matches the pattern exes = [] for path in paths: full_path = os.path.join(path, text) 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.append(os.path.basename(match)) # If there is a single completion and we are at end of the line, then add a space at the end for convenience if len(exes) == 1 and endidx == len(line): exes[0] += ' ' # If there are multiple completions, then sort them alphabetically return sorted(exes) # noinspection PyUnusedLocal def complete_shell(self, text, line, begidx, endidx): """Handles tab completion of executable commands and 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 :return: List[str] - a list of possible tab completions """ # First we strip off the shell command or shortcut key if line.startswith('!'): stripped_line = line.lstrip('!') initial_length = len('!') else: stripped_line = line[len('shell'):] initial_length = len('shell') line_parts = stripped_line.split() # Don't tab complete anything if user only typed shell or ! if not line_parts: return [] # Find the start index of the first thing after the shell or ! cmd_start = line.find(line_parts[0], initial_length) cmd_end = cmd_start + len(line_parts[0]) # Check if we are in the command token if cmd_start <= begidx <= cmd_end: # See if text is part of a path possible_path = line[cmd_start:begidx] # There is nothing to search if len(possible_path) == 0 and not text: return [] if os.path.sep not in possible_path and possible_path != '~': # The text before the search text is not a directory path. # It is OK to try shell command completion. command_completions = self._shell_command_complete(text, line, begidx, endidx) if command_completions: return command_completions # If we have no results, try path completion return self.path_complete(text, line, begidx, endidx, dir_exe_only=True) # Past command token else: # Do path completion return self.path_complete(text, line, begidx, endidx) # noinspection PyBroadException def do_py(self, arg): """ 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 # noinspection PyUnusedLocal @options([], arg_desc=' [script_arguments]') def do_pyscript(self, arg, opts=None): """\nRuns a python script file inside the console 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 arg: self.perror("pyscript command requires at least 1 argument ...", traceback_war=False) self.do_help('pyscript') return if not USE_ARG_LIST: arg = shlex.split(arg, posix=POSIX_SHLEX) # Get the absolute path of the script script_path = os.path.expanduser(arg[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(arg[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 of paths for pyscript command complete_pyscript = path_complete # 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) @options([make_option('-s', '--script', action="store_true", help="Script format; no separation lines"), ], arg_desc='(limit on which commands to include)') def do_history(self, arg, opts): """history [arg]: lists past commands issued | no arg: list all | arg is integer: list one history item, by index | a..b, a:b, a:, ..b -> list history items by a span of indices (inclusive) | arg is string: list all commands matching string search | arg is /enclosed in forward-slashes/: regular expression search """ # If arguments are being passed as a list instead of as a string if USE_ARG_LIST: if arg: arg = arg[0] else: arg = '' # If an argument was supplied, then retrieve partial contents of the history if arg: # If a character indicating a slice is present, retrieve a slice of the history 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 history = self.history # Display the history items retrieved for hi in history: if opts.script: self.poutput(hi) else: self.poutput(hi.pr()) def _last_matching(self, arg): """Return the last item from the history list that matches arg. Or if arg not provided, return last item. If not match is found, return None. :param arg: str - text to search for in history :return: str - last match, last item, or None, depending on arg. """ try: if arg: return self.history.get(arg)[-1] else: return self.history[-1] except IndexError: return None @options([], arg_desc="""[N]|[file_path] * N - Number of command (from history), or `*` for all commands in history (default: last command) * file_path - path to a file to open in editor""") def do_edit(self, arg, opts=None): """Edit a file or command in a text editor. The editor used is determined by the ``editor`` settable parameter. "set editor (program-name)" to change or set the EDITOR environment variable. The optional arguments are mutually exclusive. Either a command number OR a file name can be supplied. If neither is supplied, the most recent command in the history is edited. Edited commands are always run after the editor is closed. Edited files are run on close if the ``autorun_on_edit`` settable parameter is True. """ if not self.editor: raise EnvironmentError("Please use 'set editor' to specify your text editing program of choice.") filename = None if arg and arg[0]: try: # Try to convert argument to an integer history_idx = int(arg[0]) except ValueError: # Argument passed is not convertible to an integer, so treat it as a file path filename = arg[0] history_item = '' else: # Argument passed IS convertible to an integer, so treat it as a history index # Save off original index for pringing orig_indx = history_idx # Convert negative index into equivalent positive one if history_idx < 0: history_idx += len(self.history) + 1 # Make sure the index is actually within the history if 1 <= history_idx <= len(self.history): history_item = self._last_matching(history_idx) else: self.perror('index {!r} does not exist within the history'.format(orig_indx), traceback_war=False) return else: try: history_item = self.history[-1] except IndexError: self.perror('edit must be called with argument if history is empty', traceback_war=False) return delete_tempfile = False if history_item: if filename is None: fd, filename = tempfile.mkstemp(suffix='.txt', text=True) os.close(fd) delete_tempfile = True f = open(os.path.expanduser(filename), 'w') f.write(history_item or '') f.close() os.system('"{}" "{}"'.format(self.editor, filename)) if self.autorun_on_edit or history_item: self.do_load(filename) if delete_tempfile: os.remove(filename) saveparser = (pyparsing.Optional(pyparsing.Word(pyparsing.nums) ^ '*')("idx") + pyparsing.Optional(pyparsing.Word(legalChars + '/\\'))("fname") + pyparsing.stringEnd) def do_save(self, arg): """Saves command(s) from history to file. Usage: save [N] [file_path] * N - Number of command (from history), or `*` for all commands in history (default: last command) * file_path - location to save script of command(s) to (default: value stored in temporary file)""" try: args = self.saveparser.parseString(arg) except pyparsing.ParseException: self.perror('Could not understand save target %s' % arg, traceback_war=False) raise SyntaxError(self.do_save.__doc__) # If a filename was supplied then use that, otherwise use a temp file if args.fname: fname = args.fname else: fd, fname = tempfile.mkstemp(suffix='.txt', text=True) os.close(fd) if args.idx == '*': saveme = '\n\n'.join(self.history[:]) elif args.idx: saveme = self.history[int(args.idx) - 1] else: # Wrap in try to deal with case of empty history try: # Since this save command has already been added to history, need to go one more back for previous saveme = self.history[-2] except IndexError: self.perror('History is empty, nothing to save.', traceback_war=False) return try: f = open(os.path.expanduser(fname), 'w') f.write(saveme) f.close() self.pfeedback('Saved to {}'.format(fname)) except Exception as e: self.perror('Saving {!r} - {}'.format(fname, e), traceback_war=False) @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 def do__relative_load(self, file_path): """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 file_path: self.perror('_relative_load command requires a file path:', traceback_war=False) return file_path = file_path.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() def do_load(self, file_path): """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 file_path: self.perror('load command requires a file path:', traceback_war=False) return expanded_path = os.path.abspath(os.path.expanduser(file_path.strip())) # 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)) def do_run(self, arg): """run [arg]: re-runs an earlier command no arg -> run most recent command arg is integer -> run one history item, by index arg is string -> run most recent command by string search arg is /enclosed in forward-slashes/ -> run most recent by regex""" runme = self._last_matching(arg) self.pfeedback(runme) if runme: return self.onecmd_plus_hooks(runme) @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, case_insensitive, blankLinesAllowed, prefixParser, preparse, postparse, shortcuts): """Creates and uses parsers for user input according to app's parameters.""" self.commentGrammars = commentGrammars self.preparse = preparse self.postparse = postparse self.shortcuts = shortcuts self.main_parser = self._build_main_parser(redirector=redirector, terminators=terminators, multilineCommands=multilineCommands, legalChars=legalChars, commentInProgress=commentInProgress, case_insensitive=case_insensitive, 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, case_insensitive, 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=case_insensitive) 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: x[0].strip())('outputTo')) if case_insensitive: multilineCommand.setParseAction(lambda x: x[0].lower()) oneline_command.setParseAction(lambda x: x[0].lower()) else: 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: '') 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) for (shortcut, expansion) in self.shortcuts: if s.lower().startswith(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""" 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) 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.7.9/examples/0000755000076500000240000000000013223503124016143 5ustar toddleonhardtstaff00000000000000cmd2-0.7.9/examples/event_loops.py0000755000076500000240000000157113110635531021064 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.7.9/examples/python_scripting.py0000755000076500000240000001127313126510553022135 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