summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTodd Leonhardt <todd.leonhardt@gmail.com>2018-08-09 00:25:57 -0400
committerGitHub <noreply@github.com>2018-08-09 00:25:57 -0400
commit88b739f6a11a2a1796b2758fa2ce94c562e24316 (patch)
tree13a9f8ab97ac1bf2d496ccd3bfd8cfe105064666
parent46955ec2f403a94ca3582fe3225bdcc369f334e1 (diff)
parent34784566f926b57234388150a550ca005f6f3ef9 (diff)
downloadcmd2-git-88b739f6a11a2a1796b2758fa2ce94c562e24316.tar.gz
Merge pull request #496 from python-cmd2/embedded_newlines
Fix #495 by allowing embedded newlines in unclosed quote marks when entering multiline commands
-rw-r--r--cmd2/cmd2.py27
-rw-r--r--cmd2/parsing.py15
-rw-r--r--tests/test_cmd2.py17
-rw-r--r--tests/test_parsing.py29
4 files changed, 79 insertions, 9 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py
index 7273286b..94b75e5f 100644
--- a/cmd2/cmd2.py
+++ b/cmd2/cmd2.py
@@ -1858,8 +1858,30 @@ class Cmd(cmd.Cmd):
pipe runs out. We can't refactor it because we need to retain
backwards compatibility with the standard library version of cmd.
"""
- statement = self.statement_parser.parse(self.preparse(line))
- while statement.multiline_command and not statement.terminator:
+ # preparse() is deprecated, use self.register_postparsing_hook() instead
+ line = self.preparse(line)
+
+ while True:
+ try:
+ statement = self.statement_parser.parse(line)
+ if statement.multiline_command and statement.terminator:
+ # we have a completed multiline command, we are done
+ break
+ if not statement.multiline_command:
+ # it's not a multiline command, but we parsed it ok
+ # so we are done
+ break
+ except ValueError:
+ # we have unclosed quotation marks, lets parse only the command
+ # and see if it's a multiline
+ statement = self.statement_parser.parse_command_only(line)
+ if not statement.multiline_command:
+ # not a multiline command, so raise the exception
+ raise
+
+ # if we get here we must have:
+ # - a multiline command with no terminator
+ # - a multiline command with unclosed quotation marks
if not self.quit_on_sigint:
try:
newline = self.pseudo_raw_input(self.continuation_prompt)
@@ -1885,7 +1907,6 @@ class Cmd(cmd.Cmd):
newline = '\n'
self.poutput(newline)
line = '{}\n{}'.format(statement.raw, newline)
- statement = self.statement_parser.parse(line)
if not statement.command:
raise EmptyStatement()
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index 475554b0..b67cef10 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -407,8 +407,8 @@ class StatementParser:
"""Partially parse input into a Statement object.
The command is identified, and shortcuts and aliases are expanded.
- Terminators, multiline commands, and output redirection are not
- parsed.
+ Multiline commands are identified, but terminators and output
+ redirection are not parsed.
This method is used by tab completion code and therefore must not
generate an exception if there are unclosed quotes.
@@ -420,8 +420,8 @@ class StatementParser:
- args
Different from parse(), this method does not remove redundant whitespace
- within statement.args. It does however, ensure args does not have leading
- or trailing whitespace.
+ within statement.args. It does however, ensure args does not have
+ leading or trailing whitespace.
"""
# expand shortcuts and aliases
line = self._expand(rawinput)
@@ -447,6 +447,12 @@ class StatementParser:
if not command or not args:
args = None
+ # set multiline
+ if command in self.multiline_commands:
+ multiline_command = command
+ else:
+ multiline_command = None
+
# build the statement
# string representation of args must be an empty string instead of
# None for compatibility with standard library cmd
@@ -454,6 +460,7 @@ class StatementParser:
raw=rawinput,
command=command,
args=args,
+ multiline_command=multiline_command,
)
return statement
diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py
index 3324a105..0ec993e9 100644
--- a/tests/test_cmd2.py
+++ b/tests/test_cmd2.py
@@ -1471,7 +1471,8 @@ def test_multiline_complete_empty_statement_raises_exception(multiline_app):
multiline_app._complete_statement('')
def test_multiline_complete_statement_without_terminator(multiline_app):
- # Mock out the input call so we don't actually wait for a user's response on stdin when it looks for more input
+ # Mock out the input call so we don't actually wait for a user's response
+ # on stdin when it looks for more input
m = mock.MagicMock(name='input', return_value='\n')
builtins.input = m
@@ -1481,6 +1482,20 @@ def test_multiline_complete_statement_without_terminator(multiline_app):
statement = multiline_app._complete_statement(line)
assert statement == args
assert statement.command == command
+ assert statement.multiline_command == command
+
+def test_multiline_complete_statement_with_unclosed_quotes(multiline_app):
+ # Mock out the input call so we don't actually wait for a user's response
+ # on stdin when it looks for more input
+ m = mock.MagicMock(name='input', side_effect=['quotes', '" now closed;'])
+ builtins.input = m
+
+ line = 'orate hi "partially open'
+ statement = multiline_app._complete_statement(line)
+ assert statement == 'hi "partially open\nquotes\n" now closed'
+ assert statement.command == 'orate'
+ assert statement.multiline_command == 'orate'
+ assert statement.terminator == ';'
def test_clipboard_failure(base_app, capsys):
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 6e795660..de4c637e 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -376,7 +376,7 @@ def test_parse_multiline_with_complete_comment(parser):
assert statement.argv == ['multiline', 'command', 'is', 'done']
assert statement.terminator == ';'
-def test_parse_multiline_termninated_by_empty_line(parser):
+def test_parse_multiline_terminated_by_empty_line(parser):
line = 'multiline command ends\n\n'
statement = parser.parse(line)
assert statement.multiline_command == 'multiline'
@@ -386,6 +386,23 @@ def test_parse_multiline_termninated_by_empty_line(parser):
assert statement.argv == ['multiline', 'command', 'ends']
assert statement.terminator == '\n'
+@pytest.mark.parametrize('line,terminator',[
+ ('multiline command "with\nembedded newline";', ';'),
+ ('multiline command "with\nembedded newline";;;', ';'),
+ ('multiline command "with\nembedded newline";; ;;', ';'),
+ ('multiline command "with\nembedded newline" &', '&'),
+ ('multiline command "with\nembedded newline" & &', '&'),
+ ('multiline command "with\nembedded newline"\n\n', '\n'),
+])
+def test_parse_multiline_with_embedded_newline(parser, line, terminator):
+ statement = parser.parse(line)
+ assert statement.multiline_command == 'multiline'
+ assert statement.command == 'multiline'
+ assert statement.args == 'command "with\nembedded newline"'
+ assert statement == statement.args
+ assert statement.argv == ['multiline', 'command', 'with\nembedded newline']
+ assert statement.terminator == terminator
+
def test_parse_multiline_ignores_terminators_in_comments(parser):
line = 'multiline command "with term; ends" now\n\n'
statement = parser.parse(line)
@@ -584,6 +601,16 @@ def test_parse_command_only_none(parser, line):
assert statement.args is None
assert statement == ''
+def test_parse_command_only_multiline(parser):
+ line = 'multiline with partially "open quotes and no terminator'
+ statement = parser.parse_command_only(line)
+ assert statement.command == 'multiline'
+ assert statement.multiline_command == 'multiline'
+ assert statement.args == 'with partially "open quotes and no terminator'
+ assert statement == statement.args
+ assert statement.command_and_args == line
+
+
def test_statement_initialization(parser):
string = 'alias'
statement = cmd2.Statement(string)