Repository: xonsh/xonsh

Issue:
Syntax errors in subprocess commands containing []
I am trying to run a command which should be getting passed to a subprocess (in this case rake). The command itself includes square brackets (`[]`) which results in a traceback.

The command: `rake so_thing[some,parameters]`
The error:

<details>

```
Traceback (most recent call last):
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/ptk/shell.py", line 137, in _push
    locs=None)
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/execer.py", line 110, in compile
    transform=transform)
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/execer.py", line 79, in parse
    tree, input = self._parse_ctx_free(input, mode=mode, filename=filename)
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/execer.py", line 179, in _parse_ctx_free
    raise original_error from None
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/execer.py", line 166, in _parse_ctx_free
    debug_level=(self.debug_level > 2))
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/parsers/base.py", line 348, in parse
    tree = self.parser.parse(input=s, lexer=self.lexer, debug=debug_level)
  File "/usr/lib/python3.6/site-packages/ply/yacc.py", line 331, in parse
    return self.parseopt_notrack(input, lexer, debug, tracking, tokenfunc)
  File "/usr/lib/python3.6/site-packages/ply/yacc.py", line 1199, in parseopt_notrack
    tok = call_errorfunc(self.errorfunc, errtoken, self)
  File "/usr/lib/python3.6/site-packages/ply/yacc.py", line 193, in call_errorfunc
    r = errorfunc(token)
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/parsers/base.py", line 2726, in p_error
    column=p.lexpos))
  File "/home/tmacey/.local/lib/python3.6/site-packages/xonsh/parsers/base.py", line 479, in _parse_error
    raise err
  File "<string>", line None
SyntaxError: /home/tmacey/.local/lib/python3.6/site-packages/xontrib/jedi.xsh:1:21: ('code: [',)
rake generate_stack[01,dev2-useast1]
                   ^
```

</details>

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**



Discussion:
This also happens with pip+extras.
I assumed that would be the case. I tried wrapping it in a subprocess call (`$[rake do_stuff[foo,bar]]`) but got the same traceback.
The work around would be `rake "do_stuff[foo,bar]"`, but I think this is common enough to be a bug. (Assuming the parser guys think it's doable.)
I was trying to figure out the workaround and was slowly converging to that. Thanks for saving me the effort!
I've found that zsh also raise an error and requires quotes when using brackets:
```python
zsh
pip install hello[world]
# zsh: no matches found: hello[world]
```
But bash and fish works with this kind of arguments. So this looks like a feature.

Resolving patch:
diff --git a/xonsh/parsers/base.py b/xonsh/parsers/base.py
--- a/xonsh/parsers/base.py
+++ b/xonsh/parsers/base.py
@@ -1,5 +1,6 @@
 """Implements the base xonsh parser."""
 
+import itertools
 import os
 import re
 import textwrap
@@ -3496,27 +3497,43 @@ def p_subproc_arg_single(self, p):
         """subproc_arg : subproc_arg_part"""
         p[0] = p[1]
 
+    def _arg_part_combine(self, *arg_parts):
+        """Combines arg_parts. If all arg_parts are strings, concatenate the strings.
+        Otherwise, return a list of arg_parts."""
+        if all(ast.is_const_str(ap) for ap in arg_parts):
+            return ast.const_str(
+                "".join(ap.value for ap in arg_parts),
+                lineno=arg_parts[0].lineno,
+                col_offset=arg_parts[0].col_offset,
+            )
+        else:
+            return list(
+                itertools.chain.from_iterable(
+                    ap if isinstance(ap, list) else [ap] for ap in arg_parts
+                )
+            )
+
     def p_subproc_arg_many(self, p):
         """subproc_arg : subproc_arg subproc_arg_part"""
         # This glues the string together after parsing
+        p[0] = self._arg_part_combine(p[1], p[2])
+
+    def p_subproc_arg_part_brackets(self, p):
+        """subproc_arg_part : lbracket_tok subproc_arg rbracket_tok"""
         p1 = p[1]
         p2 = p[2]
-        if ast.is_const_str(p1) and ast.is_const_str(p2):
-            p0 = ast.const_str(
-                p1.value + p2.value, lineno=p1.lineno, col_offset=p1.col_offset
-            )
-        elif isinstance(p1, list):
-            if isinstance(p2, list):
-                p1.extend(p2)
-            else:
-                p1.append(p2)
-            p0 = p1
-        elif isinstance(p2, list):
-            p2.insert(0, p1)
-            p0 = p2
-        else:
-            p0 = [p1, p2]
-        p[0] = p0
+        p3 = p[3]
+        p1 = ast.const_str(s=p1.value, lineno=p1.lineno, col_offset=p1.lexpos)
+        p3 = ast.const_str(s=p3.value, lineno=p3.lineno, col_offset=p3.lexpos)
+        p[0] = self._arg_part_combine(p1, p2, p3)
+
+    def p_subproc_arg_part_brackets_empty(self, p):
+        """subproc_arg_part : lbracket_tok rbracket_tok"""
+        p1 = p[1]
+        p2 = p[2]
+        p1 = ast.const_str(s=p1.value, lineno=p1.lineno, col_offset=p1.lexpos)
+        p2 = ast.const_str(s=p2.value, lineno=p2.lineno, col_offset=p2.lexpos)
+        p[0] = self._arg_part_combine(p1, p2)
 
     def _attach_subproc_arg_part_rules(self):
         toks = set(self.tokens)


Test patch:
diff --git a/tests/test_parser.py b/tests/test_parser.py
--- a/tests/test_parser.py
+++ b/tests/test_parser.py
@@ -2649,6 +2649,25 @@ def test_echo_slash_question(check_xonsh_ast):
     check_xonsh_ast({}, "![echo /?]", False)
 
 
+@pytest.mark.parametrize(
+    "case",
+    [
+        "[]",
+        "[[]]",
+        "[a]",
+        "[a][b]",
+        "a[b]",
+        "[a]b",
+        "a[b]c",
+        "a[b[c]]",
+        "[a]b[[]c[d,e]f[]g,h]",
+        "[a@([1,2])]@([3,4])",
+    ],
+)
+def test_echo_brackets(case, check_xonsh_ast):
+    check_xonsh_ast({}, f"![echo {case}]")
+
+
 def test_bad_quotes(check_xonsh_ast):
     with pytest.raises(SyntaxError):
         check_xonsh_ast({}, '![echo """hello]', False)