Skip to content

Commit e28bce2

Browse files
authored
Merge branch 'master' into attributes
2 parents 3b6ed11 + 47dce29 commit e28bce2

File tree

7 files changed

+107
-95
lines changed

7 files changed

+107
-95
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@
66
also be colored.
77
* `help_error` - the error that prints when no help information can be found
88
* `default_error` - the error that prints when a non-existent command is run
9+
* The `with_argparser` decorators now add the Statement object created when parsing the command line to the
10+
`argparse.Namespace` object they pass to the `do_*` methods. It is stored in an attribute called `__statement__`.
11+
This can be useful if a command function needs to know the command line for things like logging.
912
* Potentially breaking changes
1013
* The following commands now write to stderr instead of stdout when printing an error. This will make catching
1114
errors easier in pyscript.

cmd2/cmd2.py

Lines changed: 25 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@
4949
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
5050
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
5151
from .history import History, HistoryItem
52-
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split, get_command_arg_list
52+
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split
5353

5454
# Set up readline
5555
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) ->
175175
def arg_decorator(func: Callable):
176176
@functools.wraps(func)
177177
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
178-
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
178+
_, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
179+
statement,
180+
preserve_quotes)
181+
179182
return func(cmd2_instance, parsed_arglist)
180183

184+
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
181185
cmd_wrapper.__doc__ = func.__doc__
182186
return cmd_wrapper
183187

@@ -194,26 +198,33 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve
194198
195199
:param argparser: unique instance of ArgumentParser
196200
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
197-
:return: function that gets passed argparse-parsed args and a list of unknown argument strings
201+
:return: function that gets passed argparse-parsed args in a Namespace and a list of unknown argument strings
202+
A member called __statement__ is added to the Namespace to provide command functions access to the
203+
Statement object. This can be useful if the command function needs to know the command line.
204+
198205
"""
199206
import functools
200207

201208
# noinspection PyProtectedMember
202209
def arg_decorator(func: Callable):
203210
@functools.wraps(func)
204211
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
205-
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
212+
statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
213+
statement,
214+
preserve_quotes)
206215

207216
try:
208217
args, unknown = argparser.parse_known_args(parsed_arglist)
209218
except SystemExit:
210219
return
211220
else:
221+
setattr(args, '__statement__', statement)
212222
return func(cmd2_instance, args, unknown)
213223

214224
# argparser defaults the program name to sys.argv[0]
215225
# we want it to be the name of our command
216-
argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]
226+
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
227+
argparser.prog = command_name
217228

218229
# If the description has not been set, then use the method docstring if one exists
219230
if argparser.description is None and func.__doc__:
@@ -237,27 +248,31 @@ def with_argparser(argparser: argparse.ArgumentParser,
237248
238249
:param argparser: unique instance of ArgumentParser
239250
:param preserve_quotes: if True, then arguments passed to argparse maintain their quotes
240-
:return: function that gets passed the argparse-parsed args
251+
:return: function that gets passed the argparse-parsed args in a Namespace
252+
A member called __statement__ is added to the Namespace to provide command functions access to the
253+
Statement object. This can be useful if the command function needs to know the command line.
241254
"""
242255
import functools
243256

244257
# noinspection PyProtectedMember
245258
def arg_decorator(func: Callable):
246259
@functools.wraps(func)
247260
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
248-
249-
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
250-
261+
statement, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
262+
statement,
263+
preserve_quotes)
251264
try:
252265
args = argparser.parse_args(parsed_arglist)
253266
except SystemExit:
254267
return
255268
else:
269+
setattr(args, '__statement__', statement)
256270
return func(cmd2_instance, args)
257271

258272
# argparser defaults the program name to sys.argv[0]
259273
# we want it to be the name of our command
260-
argparser.prog = func.__name__[len(COMMAND_FUNC_PREFIX):]
274+
command_name = func.__name__[len(COMMAND_FUNC_PREFIX):]
275+
argparser.prog = command_name
261276

262277
# If the description has not been set, then use the method docstring if one exists
263278
if argparser.description is None and func.__doc__:

