Repository: xonsh/xonsh

Issue:
Source-bash doesn't honor nested commands in aliases
A common syntax in bash is to pipe the output from one command into another, such as with the following bash alias:

```
alias gpo="git push --set-upstream origin \$(git rev-parse --abbrev-ref HEAD)"
```

Attempting to run that command after running source-bash on it results in this error:

```
$ gpo
error: unknown option `abbrev-ref'
usage: git push [<options>] [<repository> [<refspec>...]]
...
```

## xonfig

```
+------------------+-----------------+
| xonsh            | 0.7.9           |
| Python           | 3.7.1           |
| PLY              | 3.9             |
| have readline    | True            |
| prompt toolkit   | 2.0.4           |
| shell type       | prompt_toolkit2 |
| pygments         | None            |
| on posix         | True            |
| on linux         | False           |
| on darwin        | True            |
| on windows       | False           |
| on cygwin        | False           |
| on msys2         | False           |
| is superuser     | False           |
| default encoding | utf-8           |
| xonsh encoding   | utf-8           |
| encoding errors  | surrogateescape |
+------------------+-----------------+
```

## Expected Behavior

Xonsh should honor this syntax or declare this syntax as unsupported.

## Replication Steps

```
# from any git repo
$ source-bash 'alias gpo="git push --set-upstream origin \$(git rev-parse --abbrev-ref HEAD)"'
$ gpo
```


Discussion:
A simpler example is:

```console
$ source-bash 'alias eee="echo aaa \$(echo b)"'
$ eee
```
Or even just:

```python
aliases['uuu'] = "echo ccc $(echo ddd)"
```

Resolving patch:
diff --git a/xonsh/aliases.py b/xonsh/aliases.py
--- a/xonsh/aliases.py
+++ b/xonsh/aliases.py
@@ -1,6 +1,7 @@
 # -*- coding: utf-8 -*-
 """Aliases for the xonsh shell."""
 import os
+import re
 import sys
 import inspect
 import argparse
@@ -38,6 +39,11 @@
 import xonsh.xoreutils.which as xxw
 
 
+@lazyobject
+def SUB_EXEC_ALIAS_RE():
+    return re.compile(r"@\(|\$\(|!\(|\$\[|!\[")
+
+
 class Aliases(cabc.MutableMapping):
     """Represents a location to hold and look up aliases."""
 
@@ -121,13 +127,16 @@ def __getitem__(self, key):
 
     def __setitem__(self, key, val):
         if isinstance(val, str):
-            if isexpression(val):
+            f = "<exec-alias:" + key + ">"
+            if SUB_EXEC_ALIAS_RE.search(val) is not None:
+                # We have a sub-command, e.g. $(cmd), to evaluate
+                self._raw[key] = ExecAlias(val, filename=f)
+            elif isexpression(val):
                 # expansion substitution
-                lexer = __xonsh__.execer.parser.lexer
+                lexer = builtins.__xonsh__.execer.parser.lexer
                 self._raw[key] = list(map(strip_simple_quotes, lexer.split(val)))
             else:
                 # need to exec alias
-                f = "<exec-alias:" + key + ">"
                 self._raw[key] = ExecAlias(val, filename=f)
         else:
             self._raw[key] = val
diff --git a/xonsh/foreign_shells.py b/xonsh/foreign_shells.py
--- a/xonsh/foreign_shells.py
+++ b/xonsh/foreign_shells.py
@@ -298,7 +298,7 @@ def foreign_shell_data(
         if use_tmpfile:
             os.remove(tmpfile.name)
     env = parse_env(s)
-    aliases = parse_aliases(s)
+    aliases = parse_aliases(s, shell=shell, sourcer=sourcer, extra_args=extra_args)
     funcs = parse_funcs(s, shell=shell, sourcer=sourcer, extra_args=extra_args)
     aliases.update(funcs)
     return env, aliases
@@ -332,7 +332,12 @@ def ALIAS_RE():
     )
 
 
-def parse_aliases(s):
+@lazyobject
+def FS_EXEC_ALIAS_RE():
+    return re.compile(r";|`|\$\(")
+
+
+def parse_aliases(s, shell, sourcer=None, extra_args=()):
     """Parses the aliases portion of string into a dict."""
     m = ALIAS_RE.search(s)
     if m is None:
@@ -352,7 +357,20 @@ def parse_aliases(s):
             # strip one single quote at the start and end of value
             if value[0] == "'" and value[-1] == "'":
                 value = value[1:-1]
-            value = shlex.split(value)
+            # now compute actual alias
+            if FS_EXEC_ALIAS_RE.search(value) is None:
+                # simple list of args alias
+                value = shlex.split(value)
+            else:
+                # alias is more complex, use ExecAlias, but via shell
+                filename = "<foreign-shell-exec-alias:" + key + ">"
+                value = ForeignShellExecAlias(
+                    src=value,
+                    shell=shell,
+                    filename=filename,
+                    sourcer=sourcer,
+                    extra_args=extra_args,
+                )
         except ValueError as exc:
             warnings.warn(
                 'could not parse alias "{0}": {1!r}'.format(key, exc), RuntimeWarning
@@ -400,7 +418,7 @@ def parse_funcs(s, shell, sourcer=None, extra_args=()):
         if not os.path.isabs(filename):
             filename = os.path.abspath(filename)
         wrapper = ForeignShellFunctionAlias(
-            name=funcname,
+            funcname=funcname,
             shell=shell,
             sourcer=sourcer,
             filename=filename,
@@ -410,19 +428,17 @@ def parse_funcs(s, shell, sourcer=None, extra_args=()):
     return funcs
 
 
-class ForeignShellFunctionAlias(object):
+class ForeignShellBaseAlias(object):
     """This class is responsible for calling foreign shell functions as if
     they were aliases. This does not currently support taking stdin.
     """
 
-    INPUT = '{sourcer} "{filename}"\n' "{funcname} {args}\n"
+    INPUT = "echo ForeignShellBaseAlias {shell} {filename} {args}\n"
 
-    def __init__(self, name, shell, filename, sourcer=None, extra_args=()):
+    def __init__(self, shell, filename, sourcer=None, extra_args=()):
         """
         Parameters
         ----------
-        name : str
-            function name
         shell : str
             Name or path to shell
         filename : str
@@ -433,36 +449,29 @@ def __init__(self, name, shell, filename, sourcer=None, extra_args=()):
             Additional command line options to pass into the shell.
         """
         sourcer = DEFAULT_SOURCERS.get(shell, "source") if sourcer is None else sourcer
-        self.name = name
         self.shell = shell
         self.filename = filename
         self.sourcer = sourcer
         self.extra_args = extra_args
 
+    def _input_kwargs(self):
+        return {
+            "shell": self.shell,
+            "filename": self.filename,
+            "sourcer": self.sourcer,
+            "extra_args": self.extra_args,
+        }
+
     def __eq__(self, other):
-        if (
-            not hasattr(other, "name")
-            or not hasattr(other, "filename")
-            or not hasattr(other, "sourcer")
-            or not hasattr(other, "exta_args")
-        ):
+        if not hasattr(other, "_input_kwargs") or not callable(other._input_kwargs):
             return NotImplemented
-        return (
-            (self.name == other.name)
-            and (self.shell == other.shell)
-            and (self.filename == other.filename)
-            and (self.sourcer == other.sourcer)
-            and (self.extra_args == other.extra_args)
-        )
+        return self._input_kwargs() == other._input_kwargs()
 
-    def __call__(self, args, stdin=None):
+    def __call__(
+        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
+    ):
         args, streaming = self._is_streaming(args)
-        input = self.INPUT.format(
-            sourcer=self.sourcer,
-            filename=self.filename,
-            funcname=self.name,
-            args=" ".join(args),
-        )
+        input = self.INPUT.format(args=" ".join(args), **self._input_kwargs())
         cmd = [self.shell] + list(self.extra_args) + ["-c", input]
         env = builtins.__xonsh__.env
         denv = env.detype()
@@ -478,7 +487,21 @@ def __call__(self, args, stdin=None):
             out = out.replace("\r\n", "\n")
         return out
 
-    def _is_streaming(self, args):
+    def __repr__(self):
+        return (
+            self.__class__.__name__
+            + "("
+            + ", ".join(
+                [
+                    "{k}={v!r}".format(k=k, v=v)
+                    for k, v in sorted(self._input_kwargs().items())
+                ]
+            )
+            + ")"
+        )
+
+    @staticmethod
+    def _is_streaming(args):
         """Test and modify args if --xonsh-stream is present."""
         if "--xonsh-stream" not in args:
             return args, False
@@ -487,6 +510,77 @@ def _is_streaming(self, args):
         return args, True
 
 
+class ForeignShellFunctionAlias(ForeignShellBaseAlias):
+    """This class is responsible for calling foreign shell functions as if
+    they were aliases. This does not currently support taking stdin.
+    """
+
+    INPUT = '{sourcer} "{filename}"\n' "{funcname} {args}\n"
+
+    def __init__(self, funcname, shell, filename, sourcer=None, extra_args=()):
+        """
+        Parameters
+        ----------
+        funcname : str
+            function name
+        shell : str
+            Name or path to shell
+        filename : str
+            Where the function is defined, path to source.
+        sourcer : str or None, optional
+            Command to source foreign files with.
+        extra_args : tuple of str, optional
+            Additional command line options to pass into the shell.
+        """
+        super().__init__(
+            shell=shell, filename=filename, sourcer=sourcer, extra_args=extra_args
+        )
+        self.funcname = funcname
+
+    def _input_kwargs(self):
+        inp = super()._input_kwargs()
+        inp["funcname"] = self.funcname
+        return inp
+
+
+class ForeignShellExecAlias(ForeignShellBaseAlias):
+    """Provides a callable alias for source code in a foreign shell."""
+
+    INPUT = "{src} {args}\n"
+
+    def __init__(
+        self,
+        src,
+        shell,
+        filename="<foreign-shell-exec-alias>",
+        sourcer=None,
+        extra_args=(),
+    ):
+        """
+        Parameters
+        ----------
+        src : str
+            Source code in the shell language
+        shell : str
+            Name or path to shell
+        filename : str
+            Where the function is defined, path to source.
+        sourcer : str or None, optional
+            Command to source foreign files with.
+        extra_args : tuple of str, optional
+            Additional command line options to pass into the shell.
+        """
+        super().__init__(
+            shell=shell, filename=filename, sourcer=sourcer, extra_args=extra_args
+        )
+        self.src = src.strip()
+
+    def _input_kwargs(self):
+        inp = super()._input_kwargs()
+        inp["src"] = self.src
+        return inp
+
+
 @lazyobject
 def VALID_SHELL_PARAMS():
     return frozenset(


Test patch:
diff --git a/tests/test_foreign_shells.py b/tests/test_foreign_shells.py
--- a/tests/test_foreign_shells.py
+++ b/tests/test_foreign_shells.py
@@ -64,7 +64,7 @@ def test_parse_aliases():
         "__XONSH_ALIAS_END__\n"
         "more filth"
     )
-    obs = parse_aliases(s)
+    obs = parse_aliases(s, 'bash')
     assert exp == obs