Skip to content

Commit 47dce29

Browse files
authored
Merge pull request #649 from python-cmd2/cmd_line
Cmd line
2 parents 0144d8e + cf7cb0c commit 47dce29

File tree

6 files changed

+102
-55
lines changed

6 files changed

+102
-55
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
@@ -174,9 +174,13 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
174174
def arg_decorator(func: Callable):
175175
@functools.wraps(func)
176176
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
177-
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
177+
_, parsed_arglist = cmd2_instance.statement_parser.get_command_arg_list(command_name,
178+
statement,
179+
preserve_quotes)
180+
178181
return func(cmd2_instance, parsed_arglist)
179182

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

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

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

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

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

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

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

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

261276
# If the description has not been set, then use the method docstring if one exists
262277
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
@@ -371,16 +343,22 @@ def is_valid_command(self, word: str) -> Tuple[bool, str]:
371343
errmsg = ''
372344
return valid, errmsg
373345

374-
def tokenize(self, line: str) -> List[str]:
375-
"""Lex a string into a list of tokens.
376-
377-
shortcuts and aliases are expanded and comments are removed
378-
379-
Raises ValueError if there are unclosed quotation marks.
346+
def tokenize(self, line: str, expand: bool = True) -> List[str]:
347+
"""
348+
Lex a string into a list of tokens. Shortcuts and aliases are expanded and comments are removed
349+
350+
:param line: the command line being lexed
351+
:param expand: If True, then aliases and shortcuts will be expanded.
352+
Set this to False if no expansion should occur because the command name is already known.
353+
Otherwise the command could be expanded if it matched an alias name. This is for cases where
354+
a do_* method was called manually (e.g do_help('alias').
355+
:return: A list of tokens
356+
:raises ValueError if there are unclosed quotation marks.
380357
"""
381358

382359
# expand shortcuts and aliases
383-
line = self._expand(line)
360+
if expand:
361+
line = self._expand(line)
384362

385363
# check if this line is a comment
386364
if line.strip().startswith(constants.COMMENT_CHAR):
@@ -393,12 +371,19 @@ def tokenize(self, line: str) -> List[str]:
393371
tokens = self._split_on_punctuation(tokens)
394372
return tokens
395373

396-
def parse(self, line: str) -> Statement:
397-
"""Tokenize the input and parse it into a Statement object, stripping
374+
def parse(self, line: str, expand: bool = True) -> Statement:
375+
"""
376+
Tokenize the input and parse it into a Statement object, stripping
398377
comments, expanding aliases and shortcuts, and extracting output
399378
redirection directives.
400379
401-
Raises ValueError if there are unclosed quotation marks.
380+
:param line: the command line being parsed
381+
:param expand: If True, then aliases and shortcuts will be expanded.
382+
Set this to False if no expansion should occur because the command name is already known.
383+
Otherwise the command could be expanded if it matched an alias name. This is for cases where
384+
a do_* method was called manually (e.g do_help('alias').
385+
:return: A parsed Statement
386+
:raises ValueError if there are unclosed quotation marks
402387
"""
403388

404389
# handle the special case/hardcoded terminator of a blank line
@@ -413,7 +398,7 @@ def parse(self, line: str) -> Statement:
413398
arg_list = []
414399

415400
# lex the input into a list of tokens
416-
tokens = self.tokenize(line)
401+
tokens = self.tokenize(line, expand)
417402

418403
# of the valid terminators, find the first one to occur in the input
419404
terminator_pos = len(tokens) + 1
@@ -594,6 +579,35 @@ def parse_command_only(self, rawinput: str) -> Statement:
594579
)
595580
return statement
596581

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

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:]

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)