Skip to content

Commit 08ba323

Browse files
authored
Merge pull request #699 from python-cmd2/history_fixes
History fixes
2 parents ddd07f9 + 5446411 commit 08ba323

File tree

7 files changed

+104
-22
lines changed

7 files changed

+104
-22
lines changed

CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
## 0.9.13 (TBD, 2019)
1+
## 0.9.13 (June TBD, 2019)
22
* Bug Fixes
33
* Fixed issue where the wrong terminator was being appended by `Statement.expanded_command_line()`
44
* Fixed issue where aliases and macros could not contain terminator characters in their values
@@ -11,7 +11,8 @@
1111
* Fixed a bug in how line numbers were calculated for transcript testing
1212
* Fixed issue where `_cmdloop()` suppressed exceptions by returning from within its `finally` code
1313
* Fixed UnsupportedOperation on fileno error when a shell command was one of the commands run while generating
14-
a transcript
14+
a transcript
15+
* Fixed bug where history was displaying expanded multiline commands when -x was not specified
1516
* Enhancements
1617
* Added capability to chain pipe commands and redirect their output (e.g. !ls -l | grep user | wc -l > out.txt)
1718
* `pyscript` limits a command's stdout capture to the same period that redirection does.

cmd2/cmd2.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1913,13 +1913,17 @@ def _input_line_to_statement(self, line: str) -> Statement:
19131913
:return: parsed command line as a Statement
19141914
"""
19151915
used_macros = []
1916-
orig_line = line
1916+
orig_line = None
19171917

19181918
# Continue until all macros are resolved
19191919
while True:
19201920
# Make sure all input has been read and convert it to a Statement
19211921
statement = self._complete_statement(line)
19221922

1923+
# Save the fully entered line if this is the first loop iteration
1924+
if orig_line is None:
1925+
orig_line = statement.raw
1926+
19231927
# Check if this command matches a macro and wasn't already processed to avoid an infinite loop
19241928
if statement.command in self.macros.keys() and statement.command not in used_macros:
19251929
used_macros.append(statement.command)
@@ -3483,11 +3487,13 @@ def _initialize_history(self, hist_file):
34833487
if rl_type != RlType.NONE:
34843488
last = None
34853489
for item in history:
3486-
# readline only adds a single entry for multiple sequential identical commands
3487-
# so we emulate that behavior here
3488-
if item.raw != last:
3489-
readline.add_history(item.raw)
3490-
last = item.raw
3490+
# Break the command into its individual lines
3491+
for line in item.raw.splitlines():
3492+
# readline only adds a single entry for multiple sequential identical lines
3493+
# so we emulate that behavior here
3494+
if line != last:
3495+
readline.add_history(line)
3496+
last = line
34913497

34923498
# register a function to write history at save
34933499
# if the history file is in plain text format from 0.9.12 or lower

cmd2/history.py

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -44,22 +44,27 @@ def pr(self, script=False, expanded=False, verbose=False) -> str:
4444
:return: pretty print string version of a HistoryItem
4545
"""
4646
if verbose:
47-
ret_str = self._listformat.format(self.idx, self.raw)
47+
ret_str = self._listformat.format(self.idx, self.raw.rstrip())
4848
if self.raw != self.expanded.rstrip():
49-
ret_str += self._ex_listformat.format(self.idx, self.expanded)
49+
ret_str += self._ex_listformat.format(self.idx, self.expanded.rstrip())
5050
else:
51-
if script:
52-
# display without entry numbers
53-
if expanded or self.statement.multiline_command:
54-
ret_str = self.expanded.rstrip()
55-
else:
56-
ret_str = self.raw.rstrip()
51+
if expanded:
52+
ret_str = self.expanded.rstrip()
5753
else:
58-
# display a numbered list
59-
if expanded or self.statement.multiline_command:
60-
ret_str = self._listformat.format(self.idx, self.expanded.rstrip())
61-
else:
62-
ret_str = self._listformat.format(self.idx, self.raw.rstrip())
54+
ret_str = self.raw.rstrip()
55+
56+
# In non-verbose mode, display raw multiline commands on 1 line
57+
if self.statement.multiline_command:
58+
# This is an approximation and not meant to be a perfect piecing together of lines.
59+
# All newlines will be converted to spaces, including the ones in quoted strings that
60+
# are considered literals. Also if the final line starts with a terminator, then the
61+
# terminator will have an extra space before it in the 1 line version.
62+
ret_str = ret_str.replace('\n', ' ')
63+
64+
# Display a numbered list if not writing to a script
65+
if not script:
66+
ret_str = self._listformat.format(self.idx, ret_str)
67+
6368
return ret_str
6469

6570

