diff options
-rw-r--r-- | cmd2/cmd2.py | 27 | ||||
-rw-r--r-- | cmd2/parsing.py | 15 | ||||
-rw-r--r-- | tests/test_cmd2.py | 17 | ||||
-rw-r--r-- | tests/test_parsing.py | 29 |
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) |