Repository: python-cmd2/cmd2

Issue:
Allow continuous run of terminators to end a multiline command
`test_cmd ;;;;;`   (Results in a new line)

`test_cmd ;;;;;   ;` (Completes the command)

Both of these input lines should complete the command.

Discussion:

Patch:
diff --git a/cmd2/parsing.py b/cmd2/parsing.py
index ce15bd38..655e0c58 100644
--- a/cmd2/parsing.py
+++ b/cmd2/parsing.py
@@ -250,23 +250,27 @@ class StatementParser:
         tokens = self.tokenize(rawinput)
 
         # of the valid terminators, find the first one to occur in the input
-        terminator_pos = len(tokens)+1
-        for test_terminator in self.terminators:
-            try:
-                pos = tokens.index(test_terminator)
-                if pos < terminator_pos:
+        terminator_pos = len(tokens) + 1
+        for pos, cur_token in enumerate(tokens):
+            for test_terminator in self.terminators:
+                if cur_token.startswith(test_terminator):
                     terminator_pos = pos
                     terminator = test_terminator
+                    # break the inner loop, and we want to break the
+                    # outer loop too
                     break
-            except ValueError:
-                # the terminator is not in the tokens
-                pass
+            else:
+                # this else clause is only run if the inner loop
+                # didn't execute a break. If it didn't, then
+                # continue to the next iteration of the outer loop
+                continue
+            # inner loop was broken, break the outer
+            break
 
         if terminator:
             if terminator == LINE_FEED:
                 terminator_pos = len(tokens)+1
-            else:
-                terminator_pos = tokens.index(terminator)
+
             # everything before the first terminator is the command and the args
             argv = tokens[:terminator_pos]
             (command, args) = self._command_and_args(argv)


Test patch:
diff --git a/tests/test_parsing.py b/tests/test_parsing.py
index 41966c71..7b361b7e 100644
--- a/tests/test_parsing.py
+++ b/tests/test_parsing.py
@@ -250,6 +250,23 @@ def test_parse_redirect_inside_terminator(parser):
     assert statement.argv == ['has', '>', 'inside']
     assert statement.terminator == ';'
 
+@pytest.mark.parametrize('line,terminator',[
+    ('multiline with | inside;', ';'),
+    ('multiline with | inside ;', ';'),
+    ('multiline with | inside;;;', ';'),
+    ('multiline with | inside;; ;;', ';'),
+    ('multiline with | inside&', '&'),
+    ('multiline with | inside &;', '&'),
+    ('multiline with | inside&&;', '&'),
+    ('multiline with | inside &; &;', '&'),
+])
+def test_parse_multiple_terminators(parser, line, terminator):
+    statement = parser.parse(line)
+    assert statement.multiline_command == 'multiline'
+    assert statement.args == 'with | inside'
+    assert statement.argv == ['multiline', 'with', '|', 'inside']
+    assert statement.terminator == terminator
+
 def test_parse_unfinished_multiliine_command(parser):
     line = 'multiline has > inside an unfinished command'
     statement = parser.parse(line)
@@ -261,7 +278,10 @@ def test_parse_unfinished_multiliine_command(parser):
 
 @pytest.mark.parametrize('line,terminator',[
     ('multiline has > inside;', ';'),
+    ('multiline has > inside;;;', ';'),
+    ('multiline has > inside;; ;;', ';'),
     ('multiline has > inside &', '&'),
+    ('multiline has > inside & &', '&'),
 ])
 def test_parse_multiline_command_ignores_redirectors_within_it(parser, line, terminator):
     statement = parser.parse(line)
@@ -388,8 +408,15 @@ def test_parse_alias_pipe(parser, line):
     assert not statement.args
     assert statement.pipe_to == ['less']
 
-def test_parse_alias_terminator_no_whitespace(parser):
-    line = 'helpalias;'
+@pytest.mark.parametrize('line', [
+    'helpalias;',
+    'helpalias;;',
+    'helpalias;; ;',
+    'helpalias ;',
+    'helpalias ; ;',
+    'helpalias ;; ;',
+])
+def test_parse_alias_terminator_no_whitespace(parser, line):
     statement = parser.parse(line)
     assert statement.command == 'help'
     assert not statement.args
@@ -448,6 +475,8 @@ def test_parse_command_only_quoted_args(parser):
     'helpalias>>out.txt',
     'help|less',
     'helpalias;',
+    'help ;;',
+    'help; ;;',
 ])
 def test_parse_command_only_specialchars(parser, line):
     statement = parser.parse_command_only(line)
@@ -455,6 +484,11 @@ def test_parse_command_only_specialchars(parser, line):
 
 @pytest.mark.parametrize('line', [
     ';',
+    ';;',
+    ';; ;',
+    '&',
+    '& &',
+    ' && &',
     '>',
     "'",
     '"',