cmd2/parsing.py

Lines changed: 53 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -236,34 +236,6 @@ def argv(self) -> List[str]:
236236
return rtn
237237

238238

239-
def get_command_arg_list(to_parse: Union[Statement, str], preserve_quotes: bool) -> List[str]:
240-
"""
241-
Called by the argument_list and argparse wrappers to retrieve just the arguments being
242-
passed to their do_* methods as a list.
243-
244-
:param to_parse: what is being passed to the do_* method. It can be one of two types:
245-
1. An already parsed Statement
246-
2. An argument string in cases where a do_* method is explicitly called
247-
e.g.: Calling do_help('alias create') would cause to_parse to be 'alias create'
248-
249-
:param preserve_quotes: if True, then quotes will not be stripped from the arguments
250-
:return: the arguments in a list
251-
"""
252-
if isinstance(to_parse, Statement):
253-
# In the case of a Statement, we already have what we need
254-
if preserve_quotes:
255-
return to_parse.arg_list
256-
else:
257-
return to_parse.argv[1:]
258-
else:
259-
# We have the arguments in a string. Use shlex to split it.
260-
parsed_arglist = shlex_split(to_parse)
261-
if not preserve_quotes:
262-
parsed_arglist = [utils.strip_quotes(arg) for arg in parsed_arglist]
263-
264-
return parsed_arglist
265-
266-
267239
class StatementParser:
268240
"""Parse raw text into command components.
269241
@@ -382,16 +354,22 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
382354
errmsg = ''
383355
return valid, errmsg
384356

385-
def tokenize(self, line: str) -> List[str]:
386-
"""Lex a string into a list of tokens.
387-
388-
shortcuts and aliases are expanded and comments are removed
389-
390-
Raises ValueError if there are unclosed quotation marks.
357+
def tokenize(self, line: str, expand: bool = True) -> List[str]:
358+
"""
359+
Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed
360+
361+
:param line: the command line being lexed
362+
:param expand: If True, then aliases and shortcuts will be expanded.
363+
Set this to False if no expansion should occur because the command name is already known.
364+
Otherwise the command could be expanded if it matched an alias name. This is for cases where
365+
a do_* method was called manually (e.g do_help('alias').
366+
:return: A list of tokens
367+
:raises ValueError if there are unclosed quotation marks.
391368
"""
392369

393370
# expand shortcuts and aliases
394-
line = self._expand(line)
371+
if expand:
372+
line = self._expand(line)
395373

396374
# check if this line is a comment
397375
if line.strip().startswith(constants.COMMENT_CHAR):
@@ -404,12 +382,19 @@ def tokenize(self, line: str) -> List[str]:
404382
tokens = self._split_on_punctuation(tokens)
405383
return tokens
406384