examples/cmd_as_argument.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,9 @@ def __init__(self):
3131
shortcuts = dict(self.DEFAULT_SHORTCUTS)
3232
shortcuts.update({'&': 'speak'})
3333
# Set use_ipython to True to enable the "ipy" command which embeds and interactive IPython shell
34-
super().__init__(allow_cli_args=False, use_ipython=False, multiline_commands=['orate'], shortcuts=shortcuts)
34+
super().__init__(allow_cli_args=False, use_ipython=True, multiline_commands=['orate'], shortcuts=shortcuts)
3535

36+
self.locals_in_py = True
3637
self.maxrepeats = 3
3738
# Make maxrepeats settable at runtime
3839
self.settable['maxrepeats'] = 'max repetitions for speak command'

tests/test_cmd2.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1317,6 +1317,20 @@ def test_multiline_complete_statement_with_unclosed_quotes(multiline_app):
13171317
assert statement.multiline_command == 'orate'
13181318
assert statement.terminator == ';'
13191319

1320+
def test_multiline_input_line_to_statement(multiline_app):
1321+
# Verify _input_line_to_statement saves the fully entered input line for multiline commands
1322+
1323+
# Mock out the input call so we don't actually wait for a user's response
1324+
# on stdin when it looks for more input
1325+
m = mock.MagicMock(name='input', side_effect=['person', '\n'])
1326+
builtins.input = m
1327+
1328+
line = 'orate hi'
1329+
statement = multiline_app._input_line_to_statement(line)
1330+
assert statement.raw == 'orate hi\nperson\n'
1331+
assert statement == 'hi person'
1332+
assert statement.command == 'orate'
1333+
assert statement.multiline_command == 'orate'
13201334

13211335
def test_clipboard_failure(base_app, capsys):
13221336
# Force cmd2 clipboard to be disabled

tests/test_history.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@
1010

1111
# Python 3.5 had some regressions in the unitest.mock module, so use
1212
# 3rd party mock if available
13+
from cmd2.parsing import StatementParser
14+
1315
try:
1416
import mock
1517
except ImportError:
@@ -262,6 +264,47 @@ def histitem():
262264
histitem = HistoryItem(statement, 1)
263265
return histitem
264266

267+
@pytest.fixture
268+
def parser():
269+
from cmd2.parsing import StatementParser
270+
parser = StatementParser(
271+
allow_redirection=True,
272+
terminators=[';', '&'],
273+
multiline_commands=['multiline'],
274+
aliases={'helpalias': 'help',
275+
'42': 'theanswer',
276+
'l': '!ls -al',
277+
'anothermultiline': 'multiline',
278+
'fake': 'pyscript'},
279+
shortcuts=[('?', 'help'), ('!', 'shell')]
280+
)
281+
return parser
282+
283+
def test_multiline_histitem(parser):
284+
from cmd2.history import History
285+
line = 'multiline foo\nbar\n\n'
286+
statement = parser.parse(line)
287+
history = History()
288+
history.append(statement)
289+
assert len(history) == 1
290+
hist_item = history[0]
291+
assert hist_item.raw == line
292+
pr_lines = hist_item.pr().splitlines()
293+
assert pr_lines[0].endswith('multiline foo bar')
294+
295+
def test_multiline_histitem_verbose(parser):
296+
from cmd2.history import History
297+
line = 'multiline foo\nbar\n\n'
298+
statement = parser.parse(line)
299+
history = History()
300+
history.append(statement)
301+
assert len(history) == 1
302+
hist_item = history[0]
303+
assert hist_item.raw == line
304+
pr_lines = hist_item.pr(verbose=True).splitlines()
305+
assert pr_lines[0].endswith('multiline foo')
306+
assert pr_lines[1] == 'bar'
307+
265308
def test_history_item_instantiate():
266309
from cmd2.parsing import Statement
267310
from cmd2.history import HistoryItem

tests/test_parsing.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -486,6 +486,18 @@ def test_parse_unfinished_multiliine_command(parser):
486486
assert statement.arg_list == statement.argv[1:]
487487
assert statement.terminator == ''
488488

489+
def test_parse_basic_multiline_command(parser):
490+
line = 'multiline foo\nbar\n\n'
491+
statement = parser.parse(line)
492+
assert statement.multiline_command == 'multiline'
493+
assert statement.command == 'multiline'
494+
assert statement == 'foo bar'
495+
assert statement.args == statement
496+
assert statement.argv == ['multiline', 'foo', 'bar']
497+
assert statement.arg_list == ['foo', 'bar']
498+
assert statement.raw == line
499+
assert statement.terminator == '\n'
500+
489501
@pytest.mark.parametrize('line,terminator',[
490502
('multiline has > inside;', ';'),
491503
('multiline has > inside;;;', ';'),

0 commit comments

Comments
 (0)