Skip to content

Commit d6c6cf3

Browse files
authored
Merge pull request #638 from python-cmd2/common_split
Added a shlex.split() wrapper to have a common way of calling it.
2 parents 04eac4b + 38804d7 commit d6c6cf3

File tree

3 files changed

+60
-52
lines changed

3 files changed

+60
-52
lines changed

cmd2/cmd2.py

Lines changed: 20 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,6 @@
3535
import inspect
3636
import os
3737
import re
38-
import shlex
3938
import sys
4039
import threading
4140
from typing import Any, Callable, Dict, List, Mapping, Optional, Tuple, Type, Union, IO
@@ -48,8 +47,8 @@
4847
from . import utils
4948
from .argparse_completer import AutoCompleter, ACArgumentParser, ACTION_ARG_CHOICES
5049
from .clipboard import can_clip, get_paste_buffer, write_to_paste_buffer
51-
from .parsing import StatementParser, Statement, Macro, MacroArg
5250
from .history import History, HistoryItem
51+
from .parsing import StatementParser, Statement, Macro, MacroArg, shlex_split, get_command_arg_list
5352

5453
# Set up readline
5554
from .rl_utils import rl_type, RlType, rl_get_point, rl_set_prompt, vt100_support, rl_make_safe_prompt
@@ -150,24 +149,6 @@ def categorize(func: Union[Callable, Iterable], category: str) -> None:
150149
setattr(func, HELP_CATEGORY, category)
151150

152151

153-
def parse_quoted_string(string: str, preserve_quotes: bool) -> List[str]:
154-
"""
155-
Parse a quoted string into a list of arguments
156-
:param string: the string being parsed
157-
:param preserve_quotes: if True, then quotes will not be stripped
158-
"""
159-
if isinstance(string, list):
160-
# arguments are already a list, return the list we were passed
161-
lexed_arglist = string
162-
else:
163-
# Use shlex to split the command line into a list of arguments based on shell rules
164-
lexed_arglist = shlex.split(string, comments=False, posix=False)
165-
166-
if not preserve_quotes:
167-
lexed_arglist = [utils.strip_quotes(arg) for arg in lexed_arglist]
168-
return lexed_arglist
169-
170-
171152
def with_category(category: str) -> Callable:
172153
"""A decorator to apply a category to a command function."""
173154
def cat_decorator(func):
@@ -178,8 +159,7 @@ def cat_decorator(func):
178159

