Skip to content

Commit 54e3014

Browse files
authored
Merge pull request #747 from python-cmd2/allow_redirection_fix
Fixed inconsistent treatment of allow_redirection
2 parents 65875bd + a1ce507 commit 54e3014

File tree

7 files changed

+108
-142
lines changed

7 files changed

+108
-142
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
11
## 0.9.16 (TBD, 2019)
2+
* Bug Fixes
3+
* Fixed inconsistent parsing/tab completion behavior based on the value of `allow_redirection`. This flag is
4+
only meant to be a security setting that prevents redirection of stdout and should not alter parsing logic.
25
* Enhancements
36
* Raise `TypeError` if trying to set choices/completions on argparse action that accepts no arguments
47
* Create directory for the persistent history file if it does not already exist

cmd2/cmd2.py

100644100755
Lines changed: 103 additions & 119 deletions
Original file line numberDiff line numberDiff line change
@@ -353,7 +353,8 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
353353
commands to be run or, if -t is specified, transcript files to run.
354354
This should be set to False if your application parses its own arguments.
355355
:param transcript_files: allow running transcript tests when allow_cli_args is False
356-
:param allow_redirection: should output redirection and pipes be allowed
356+
:param allow_redirection: should output redirection and pipes be allowed. this is only a security setting
357+
and does not alter parsing behavior.
357358
:param multiline_commands: list of commands allowed to accept multi-line input
358359
:param terminators: list of characters that terminate a command. These are mainly intended for terminating
359360
multiline commands, but will also terminate single-line commands. If not supplied, then
@@ -376,11 +377,14 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
376377
# Call super class constructor
377378
super().__init__(completekey=completekey, stdin=stdin, stdout=stdout)
378379

379-
# Attributes which should NOT be dynamically settable at runtime
380+
# Attributes which should NOT be dynamically settable via the set command at runtime
381+
# To prevent a user from altering these with the py/ipy commands, remove locals_in_py from the
382+
# settable dictionary during your applications's __init__ method.
380383
self.default_to_shell = False # Attempt to run unrecognized commands as shell commands
381384
self.quit_on_sigint = False # Quit the loop on interrupt instead of just resetting prompt
385+
self.allow_redirection = allow_redirection # Security setting to prevent redirection of stdout
382386

