Skip to content

Commit 3478456

Browse files
committed
Allow newlines inside unclosed quotes. Fixes #495
1 parent ad09ee3 commit 3478456

File tree

2 files changed

+40
-4
lines changed

2 files changed

+40
-4
lines changed

cmd2/cmd2.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1858,8 +1858,30 @@ def _complete_statement(self, line: str) -> Statement:
18581858
pipe runs out. We can't refactor it because we need to retain
18591859
backwards compatibility with the standard library version of cmd.
18601860
"""
1861-
statement = self.statement_parser.parse(self.preparse(line))
1862-
while statement.multiline_command and not statement.terminator:
1861+
# preparse() is deprecated, use self.register_postparsing_hook() instead
1862+
line = self.preparse(line)
1863+
1864+
while True:
1865+
try:
1866+
statement = self.statement_parser.parse(line)
1867+
if statement.multiline_command and statement.terminator:
1868+
# we have a completed multiline command, we are done
1869+
break
1870+
if not statement.multiline_command:
1871+
# it's not a multiline command, but we parsed it ok
1872+
# so we are done
1873+
break
1874+
except ValueError:
1875+
# we have unclosed quotation marks, lets parse only the command
1876+
# and see if it's a multiline
1877+
statement = self.statement_parser.parse_command_only(line)
1878+
if not statement.multiline_command:
1879+
# not a multiline command, so raise the exception
1880+
raise
1881+
1882+
# if we get here we must have:
1883+
# - a multiline command with no terminator
1884+
# - a multiline command with unclosed quotation marks
18631885
if not self.quit_on_sigint:
18641886
try:
18651887
newline = self.pseudo_raw_input(self.continuation_prompt)
@@ -1885,7 +1907,6 @@ def _complete_statement(self, line: str) -> Statement:
18851907
newline = '\n'
18861908
self.poutput(newline)
18871909
line = '{}\n{}'.format(statement.raw, newline)
1888-
statement = self.statement_parser.parse(line)
18891910

18901911
if not statement.command:
18911912
raise EmptyStatement()

tests/test_cmd2.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1471,7 +1471,8 @@ def test_multiline_complete_empty_statement_raises_exception(multiline_app):
14711471
multiline_app._complete_statement('')
14721472

14731473
def test_multiline_complete_statement_without_terminator(multiline_app):
1474-
# Mock out the input call so we don't actually wait for a user's response on stdin when it looks for more input
1474+
# Mock out the input call so we don't actually wait for a user's response
1475+
# on stdin when it looks for more input
14751476
m = mock.MagicMock(name='input', return_value='\n')
14761477
builtins.input = m
14771478

@@ -1481,6 +1482,20 @@ def test_multiline_complete_statement_without_terminator(multiline_app):
14811482
statement = multiline_app._complete_statement(line)
14821483
assert statement == args
14831484
assert statement.command == command
1485+
assert statement.multiline_command == command
1486+
1487+
def test_multiline_complete_statement_with_unclosed_quotes(multiline_app):
1488+
# Mock out the input call so we don't actually wait for a user's response
1489+
# on stdin when it looks for more input
1490+
m = mock.MagicMock(name='input', side_effect=['quotes', '" now closed;'])
1491+
builtins.input = m
1492+
1493+
line = 'orate hi "partially open'
1494+
statement = multiline_app._complete_statement(line)
1495+
assert statement == 'hi "partially open\nquotes\n" now closed'
1496+
assert statement.command == 'orate'
1497+
assert statement.multiline_command == 'orate'
1498+
assert statement.terminator == ';'
14841499

14851500

14861501
def test_clipboard_failure(base_app, capsys):

0 commit comments

Comments
 (0)