Repository: xonsh/xonsh

Issue:
Command cannot use multiple lines values
```
$ git commit -am 'Fixed issue xxx - <hit RETURN key>
fatal: Paths with -a does not make sense.
$
```

When writing git commit message, we sometime need to type into multiple messages for good. But this is break in xonsh when hit Enter.



Discussion:
For git specifically, we could use `git commit <hit ENTER>` for workaround though.

closing this one, since this seems duplicated with #806 

Interesting.  It makes sense that this won't work as written (single-quoted strings can't span multiple lines); I would have thought this would work with Python's multiline strings (with triple-quotes), but that also seems not to work.  I think there is indeed a bug here.

I get really bizarre output for the following:

```
hartz@virtual1 ~/cat-soop default $ echo """hello
```

```
"""hello]
![echo """hello
```

I agree that the multiline strings should probably work in subproc mode


Resolving patch:
diff --git a/xonsh/tools.py b/xonsh/tools.py
--- a/xonsh/tools.py
+++ b/xonsh/tools.py
@@ -319,6 +319,8 @@ def subproc_toks(line, mincol=-1, maxcol=None, lexer=None, returnline=False):
             else:
                 tok.lexpos = len(line)
             break
+        elif check_bad_str_token(tok):
+            return
     else:
         if len(toks) > 0 and toks[-1].type in END_TOK_TYPES:
             if _is_not_lparen_and_rparen(lparens, toks[-1]):
@@ -346,6 +348,44 @@ def subproc_toks(line, mincol=-1, maxcol=None, lexer=None, returnline=False):
     return rtn
 
 
+def check_bad_str_token(tok):
+    """Checks if a token is a bad string."""
+    if tok.type == 'ERRORTOKEN' and tok.value == 'EOF in multi-line string':
+        return True
+    elif isinstance(tok.value, str) and not check_quotes(tok.value):
+        return True
+    else:
+        return False
+
+
+def check_quotes(s):
+    """Checks a string to make sure that if it starts with quotes, it also
+    ends with quotes.
+    """
+    starts_as_str = RE_BEGIN_STRING.match(s) is not None
+    ends_as_str = s.endswith('"') or s.endswith("'")
+    if not starts_as_str and not ends_as_str:
+        ok = True
+    elif starts_as_str and not ends_as_str:
+        ok = False
+    elif not starts_as_str and ends_as_str:
+        ok = False
+    else:
+        m = RE_COMPLETE_STRING.match(s)
+        ok = m is not None
+    return ok
+
+
+def _have_open_triple_quotes(s):
+    if s.count('"""') % 2 == 1:
+        open_triple = '"""'
+    elif s.count("'''") % 2 == 1:
+        open_triple = "'''"
+    else:
+        open_triple = False
+    return open_triple
+
+
 @lazyobject
 def LINE_CONTINUATION():
     """ The line contiuation characters used in subproc mode. In interactive
@@ -367,10 +407,16 @@ def get_logical_line(lines, idx):
     n = 1
     nlines = len(lines)
     line = lines[idx]
-    while line.endswith(str(LINE_CONTINUATION)) and idx < nlines:
+    linecont = str(LINE_CONTINUATION)
+    open_triple = _have_open_triple_quotes(line)
+    while (line.endswith(linecont) or open_triple) and idx < nlines:
         n += 1
         idx += 1
-        line = line[:-1] + lines[idx]
+        if line.endswith(linecont):
+            line = line[:-1] + lines[idx]
+        else:
+            line = line + '\n' + lines[idx]
+        open_triple = _have_open_triple_quotes(line)
     return line, n
 
 
@@ -1538,7 +1584,7 @@ def format_std_prepost(template, env=None):
     return s
 
 
-_RE_STRING_START = "[bBrRuU]*"
+_RE_STRING_START = "[bBprRuU]*"
 _RE_STRING_TRIPLE_DOUBLE = '"""'
 _RE_STRING_TRIPLE_SINGLE = "'''"
 _RE_STRING_DOUBLE = '"'
