summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG.md3
-rw-r--r--cmd2/cmd2.py35
-rw-r--r--cmd2/parsing.py92
-rw-r--r--docs/argument_processing.rst4
-rwxr-xr-xexamples/decorator_example.py16
-rwxr-xr-xsetup.py45
-rw-r--r--tests/test_parsing.py7
7 files changed, 107 insertions, 95 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3f478cb2..694d2786 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -6,6 +6,9 @@
also be colored.
* `help_error` - the error that prints when no help information can be found
* `default_error` - the error that prints when a non-existent command is run
+ * The `with_argparser` decorators now add the Statement object created when parsing the command line to the
+ `argparse.Namespace` object they pass to the `do_*` methods. It is stored in an attribute called `__statement__`.
+ This can be useful if a command function needs to know the command line for things like logging.
* Potentially breaking changes
* The following commands now write to stderr instead of stdout when printing an error. This will make catching
errors easier in pyscript.
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index d3028961..c1cebdd2 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -49,7 +49,7 @@ from . import utils
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
from .history import History, HistoryItem
-from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split, get_command_arg_list
+from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
# Set up readline
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
@@ -175,9 +175,13 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- parsed_arglist = get_command_arg_list(statement, preserve_quotes)
+ _, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
+
return func(cmd2_instance, parsed_arglist)
+ command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
cmd_wrapper.__doc__ = func.__doc__
return cmd_wrapper
@@ -194,7 +198,10 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve
:param argparser: unique instance of ArgumentParser
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
- :return: function that gets passed argparse-parsed args and a list of unknown argument strings
+ :return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings
+ A member called __statement__ is added to the Namespace to provide command functions access to the
+ Statement object. This can be useful if the command function needs to know the command line.
+
"""
import functools
@@ -202,18 +209,22 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
- parsed_arglist = get_command_arg_list(statement, preserve_quotes)
+ statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
try:
args, unknown = argparser.parse_known_args(parsed_arglist)
except SystemExit:
return
else:
+ setattr(args, '__statement__', statement)
return func(cmd2_instance, args, unknown)
# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
- argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]
+ command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
+ argparser.prog = command_name
# If the description has not been set, then use the method docstring if one exists
if argparser.description is None and func.__doc__:
@@ -237,7 +248,9 @@ def with_argparser(argparser: argparse.ArgumentParser,
:param argparser: unique instance of ArgumentParser
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
- :return: function that gets passed the argparse-parsed args
+ :return: function that gets passed the argparse-parsed args in a Namespace
+ A member called __statement__ is added to the Namespace to provide command functions access to the
+ Statement object. This can be useful if the command function needs to know the command line.
"""
import functools
@@ -245,19 +258,21 @@ def with_argparser(argparser: argparse.ArgumentParser,
def arg_decorator(func: Callable):
@functools.wraps(func)
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
-
- parsed_arglist = get_command_arg_list(statement, preserve_quotes)
-
+ statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
+ statement,
+ preserve_quotes)
try:
args = argparser.parse_args(parsed_arglist)
except SystemExit:
return
else:
+ setattr(args, '__statement__', statement)
return func(cmd2_instance, args)
# argparser defaults the program name to sys.argv[0]
# we want it to be the name of our command
- argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]
+ command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
+ argparser.prog = command_name
# If the description has not been set, then use the method docstring if one exists
if argparser.description is None and func.__doc__:
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index cd81f250..2dc698b0 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -236,34 +236,6 @@ class Statement(str):
return rtn
-def get_command_arg_list(to_parse: Union[Statement, str], preserve_quotes: bool) -> List[str]:
- """
- Called by the argument_list and argparse wrappers to retrieve just the arguments being
- passed to their do_* methods as a list.
-
- :param to_parse: what is being passed to the do_* method. It can be one of two types:
- 1. An already parsed Statement
- 2. An argument string in cases where a do_* method is explicitly called
- e.g.: Calling do_help('alias create') would cause to_parse to be 'alias create'
-
- :param preserve_quotes: if True, then quotes will not be stripped from the arguments
- :return: the arguments in a list
- """
- if isinstance(to_parse, Statement):
- # In the case of a Statement, we already have what we need
- if preserve_quotes:
- return to_parse.arg_list
- else:
- return to_parse.argv[1:]
- else:
- # We have the arguments in a string. Use shlex to split it.
- parsed_arglist = shlex_split(to_parse)
- if not preserve_quotes:
- parsed_arglist = [utils.strip_quotes(arg) for arg in parsed_arglist]
-
- return parsed_arglist
-
-
class StatementParser:
"""Parse raw text into command components.
@@ -382,16 +354,22 @@ class StatementParser:
errmsg = ''
return valid, errmsg
- def tokenize(self, line: str) -> List[str]:
- """Lex a string into a list of tokens.
-
- shortcuts and aliases are expanded and comments are removed
-
- Raises ValueError if there are unclosed quotation marks.
+ def tokenize(self, line: str, expand: bool = True) -> List[str]:
+ """
+ Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed
+
+ :param line: the command line being lexed
+ :param expand: If True, then aliases and shortcuts will be expanded.
+ Set this to False if no expansion should occur because the command name is already known.
+ Otherwise the command could be expanded if it matched an alias name. This is for cases where
+ a do_* method was called manually (e.g do_help('alias').
+ :return: A list of tokens
+ :raises ValueError if there are unclosed quotation marks.
"""
# expand shortcuts and aliases
- line = self._expand(line)
+ if expand:
+ line = self._expand(line)
# check if this line is a comment
if line.strip().startswith(constants.COMMENT_CHAR):
@@ -404,12 +382,19 @@ class StatementParser:
tokens = self._split_on_punctuation(tokens)
return tokens
- def parse(self, line: str) -> Statement:
- """Tokenize the input and parse it into a Statement object, stripping
+ def parse(self, line: str, expand: bool = True) -> Statement:
+ """
+ Tokenize the input and parse it into a Statement object, stripping
comments, expanding aliases and shortcuts, and extracting output
redirection directives.
- Raises ValueError if there are unclosed quotation marks.
+ :param line: the command line being parsed
+ :param expand: If True, then aliases and shortcuts will be expanded.
+ Set this to False if no expansion should occur because the command name is already known.
+ Otherwise the command could be expanded if it matched an alias name. This is for cases where
+ a do_* method was called manually (e.g do_help('alias').
+ :return: A parsed Statement
+ :raises ValueError if there are unclosed quotation marks
"""
# handle the special case/hardcoded terminator of a blank line
@@ -424,7 +409,7 @@ class StatementParser:
arg_list = []
# lex the input into a list of tokens
- tokens = self.tokenize(line)
+ tokens = self.tokenize(line, expand)
# of the valid terminators, find the first one to occur in the input
terminator_pos = len(tokens) + 1
@@ -605,6 +590,35 @@ class StatementParser:
)
return statement
+ def get_command_arg_list(self, command_name: str, to_parse: Union[Statement, str],
+ preserve_quotes: bool) -> Tuple[Statement, List[str]]:
+ """
+ Called by the argument_list and argparse wrappers to retrieve just the arguments being
+ passed to their do_* methods as a list.
+
+ :param command_name: name of the command being run
+ :param to_parse: what is being passed to the do_* method. It can be one of two types:
+ 1. An already parsed Statement
+ 2. An argument string in cases where a do_* method is explicitly called
+ e.g.: Calling do_help('alias create') would cause to_parse to be 'alias create'
+
+ In this case, the string will be converted to a Statement and returned along
+ with the argument list.
+
+ :param preserve_quotes: if True, then quotes will not be stripped from the arguments
+ :return: A tuple containing:
+ The Statement used to retrieve the arguments
+ The argument list
+ """
+ # Check if to_parse needs to be converted to a Statement
+ if not isinstance(to_parse, Statement):
+ to_parse = self.parse(command_name + ' ' + to_parse, expand=False)
+
+ if preserve_quotes:
+ return to_parse, to_parse.arg_list
+ else:
+ return to_parse, to_parse.argv[1:]
+
def _expand(self, line: str) -> str:
"""Expand shortcuts and aliases"""
diff --git a/docs/argument_processing.rst b/docs/argument_processing.rst
index bad683bf..fc1f2433 100644
--- a/docs/argument_processing.rst
+++ b/docs/argument_processing.rst
@@ -9,7 +9,9 @@ Argument Processing
1. Parsing input and quoted strings like the Unix shell
2. Parse the resulting argument list using an instance of ``argparse.ArgumentParser`` that you provide
-3. Passes the resulting ``argparse.Namespace`` object to your command function
+3. Passes the resulting ``argparse.Namespace`` object to your command function. The ``Namespace`` includes the
+ ``Statement`` object that was created when parsing the command line. It is stored in the ``__statement__``
+ attribute of the ``Namespace``.
4. Adds the usage message from the argument parser to your command.
5. Checks if the ``-h/--help`` option is present, and if so, display the help message for the command
diff --git a/examples/decorator_example.py b/examples/decorator_example.py
index cf948d1d..d8088c0a 100755
--- a/examples/decorator_example.py
+++ b/examples/decorator_example.py
@@ -12,6 +12,7 @@ verifying that the output produced matches the transcript.
"""
import argparse
import sys
+from typing import List
import cmd2
@@ -46,7 +47,7 @@ class CmdLineApp(cmd2.Cmd):
speak_parser.add_argument('words', nargs='+', help='words to say')
@cmd2.with_argparser(speak_parser)
- def do_speak(self, args):
+ def do_speak(self, args: argparse.Namespace):
"""Repeats what you tell me to."""
words = []
for word in args.words:
@@ -67,13 +68,18 @@ class CmdLineApp(cmd2.Cmd):
tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
@cmd2.with_argparser(tag_parser)
- def do_tag(self, args):
- """create a html tag"""
+ def do_tag(self, args: argparse.Namespace):
+ """create an html tag"""
+ # The Namespace always includes the Statement object created when parsing the command line
+ statement = args.__statement__
+
+ self.poutput("The command line you ran was: {}".format(statement.command_and_args))
+ self.poutput("It generated this tag:")
self.poutput('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
@cmd2.with_argument_list
- def do_tagg(self, arglist):
- """verion of creating an html tag using arglist instead of argparser"""
+ def do_tagg(self, arglist: List[str]):
+ """version of creating an html tag using arglist instead of argparser"""
if len(arglist) >= 2:
tag = arglist[0]
content = arglist[1:]
diff --git a/setup.py b/setup.py
index fa56b08d..d8775d58 100755
--- a/setup.py
+++ b/setup.py
@@ -3,49 +3,13 @@
"""
Setuptools setup file, used to install or test 'cmd2'
"""
+import codecs
from setuptools import setup
-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.
+DESCRIPTION = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python"
-The latest documentation for cmd2 can be read online here:
-https://cmd2.readthedocs.io/
-
-Main features:
-
- - Searchable command history (`history` command and `<Ctrl>+r`) - optionally persistent
- - Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`)
- - Python scripting of your application with ``pyscript``
- - Run shell commands with ``!``
- - Pipe command output to shell commands with `|`
- - Redirect command output to file with `>`, `>>`
- - Bare `>`, `>>` with no filename send output to paste buffer (clipboard)
- - `py` enters interactive Python console (opt-in `ipy` for IPython console)
- - Option to display long output using a pager with ``cmd2.Cmd.ppaged()``
- - Multi-line commands
- - Special-character command shortcuts (beyond cmd's `?` and `!`)
- - Command aliasing similar to bash `alias` command
- - Macros, which are similar to aliases, but they can contain argument placeholders
- - Ability to load commands at startup from an initialization script
- - Settable environment parameters
- - Parsing commands with arguments using `argparse`, including support for sub-commands
- - Unicode character support
- - Good tab-completion of commands, sub-commands, file system paths, and shell commands
- - Automatic tab-completion of `argparse` flags when using one of the `cmd2` `argparse` decorators
- - Support for Python 3.4+ on Windows, macOS, and Linux
- - Trivial to provide built-in help for all commands
- - Built-in regression testing framework for your applications (transcript-based testing)
- - Transcripts for use with built-in regression can be automatically generated from `history -t`
- - Alerts that seamlessly print while user enters text at prompt
-
-Usable without modification anywhere cmd is used; simply import cmd2.Cmd in place of cmd.Cmd.
-
-Version 0.9.0+ of cmd2 supports Python 3.4+ only. If you wish to use cmd2 with Python 2.7, then
-please install version 0.8.x.
-"""
+with codecs.open('README.md', encoding='utf8') as f:
+ LONG_DESCRIPTION = f.read()
CLASSIFIERS = list(filter(None, map(str.strip,
"""
@@ -90,6 +54,7 @@ setup(
use_scm_version=True,
description=DESCRIPTION,
long_description=LONG_DESCRIPTION,
+ long_description_content_type='text/markdown',
classifiers=CLASSIFIERS,
author='Catherine Devlin',
author_email='catherine.devlin@gmail.com',
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 85ee0765..8cea3305 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -471,11 +471,18 @@ def test_empty_statement_raises_exception():
('l', 'shell', 'ls -al')
])
def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
+ # Test first with expansion
statement = parser.parse(line)
assert statement.command == command
assert statement == args
assert statement.args == statement
+ # Now allow no expansion
+ statement = parser.parse(line, expand=False)
+ assert statement.command == line.split()[0]
+ assert statement.split() == line.split()[1:]
+ assert statement.args == statement
+
def test_parse_alias_on_multiline_command(parser):
line = 'anothermultiline has > inside an unfinished command'
statement = parser.parse(line)