383-
# Attributes which ARE dynamically settable at runtime
387+
# Attributes which ARE dynamically settable via the set command at runtime
384388
self.continuation_prompt = '> '
385389
self.debug = False
386390
self.echo = False
@@ -440,8 +444,7 @@ def __init__(self, completekey: str = 'tab', stdin=None, stdout=None, *,
440444
# True if running inside a Python script or interactive console, False otherwise
441445
self._in_py = False
442446

443-
self.statement_parser = StatementParser(allow_redirection=allow_redirection,
444-
terminators=terminators,
447+
self.statement_parser = StatementParser(terminators=terminators,
445448
multiline_commands=multiline_commands,
446449
shortcuts=shortcuts)
447450

@@ -616,16 +619,6 @@ def aliases(self) -> Dict[str, str]:
616619
"""Read-only property to access the aliases stored in the StatementParser."""
617620
return self.statement_parser.aliases
618621

619-
@property
620-
def allow_redirection(self) -> bool:
621-
"""Getter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
622-
return self.statement_parser.allow_redirection
623-
624-
@allow_redirection.setter
625-
def allow_redirection(self, value: bool) -> None:
626-
"""Setter for the allow_redirection property that determines whether or not redirection of stdout is allowed."""
627-
self.statement_parser.allow_redirection = value
628-
629622
def poutput(self, msg: Any, *, end: str = '\n') -> None:
630623
"""Print message to self.stdout and appends a newline by default
631624
@@ -831,61 +824,56 @@ def tokens_for_completion(self, line: str, begidx: int, endidx: int) -> Tuple[Li
831824
# Return empty lists since this means the line is malformed.
832825
return [], []
833826

834-
if self.allow_redirection:
827+
# We need to treat redirection characters (|, <, >) as word breaks when they are in unquoted strings.
828+
# Go through each token and further split them on these characters. Each run of redirect characters
829+
# is treated as a single token.
830+
raw_tokens = []
835831

836-
# Since redirection is enabled, we need to treat redirection characters (|, <, >)
837-
# as word breaks when they are in unquoted strings. Go through each token
838-
# and further split them on these characters. Each run of redirect characters
839-
# is treated as a single token.
840-
raw_tokens = []
832+
for cur_initial_token in initial_tokens:
841833

842-
for cur_initial_token in initial_tokens:
834+
# Save tokens up to 1 character in length or quoted tokens. No need to parse these.
835+
if len(cur_initial_token) <= 1 or cur_initial_token[0] in constants.QUOTES:
836+
raw_tokens.append(cur_initial_token)
837+
continue
843838

844-
# Save tokens up to 1 character in length or quoted tokens. No need to parse these.
845-
if len(cur_initial_token) <= 1 or cur_initial_token[0] in constants.QUOTES:
846-
raw_tokens.append(cur_initial_token)
847-
continue
839+
# Iterate over each character in this token
840+
cur_index = 0
841+
cur_char = cur_initial_token[cur_index]
848842

849-
# Iterate over each character in this token
850-
cur_index = 0
851-
cur_char = cur_initial_token[cur_index]
843+
# Keep track of the token we are building
844+
cur_raw_token = ''
852845

853-
# Keep track of the token we are building
854-
cur_raw_token = ''
846+
while True:
847+
if cur_char not in constants.REDIRECTION_CHARS:
855848

856-
while True:
857-
if cur_char not in constants.REDIRECTION_CHARS:
858-
859-
# Keep appending to cur_raw_token until we hit a redirect char
860-
while cur_char not in constants.REDIRECTION_CHARS:
861-
cur_raw_token += cur_char
862-
cur_index += 1
863-
if cur_index < len(cur_initial_token):
864-
cur_char = cur_initial_token[cur_index]
865-
else:
866-
break
849+
# Keep appending to cur_raw_token until we hit a redirect char
850+
while cur_char not in constants.REDIRECTION_CHARS:
851+
cur_raw_token += cur_char
852+
cur_index += 1
853+
if cur_index < len(cur_initial_token):
854+
cur_char = cur_initial_token[cur_index]
855+
else:
856+
break
867857

868-
else:
869-
redirect_char = cur_char
870-
871-
# Keep appending to cur_raw_token until we hit something other than redirect_char
872-
while cur_char == redirect_char:
873-
cur_raw_token += cur_char
874-
cur_index += 1
875-
if cur_index < len(cur_initial_token):
876-
cur_char = cur_initial_token[cur_index]
877-
else:
878-
break
858+
else:
859+
redirect_char = cur_char
860+
861+
# Keep appending to cur_raw_token until we hit something other than redirect_char
862+
while cur_char == redirect_char:
863+
cur_raw_token += cur_char
864+
cur_index += 1
865+
if cur_index < len(cur_initial_token):
866+
cur_char = cur_initial_token[cur_index]
867+
else:
868+
break
879869

880-
# Save the current token
881-
raw_tokens.append(cur_raw_token)
882-
cur_raw_token = ''
870+
# Save the current token
871+
raw_tokens.append(cur_raw_token)
872+
cur_raw_token = ''
883873

884-
# Check if we've viewed all characters
885-
if cur_index >= len(cur_initial_token):
886-
break
887-
else:
888-
raw_tokens = initial_tokens
874+
# Check if we've viewed all characters
875+
if cur_index >= len(cur_initial_token):
876+
break
889877

890878
# Save the unquoted tokens
891879
tokens = [utils.strip_quotes(cur_token) for cur_token in raw_tokens]
@@ -1228,72 +1216,70 @@ def _redirect_complete(self, text: str, line: str, begidx: int, endidx: int, com
12281216
this will be called if we aren't completing for redirection
12291217
:return: a list of possible tab completions
12301218
"""
1231-
if self.allow_redirection:
1232-
1233-
# Get all tokens through the one being completed. We want the raw tokens
1234-
# so we can tell if redirection strings are quoted and ignore them.
1235-
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
1236-
if not raw_tokens:
1237-
return []
1219+
# Get all tokens through the one being completed. We want the raw tokens
1220+
# so we can tell if redirection strings are quoted and ignore them.
1221+
_, raw_tokens = self.tokens_for_completion(line, begidx, endidx)
1222+
if not raw_tokens:
1223+
return []
12381224

1239-
# Must at least have the command
1240-
if len(raw_tokens) > 1:
1225+
# Must at least have the command
1226+
if len(raw_tokens) > 1:
12411227

1242-
# True when command line contains any redirection tokens
1243-
has_redirection = False
1228+
# True when command line contains any redirection tokens
1229+
has_redirection = False
12441230

1245-
# Keep track of state while examining tokens
1246-
in_pipe = False
1247-
in_file_redir = False
1248-
do_shell_completion = False
1249-
do_path_completion = False
1250-
prior_token = None
1231+
# Keep track of state while examining tokens
1232+
in_pipe = False
1233+
in_file_redir = False
1234+
do_shell_completion = False
1235+
do_path_completion = False
1236+
prior_token = None
12511237

1252-
for cur_token in raw_tokens:
1253-
# Process redirection tokens
1254-
if cur_token in constants.REDIRECTION_TOKENS:
1255-
has_redirection = True
1238+
for cur_token in raw_tokens:
1239+
# Process redirection tokens
1240+
if cur_token in constants.REDIRECTION_TOKENS:
1241+
has_redirection = True
12561242

1257-
# Check if we are at a pipe
1258-
if cur_token == constants.REDIRECTION_PIPE:
1259-
# Do not complete bad syntax (e.g cmd | |)
1260-
if prior_token == constants.REDIRECTION_PIPE:
1261-
return []
1243+
# Check if we are at a pipe
1244+
if cur_token == constants.REDIRECTION_PIPE:
1245+
# Do not complete bad syntax (e.g cmd | |)
1246+
if prior_token == constants.REDIRECTION_PIPE:
1247+
return []
12621248

1263-
in_pipe = True
1264-
in_file_redir = False
1249+
in_pipe = True
1250+
in_file_redir = False
12651251

1266-
# Otherwise this is a file redirection token
1267-
else:
1268-
if prior_token in constants.REDIRECTION_TOKENS or in_file_redir:
1269-
# Do not complete bad syntax (e.g cmd | >) (e.g cmd > blah >)
1270-
return []
1252+
# Otherwise this is a file redirection token
1253+
else:
1254+
if prior_token in constants.REDIRECTION_TOKENS or in_file_redir:
1255+
# Do not complete bad syntax (e.g cmd | >) (e.g cmd > blah >)
1256+
return []
12711257

1272-
in_pipe = False
1273-
in_file_redir = True
1258+
in_pipe = False
1259+
in_file_redir = True
12741260

1275-
# Not a redirection token
1276-
else:
1277-
do_shell_completion = False
1278-
do_path_completion = False
1261+
# Not a redirection token
1262+
else:
1263+
do_shell_completion = False
1264+
do_path_completion = False
12791265

1280-
if prior_token == constants.REDIRECTION_PIPE:
1281-
do_shell_completion = True
1282-
elif in_pipe or prior_token in (constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND):
1283-
do_path_completion = True
1266+
if prior_token == constants.REDIRECTION_PIPE:
1267+
do_shell_completion = True
1268+
elif in_pipe or prior_token in (constants.REDIRECTION_OUTPUT, constants.REDIRECTION_APPEND):
1269+
do_path_completion = True
12841270

1285-
prior_token = cur_token
1271+
prior_token = cur_token
12861272

1287-
if do_shell_completion:
1288-
return self.shell_cmd_complete(text, line, begidx, endidx)
1273+
if do_shell_completion:
1274+
return self.shell_cmd_complete(text, line, begidx, endidx)
12891275

1290-
elif do_path_completion:
1291-
return self.path_complete(text, line, begidx, endidx)
1276+
elif do_path_completion:
1277+
return self.path_complete(text, line, begidx, endidx)
12921278

1293-
# If there were redirection strings anywhere on the command line, then we
1294-
# are no longer tab completing for the current command
1295-
elif has_redirection:
1296-
return []
1279+
# If there were redirection strings anywhere on the command line, then we
1280+
# are no longer tab completing for the current command
1281+
elif has_redirection:
1282+
return []
12971283

12981284
# Call the command's completer function
12991285
return compfunc(text, line, begidx, endidx)
@@ -2313,12 +2299,10 @@ def _set_up_cmd2_readline(self) -> _SavedReadlineSettings:
23132299
readline_settings.completer = readline.get_completer()
23142300
readline.set_completer(self.complete)
23152301

2316-
# Break words on whitespace and quotes when tab completing
2317-
completer_delims = " \t\n" + ''.join(constants.QUOTES)
2318-
2319-
if self.allow_redirection:
2320-
# If redirection is allowed, then break words on those characters too
2321-
completer_delims += ''.join(constants.REDIRECTION_CHARS)
2302+
# Break words on whitespace, quotes, and redirectors when tab completing
2303+
completer_delims = " \t\n"
2304+
completer_delims += ''.join(constants.QUOTES)
2305+
completer_delims += ''.join(constants.REDIRECTION_CHARS)
23222306

23232307
readline_settings.delims = readline.get_completer_delims()
23242308
readline.set_completer_delims(completer_delims)

cmd2/parsing.py

100644100755
Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,6 @@ class StatementParser:
245245
the expansion.
246246
"""
247247
def __init__(self,
248-
allow_redirection: bool = True,
249248
terminators: Optional[Iterable[str]] = None,
250249
multiline_commands: Optional[Iterable[str]] = None,
251250
aliases: Optional[Dict[str, str]] = None,
@@ -257,13 +256,11 @@ def __init__(self,
257256
* multiline commands
258257
* shortcuts
259258
260-
:param allow_redirection: should redirection and pipes be allowed?
261259
:param terminators: iterable containing strings which should terminate commands
262260
:param multiline_commands: iterable containing the names of commands that accept multiline input
263261
:param aliases: dictionary containing aliases
264262
:param shortcuts: dictionary containing shortcuts
265263
"""
266-
self.allow_redirection = allow_redirection
267264
if terminators is None:
268265
self.terminators = (constants.MULTILINE_TERMINATOR,)
269266
else:
@@ -690,8 +687,7 @@ def _split_on_punctuation(self, tokens: List[str]) -> List[str]:
690687
"""
691688
punctuation = []
692689
punctuation.extend(self.terminators)
693-
if self.allow_redirection:
694-
punctuation.extend(constants.REDIRECTION_CHARS)
690+
punctuation.extend(constants.REDIRECTION_CHARS)
695691

696692
punctuated_tokens = []
697693

tests/test_cmd2.py

100644100755
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -558,7 +558,7 @@ def test_feedback_to_output_false(base_app):
558558

559559
def test_disallow_redirection(base_app):
560560
# Set allow_redirection to False
561-
base_app.statement_parser.allow_redirection = False
561+
base_app.allow_redirection = False
562562

563563
filename = 'test_allow_redirect.txt'
564564

tests/test_completion.py

100644100755
Lines changed: 0 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -695,7 +695,6 @@ def test_tokens_for_completion_redirect(cmd2_app):
695695
endidx = len(line)
696696
begidx = endidx - len(text)
697697

698-
cmd2_app.allow_redirection = True
699698
expected_tokens = ['command', '|', '<', '>>', 'file']
700699
expected_raw_tokens = ['command', '|', '<', '>>', 'file']
701700

@@ -717,20 +716,6 @@ def test_tokens_for_completion_quoted_redirect(cmd2_app):
717716
assert expected_tokens == tokens
718717
assert expected_raw_tokens == raw_tokens
719718

720-
def test_tokens_for_completion_redirect_off(cmd2_app):
721-
text = '>file'
722-
line = 'command {}'.format(text)
723-
endidx = len(line)
724-
begidx = endidx - len(text)
725-
726-
cmd2_app.statement_parser.allow_redirection = False
727-
expected_tokens = ['command', '>file']
728-
expected_raw_tokens = ['command', '>file']
729-
730-
tokens, raw_tokens = cmd2_app.tokens_for_completion(line, begidx, endidx)
731-
assert expected_tokens == tokens
732-
assert expected_raw_tokens == raw_tokens
733-
734719
def test_add_opening_quote_basic_no_text(cmd2_app):
735720
text = ''
736721
line = 'test_basic {}'.format(text)

tests/test_history.py

100644100755
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -268,7 +268,6 @@ def histitem():
268268
def parser():
269269
from cmd2.parsing import StatementParser
270270
parser = StatementParser(
271-
allow_redirection=True,
272271
terminators=[';', '&'],
273272
multiline_commands=['multiline'],
274273
aliases={'helpalias': 'help',

tests/test_parsing.py

100644100755
Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
@pytest.fixture
1414
def parser():
1515
parser = StatementParser(
16-
allow_redirection=True,
1716
terminators=[';', '&'],
1817
multiline_commands=['multiline'],
1918
aliases={'helpalias': 'help',

0 commit comments

Comments
 (0)