@@ -1570,6 +1616,13 @@ def format_std_prepost(template, env=None):
 terminating quotes)"""
 
 
+@lazyobject
+def RE_COMPLETE_STRING():
+    ptrn = ('^' + _RE_STRING_START + '(?P<quote>' + "|".join(_STRINGS) + ')' +
+            '.*?(?P=quote)$')
+    return re.compile(ptrn, re.DOTALL)
+
+
 def check_for_partial_string(x):
     """Returns the starting index (inclusive), ending index (exclusive), and
     starting quote string of the most recent Python string found in the input.


Test patch:
diff --git a/tests/test_parser.py b/tests/test_parser.py
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -1795,6 +1795,9 @@ def test_comment_only():
 def test_echo_slash_question():
     check_xonsh_ast({}, '![echo /?]', False)
 
+def test_bad_quotes():
+    with pytest.raises(SyntaxError):
+        check_xonsh_ast({}, '![echo """hello]', False)
 
 def test_redirect():
     assert check_xonsh_ast({}, '$[cat < input.txt]', False)
diff --git a/tests/test_tools.py b/tests/test_tools.py
--- a/tests/test_tools.py
+++ b/tests/test_tools.py
@@ -24,7 +24,7 @@
     is_string_seq, pathsep_to_seq, seq_to_pathsep, is_nonstring_seq_of_strings,
     pathsep_to_upper_seq, seq_to_upper_pathsep, expandvars, is_int_as_str, is_slice_as_str,
     ensure_timestamp, get_portions, is_balanced, subexpr_before_unbalanced,
-    swap_values, get_logical_line, replace_logical_line
+    swap_values, get_logical_line, replace_logical_line, check_quotes,
     )
 from xonsh.environ import Env
 
@@ -208,6 +208,33 @@ def test_subproc_toks_hello_mom_second():
     assert exp == obs
 
 
+def test_subproc_toks_hello_bad_leading_single_quotes():
+    obs = subproc_toks('echo "hello', lexer=LEXER, returnline=True)
+    assert obs is None
+
+
+def test_subproc_toks_hello_bad_trailing_single_quotes():
+    obs = subproc_toks('echo hello"', lexer=LEXER, returnline=True)
+    assert obs is None
+
+
+def test_subproc_toks_hello_bad_leading_triple_quotes():
+    obs = subproc_toks('echo """hello', lexer=LEXER, returnline=True)
+    assert obs is None
+
+
+def test_subproc_toks_hello_bad_trailing_triple_quotes():
+    obs = subproc_toks('echo hello"""', lexer=LEXER, returnline=True)
+    assert obs is None
+
+
+def test_subproc_toks_hello_mom_triple_quotes_nl():
+    s = 'echo """hello\nmom"""'
+    exp = '![{0}]'.format(s)
+    obs = subproc_toks(s, lexer=LEXER, returnline=True)
+    assert exp == obs
+
+
 def test_subproc_toks_comment():
     exp = None
     obs = subproc_toks('# I am a comment', lexer=LEXER, returnline=True)
@@ -333,6 +360,9 @@ def test_subproc_toks_pyeval_redirect():
 14 \\
 + 2
 """, 1, '14 + 2', 2),
+('''x = """wow
+mom"""
+''', 0, 'x = """wow\nmom"""', 2),
 ]
 
 @pytest.mark.parametrize('src, idx, exp_line, exp_n', LOGICAL_LINE_CASES)
@@ -353,6 +383,19 @@ def test_replace_logical_line(src, idx, exp_line, exp_n):
     assert exp == obs
 
 
+@pytest.mark.parametrize('inp, exp', [
+    ('f(1,10),x.y', True),
+    ('"x"', True),
+    ("'y'", True),
+    ('b"x"', True),
+    ("r'y'", True),
+    ('"""hello\nmom"""', True),
+])
+def test_check_quotes(inp, exp):
+    obs = check_quotes(inp)
+    assert exp is obs
+
+
 @pytest.mark.parametrize('inp', [
     'f(1,10),x.y',
 ])