diff options
-rw-r--r-- | cmd2/cmd2.py | 18 | ||||
-rw-r--r-- | cmd2/history.py | 27 | ||||
-rwxr-xr-x | examples/cmd_as_argument.py | 3 | ||||
-rw-r--r-- | tests/test_cmd2.py | 14 | ||||
-rw-r--r-- | tests/test_history.py | 31 | ||||
-rw-r--r-- | tests/test_parsing.py | 12 |
6 files changed, 85 insertions, 20 deletions
diff --git a/cmd2/cmd2.py b/cmd2/cmd2.py index 02462d96..8f5a7abe 100644 --- a/cmd2/cmd2.py +++ b/cmd2/cmd2.py @@ -1913,13 +1913,17 @@ class Cmd(cmd.Cmd): :return: parsed command line as a Statement """ used_macros = [] - orig_line = line + orig_line = None # Continue until all macros are resolved while True: # Make sure all input has been read and convert it to a Statement statement = self._complete_statement(line) + # Save the fully entered line if this is the first loop iteration + if orig_line is None: + orig_line = statement.raw + # Check if this command matches a macro and wasn't already processed to avoid an infinite loop if statement.command in self.macros.keys() and statement.command not in used_macros: used_macros.append(statement.command) @@ -3483,11 +3487,13 @@ class Cmd(cmd.Cmd): if rl_type != RlType.NONE: last = None for item in history: - # readline only adds a single entry for multiple sequential identical commands - # so we emulate that behavior here - if item.raw != last: - readline.add_history(item.raw) - last = item.raw + # Break the command into its individual lines + for line in item.raw.splitlines(): + # readline only adds a single entry for multiple sequential identical lines + # so we emulate that behavior here + if line != last: + readline.add_history(line) + last = line # register a function to write history at save # if the history file is in plain text format from 0.9.12 or lower diff --git a/cmd2/history.py b/cmd2/history.py index bbeb9199..0f47d542 100644 --- a/cmd2/history.py +++ b/cmd2/history.py @@ -44,22 +44,23 @@ class HistoryItem(): :return: pretty print string version of a HistoryItem """ if verbose: - ret_str = self._listformat.format(self.idx, self.raw) + ret_str = self._listformat.format(self.idx, self.raw.rstrip()) if self.raw != self.expanded.rstrip(): - ret_str += self._ex_listformat.format(self.idx, self.expanded) + ret_str += self._ex_listformat.format(self.idx, self.expanded.rstrip()) else: - if script: - # display without entry numbers - if expanded or self.statement.multiline_command: - ret_str = self.expanded.rstrip() - else: - ret_str = self.raw.rstrip() + if expanded: + ret_str = self.expanded.rstrip() else: - # display a numbered list - if expanded or self.statement.multiline_command: - ret_str = self._listformat.format(self.idx, self.expanded.rstrip()) - else: - ret_str = self._listformat.format(self.idx, self.raw.rstrip()) + ret_str = self.raw.rstrip() + + # In non-verbose mode, display raw multiline commands on 1 line + if self.statement.multiline_command: + ret_str = ret_str.replace('\n', ' ') + + # Display a numbered list if not writing to a script + if not script: + ret_str = self._listformat.format(self.idx, ret_str) + return ret_str diff --git a/examples/cmd_as_argument.py b/examples/cmd_as_argument.py index 538feac1..1e7901b9 100755 --- a/examples/cmd_as_argument.py +++ b/examples/cmd_as_argument.py @@ -31,8 +31,9 @@ class CmdLineApp(cmd2.Cmd): shortcuts = dict(self.DEFAULT_SHORTCUTS) shortcuts.update({'&': 'speak'}) # Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell - super().__init__(allow_cli_args=False, use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts) + super().__init__(allow_cli_args=False, use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts) + self.locals_in_py = True self.maxrepeats = 3 # Make maxrepeats settable at runtime self.settable['maxrepeats'] = 'max repetitions for speak command' diff --git a/tests/test_cmd2.py b/tests/test_cmd2.py index 0dc8c7c2..72643308 100644 --- a/tests/test_cmd2.py +++ b/tests/test_cmd2.py @@ -1317,6 +1317,20 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app): assert statement.multiline_command == 'orate' assert statement.terminator == ';' +def test_multiline_input_line_to_statement(multiline_app): + # Verify _input_line_to_statement saves the fully entered input line for multiline commands + + # 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=['person', '\n']) + builtins.input = m + + line = 'orate hi' + statement = multiline_app._input_line_to_statement(line) + assert statement.raw == 'orate hi\nperson\n' + assert statement == 'hi person' + assert statement.command == 'orate' + assert statement.multiline_command == 'orate' def test_clipboard_failure(base_app, capsys): # Force cmd2 clipboard to be disabled diff --git a/tests/test_history.py b/tests/test_history.py index 5e01688c..ce915d1a 100644 --- a/tests/test_history.py +++ b/tests/test_history.py @@ -10,6 +10,8 @@ import pytest # Python 3.5 had some regressions in the unitest.mock module, so use # 3rd party mock if available +from cmd2.parsing import StatementParser + try: import mock except ImportError: @@ -262,6 +264,35 @@ def histitem(): histitem = HistoryItem(statement, 1) return histitem +@pytest.fixture +def parser(): + from cmd2.parsing import StatementParser + parser = StatementParser( + allow_redirection=True, + terminators=[';', '&'], + multiline_commands=['multiline'], + aliases={'helpalias': 'help', + '42': 'theanswer', + 'l': '!ls -al', + 'anothermultiline': 'multiline', + 'fake': 'pyscript'}, + shortcuts=[('?', 'help'), ('!', 'shell')] + ) + return parser + +def test_multiline_histitem(parser): + from cmd2.history import History + line = 'multiline foo\nbar\n\n' + statement = parser.parse(line) + history = History() + history.append(statement) + assert len(history) == 1 + hist_item = history[0] + assert hist_item.raw == line + pr_lines = hist_item.pr(verbose=True).splitlines() + assert pr_lines[0].endswith('multiline foo') + assert pr_lines[1] == 'bar' + def test_history_item_instantiate(): from cmd2.parsing import Statement from cmd2.history import HistoryItem diff --git a/tests/test_parsing.py b/tests/test_parsing.py index 5ba02a95..3bd635a1 100644 --- a/tests/test_parsing.py +++ b/tests/test_parsing.py @@ -486,6 +486,18 @@ def test_parse_unfinished_multiliine_command(parser): assert statement.arg_list == statement.argv[1:] assert statement.terminator == '' +def test_parse_basic_multiline_command(parser): + line = 'multiline foo\nbar\n\n' + statement = parser.parse(line) + assert statement.multiline_command == 'multiline' + assert statement.command == 'multiline' + assert statement == 'foo bar' + assert statement.args == statement + assert statement.argv == ['multiline', 'foo', 'bar'] + assert statement.arg_list == ['foo', 'bar'] + assert statement.raw == line + assert statement.terminator == '\n' + @pytest.mark.parametrize('line,terminator',[ ('multiline has > inside;', ';'), ('multiline has > inside;;;', ';'), |