407-
def parse(self, line: str) -> Statement:
408-
"""Tokenize the input and parse it into a Statement object, stripping
385+
def parse(self, line: str, expand: bool = True) -> Statement:
386+
"""
387+
Tokenize the input and parse it into a Statement object, stripping
409388
comments, expanding aliases and shortcuts, and extracting output
410389
redirection directives.
411390
412-
Raises ValueError if there are unclosed quotation marks.
391+
:param line: the command line being parsed
392+
:param expand: If True, then aliases and shortcuts will be expanded.
393+
Set this to False if no expansion should occur because the command name is already known.
394+
Otherwise the command could be expanded if it matched an alias name. This is for cases where
395+
a do_* method was called manually (e.g do_help('alias').
396+
:return: A parsed Statement
397+
:raises ValueError if there are unclosed quotation marks
413398
"""
414399

415400
# handle the special case/hardcoded terminator of a blank line
@@ -424,7 +409,7 @@ def parse(self, line: str) -> Statement:
424409
arg_list = []
425410

426411
# lex the input into a list of tokens
427-
tokens = self.tokenize(line)
412+
tokens = self.tokenize(line, expand)
428413

429414
# of the valid terminators, find the first one to occur in the input
430415
terminator_pos = len(tokens) + 1
@@ -605,6 +590,35 @@ def parse_command_only(self, rawinput: str) -> Statement:
605590
)
606591
return statement
607592

593+
def get_command_arg_list(self, command_name: str, to_parse: Union[Statement, str],
594+
preserve_quotes: bool) -> Tuple[Statement, List[str]]:
595+
"""
596+
Called by the argument_list and argparse wrappers to retrieve just the arguments being
597+
passed to their do_* methods as a list.
598+
599+
:param command_name: name of the command being run
600+
:param to_parse: what is being passed to the do_* method. It can be one of two types:
601+
1. An already parsed Statement
602+
2. An argument string in cases where a do_* method is explicitly called
603+
e.g.: Calling do_help('alias create') would cause to_parse to be 'alias create'
604+
605+
In this case, the string will be converted to a Statement and returned along
606+
with the argument list.
607+
608+
:param preserve_quotes: if True, then quotes will not be stripped from the arguments
609+
:return: A tuple containing:
610+
The Statement used to retrieve the arguments
611+
The argument list
612+
"""
613+
# Check if to_parse needs to be converted to a Statement
614+
if not isinstance(to_parse, Statement):
615+
to_parse = self.parse(command_name + ' ' + to_parse, expand=False)
616+
617+
if preserve_quotes:
618+
return to_parse, to_parse.arg_list
619+
else:
620+
return to_parse, to_parse.argv[1:]
621+
608622
def _expand(self, line: str) -> str:
609623
"""Expand shortcuts and aliases"""
610624

docs/argument_processing.rst

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,9 @@ Argument Processing
99

1010
1. Parsing input and quoted strings like the Unix shell
1111
2. Parse the resulting argument list using an instance of ``argparse.ArgumentParser`` that you provide
12-
3. Passes the resulting ``argparse.Namespace`` object to your command function
12+
3. Passes the resulting ``argparse.Namespace`` object to your command function. The ``Namespace`` includes the
13+
``Statement`` object that was created when parsing the command line. It is stored in the ``__statement__``
14+
attribute of the ``Namespace``.
1315
4. Adds the usage message from the argument parser to your command.
1416
5. Checks if the ``-h/--help`` option is present, and if so, display the help message for the command
1517

examples/decorator_example.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
"""
1313
import argparse
1414
import sys
15+
from typing import List
1516

1617
import cmd2
1718

@@ -46,7 +47,7 @@ def __init__(self, ip_addr=None, port=None, transcript_files=None):
4647
speak_parser.add_argument('words', nargs='+', help='words to say')
4748

4849
@cmd2.with_argparser(speak_parser)
49-
def do_speak(self, args):
50+
def do_speak(self, args: argparse.Namespace):
5051
"""Repeats what you tell me to."""
5152
words = []
5253
for word in args.words:
@@ -67,13 +68,18 @@ def do_speak(self, args):
6768
tag_parser.add_argument('content', nargs='+', help='content to surround with tag')
6869

6970
@cmd2.with_argparser(tag_parser)
70-
def do_tag(self, args):
71-
"""create a html tag"""
71+
def do_tag(self, args: argparse.Namespace):
72+
"""create an html tag"""
73+
# The Namespace always includes the Statement object created when parsing the command line
74+
statement = args.__statement__
75+
76+
self.poutput("The command line you ran was: {}".format(statement.command_and_args))
77+
self.poutput("It generated this tag:")
7278
self.poutput('<{0}>{1}</{0}>'.format(args.tag, ' '.join(args.content)))
7379

7480
@cmd2.with_argument_list
75-
def do_tagg(self, arglist):
76-
"""verion of creating an html tag using arglist instead of argparser"""
81+
def do_tagg(self, arglist: List[str]):
82+
"""version of creating an html tag using arglist instead of argparser"""
7783
if len(arglist) >= 2:
7884
tag = arglist[0]
7985
content = arglist[1:]

setup.py

Lines changed: 5 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,13 @@
33
"""
44
Setuptools setup file, used to install or test 'cmd2'
55
"""
6+
import codecs
67
from setuptools import setup
78

8-
DESCRIPTION = "cmd2 - a tool for building interactive command line applications in Python"
9-
LONG_DESCRIPTION = """cmd2 is a tool for building interactive command line applications in Python. Its goal is to make
10-
it quick and easy for developers to build feature-rich and user-friendly interactive command line applications. It
11-
provides a simple API which is an extension of Python's built-in cmd module. cmd2 provides a wealth of features on top
12-
of cmd to make your life easier and eliminates much of the boilerplate code which would be necessary when using cmd.
9+
DESCRIPTION = "cmd2 - quickly build feature-rich and user-friendly interactive command line applications in Python"
1310

14-
The latest documentation for cmd2 can be read online here:
15-
https://cmd2.readthedocs.io/
16-
17-
Main features:
18-
19-
- Searchable command history (`history` command and `<Ctrl>+r`) - optionally persistent
20-
- Text file scripting of your application with `load` (`@`) and `_relative_load` (`@@`)
21-
- Python scripting of your application with ``pyscript``
22-
- Run shell commands with ``!``
23-
- Pipe command output to shell commands with `|`
24-
- Redirect command output to file with `>`, `>>`
25-
- Bare `>`, `>>` with no filename send output to paste buffer (clipboard)
26-
- `py` enters interactive Python console (opt-in `ipy` for IPython console)
27-
- Option to display long output using a pager with ``cmd2.Cmd.ppaged()``
28-
- Multi-line commands
29-
- Special-character command shortcuts (beyond cmd's `?` and `!`)
30-
- Command aliasing similar to bash `alias` command
31-
- Macros, which are similar to aliases, but they can contain argument placeholders
32-
- Ability to load commands at startup from an initialization script
33-
- Settable environment parameters
34-
- Parsing commands with arguments using `argparse`, including support for sub-commands
35-
- Unicode character support
36-
- Good tab-completion of commands, sub-commands, file system paths, and shell commands
37-
- Automatic tab-completion of `argparse` flags when using one of the `cmd2` `argparse` decorators
38-
- Support for Python 3.4+ on Windows, macOS, and Linux
39-
- Trivial to provide built-in help for all commands
40-
- Built-in regression testing framework for your applications (transcript-based testing)
41-
- Transcripts for use with built-in regression can be automatically generated from `history -t`
42-
- Alerts that seamlessly print while user enters text at prompt
43-
44-
Usable without modification anywhere cmd is used; simply import cmd2.Cmd in place of cmd.Cmd.
45-
46-
Version 0.9.0+ of cmd2 supports Python 3.4+ only. If you wish to use cmd2 with Python 2.7, then
47-
please install version 0.8.x.
48-
"""
11+
with codecs.open('README.md', encoding='utf8') as f:
12+
LONG_DESCRIPTION = f.read()
4913

5014
CLASSIFIERS = list(filter(None, map(str.strip,
5115
"""
@@ -90,6 +54,7 @@
9054
use_scm_version=True,
9155
description=DESCRIPTION,
9256
long_description=LONG_DESCRIPTION,
57+
long_description_content_type='text/markdown',
9358
classifiers=CLASSIFIERS,
9459
author='Catherine Devlin',
9560
author_email='catherine.devlin@gmail.com',

tests/test_parsing.py

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -471,11 +471,18 @@ def test_empty_statement_raises_exception():
471471
('l', 'shell', 'ls -al')
472472
])
473473
def test_parse_alias_and_shortcut_expansion(parser, line, command, args):
474+
# Test first with expansion
474475
statement = parser.parse(line)
475476
assert statement.command == command
476477
assert statement == args
477478
assert statement.args == statement
478479

480+
# Now allow no expansion
481+
statement = parser.parse(line, expand=False)
482+
assert statement.command == line.split()[0]
483+
assert statement.split() == line.split()[1:]
484+
assert statement.args == statement
485+
479486
def test_parse_alias_on_multiline_command(parser):
480487
line = 'anothermultiline has > inside an unfinished command'
481488
statement = parser.parse(line)

0 commit comments

Comments
 (0)