179160
def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) -> Callable[[List], Optional[bool]]:
180161
"""A decorator to alter the arguments passed to a do_* cmd2 method. Default passes a string of whatever the user
181-
typed. With this decorator, the decorated method will receive a list of arguments parsed from user input using
182-
shlex.split().
162+
typed. With this decorator, the decorated method will receive a list of arguments parsed from user input.
183163
184164
:param args: Single-element positional argument list containing do_* method this decorator is wrapping
185165
:param preserve_quotes: if True, then argument quotes will not be stripped
@@ -189,9 +169,9 @@ def with_argument_list(*args: List[Callable], preserve_quotes: bool = False) ->
189169

190170
def arg_decorator(func: Callable):
191171
@functools.wraps(func)
192-
def cmd_wrapper(self, cmdline):
193-
lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
194-
return func(self, lexed_arglist)
172+
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
173+
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
174+
return func(cmd2_instance, parsed_arglist)
195175

196176
cmd_wrapper.__doc__ = func.__doc__
197177
return cmd_wrapper
@@ -214,16 +194,17 @@ def with_argparser_and_unknown_args(argparser: argparse.ArgumentParser, preserve
214194
import functools
215195

216196
# noinspection PyProtectedMember
217-
def arg_decorator(func: Callable[[Statement], Optional[bool]]):
197+
def arg_decorator(func: Callable):
218198
@functools.wraps(func)
219-
def cmd_wrapper(instance, cmdline):
220-
lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
199+
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
200+
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
201+
221202
try:
222-
args, unknown = argparser.parse_known_args(lexed_arglist)
203+
args, unknown = argparser.parse_known_args(parsed_arglist)
223204
except SystemExit:
224205
return
225206
else:
226-
return func(instance, args, unknown)
207+
return func(cmd2_instance, args, unknown)
227208

228209
# argparser defaults the program name to sys.argv[0]
229210
# we want it to be the name of our command
@@ -256,16 +237,18 @@ def with_argparser(argparser: argparse.ArgumentParser,
256237
import functools
257238

258239
# noinspection PyProtectedMember
259-
def arg_decorator(func: Callable[[Statement], Optional[bool]]):
240+
def arg_decorator(func: Callable):
260241
@functools.wraps(func)
261-
def cmd_wrapper(instance, cmdline):
262-
lexed_arglist = parse_quoted_string(cmdline, preserve_quotes)
242+
def cmd_wrapper(cmd2_instance, statement: Union[Statement, str]):
243+
244+
parsed_arglist = get_command_arg_list(statement, preserve_quotes)
245+
263246
try:
264-
args = argparser.parse_args(lexed_arglist)
247+
args = argparser.parse_args(parsed_arglist)
265248
except SystemExit:
266249
return
267250
else:
268-
return func(instance, args)
251+
return func(cmd2_instance, args)
269252

270253
# argparser defaults the program name to sys.argv[0]
271254
# we want it to be the name of our command
@@ -742,8 +725,7 @@ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[Li
742725
# Parse the line into tokens
743726
while True:
744727
try:
745-
# Use non-POSIX parsing to keep the quotes around the tokens
746-
initial_tokens = shlex.split(tmp_line[:tmp_endidx], comments=False, posix=False)
728+
initial_tokens = shlex_split(tmp_line[:tmp_endidx])
747729

748730
# If the cursor is at an empty token outside of a quoted string,
749731
# then that is the token being completed. Add it to the list.
@@ -1735,7 +1717,7 @@ def _run_cmdfinalization_hooks(self, stop: bool, statement: Optional[Statement])
17351717
# Fix those annoying problems that occur with terminal programs like "less" when you pipe to them
17361718
if self.stdin.isatty():
17371719
import subprocess
1738-
proc = subprocess.Popen(shlex.split('stty sane'))
1720+
proc = subprocess.Popen(['stty', 'sane'])
17391721
proc.communicate()
17401722

17411723
try:

cmd2/parsing.py

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,24 @@
55
import os
66
import re
77
import shlex
8-
from typing import List, Tuple, Dict
8+
from typing import Dict, List, Tuple, Union
99

1010
import attr
1111

1212
from . import constants
1313
from . import utils
1414

1515

16+
def shlex_split(str_to_split: str) -> List[str]:
17+
"""A wrapper around shlex.split() that uses cmd2's preferred arguments.
18+
19+
This allows other classes to easily call split() the same way StatementParser does
20+
:param str_to_split: the string being split
21+
:return: A list of tokens
22+
"""
23+
return shlex.split(str_to_split, comments=False, posix=False)
24+
25+
1626
@attr.s(frozen=True)
1727
class MacroArg:
1828
"""
@@ -226,6 +236,34 @@ def argv(self) -> List[str]:
226236
return rtn
227237

228238

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+
229267
class StatementParser:
230268
"""Parse raw text into command components.
231269
@@ -349,7 +387,7 @@ def tokenize(self, line: str) -> List[str]:
349387
return []
350388

351389
# split on whitespace
352-
tokens = shlex.split(line, comments=False, posix=False)
390+
tokens = shlex_split(line)
353391

354392
# custom lexing
355393
tokens = self._split_on_punctuation(tokens)

tests/test_argparse.py

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -72,14 +72,6 @@ def do_arglist(self, arglist):
7272
def do_preservelist(self, arglist):
7373
self.stdout.write('{}'.format(arglist))
7474

75-
@cmd2.with_argument_list
76-
@cmd2.with_argument_list
77-
def do_arglisttwice(self, arglist):
78-
if isinstance(arglist, list):
79-
self.stdout.write(' '.join(arglist))
80-
else:
81-
self.stdout.write('False')
82-
8375
known_parser = argparse.ArgumentParser()
8476
known_parser.add_argument('-p', '--piglatin', action='store_true', help='atinLay')
8577
known_parser.add_argument('-s', '--shout', action='store_true', help='N00B EMULATION MODE')
@@ -178,10 +170,6 @@ def test_preservelist(argparse_app):
178170
out = run_cmd(argparse_app, 'preservelist foo "bar baz"')
179171
assert out[0] == "['foo', '\"bar baz\"']"
180172

181-
def test_arglist_decorator_twice(argparse_app):
182-
out = run_cmd(argparse_app, 'arglisttwice "we should" get these')
183-
assert out[0] == 'we should get these'
184-
185173

186174
class SubcommandApp(cmd2.Cmd):
187175
""" Example cmd2 application where we a base command which has a couple sub-commands."""

0 commit comments

Comments
 (0)