Repository: xonsh/xonsh

Issue:
Infinite alias call
I'm trying to override 'ls' command to display dotfiles in my $DOTFILES directory.
This code goes into an endless loop because _ls function calls ls command and it calls _ls function.

```python
def _ls():
    if $(pwd).rstrip(os.linesep) == $DOTFILES:
        ls -Ga
    else:
        ls -G
aliases['ls'] = _ls
```

## xonfig

<details>

```
+------------------+---------------------+
| xonsh            | 0.8.10              |
| Git SHA          | 2cb42bdb            |
| Commit Date      | Feb 6 16:49:16 2019 |
| Python           | 3.6.4               |
| PLY              | 3.11                |
| have readline    | True                |
| prompt toolkit   | 2.0.7               |
| shell type       | prompt_toolkit2     |
| pygments         | 2.3.1               |
| 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     |
+------------------+---------------------+
```

</details>

## Current Behavior
<!--- Tell us what happens instead of the expected behavior -->
<!--- If part of your bug report is a traceback, please first enter debug mode before triggering the error
To enter debug mode, set the environment variableNSH_DEBUG=1` _before_ starting `xonsh`.  `XO
On Linux and OSX, an easy way to to do this is to run `env XONSH_DEBUG=1 xonsh` -->

When I input "ls" command, the terminal freezes.



Discussion:
Hi @miyashiiii, you can replace the inner `ls` calls with `@$(which -s ls)` which should resolve to your actual `ls` executable.
Hi @lacoch. Thank you for your answer.
I knew the way to avoid this issue, but I think the infinite alias call shouldn't happen.
cf. https://stackoverflow.com/questions/56492258/how-to-override-built-in-command-in-xonsh
I should have written it.
Oh, I see! Thanks for the pointer. I don't really think this is a bug, but if @gforsyth thinks so...
> I should have written it.

Yes, perhaps ;)
I asked @miyashiiii to open the issue.  It might not be a bug, but it does seem like a slightly inconsistent behavior to me.  It's a common pattern to alias commands + default arguments to the built-in command name and that definitely works when we define it directly, e.g.

```
gil@badcatredux ~ $ aliases['ls'] = ['ls', '--color=auto', '-v']                              
gil@badcatredux ~ $ ls                                                                        
aur.archlinux.org  github.com
```



Resolving patch:
diff --git a/xonsh/procs/proxies.py b/xonsh/procs/proxies.py
--- a/xonsh/procs/proxies.py
+++ b/xonsh/procs/proxies.py
@@ -500,10 +500,16 @@ def run(self):
             sp_stderr = sys.stderr
         # run the function itself
         try:
+            alias_stack = builtins.__xonsh__.env.get("__ALIAS_STACK", "")
+            if self.env.get("__ALIAS_NAME"):
+                alias_stack += ":" + self.env["__ALIAS_NAME"]
+
             with STDOUT_DISPATCHER.register(sp_stdout), STDERR_DISPATCHER.register(
                 sp_stderr
             ), xt.redirect_stdout(STDOUT_DISPATCHER), xt.redirect_stderr(
                 STDERR_DISPATCHER
+            ), builtins.__xonsh__.env.swap(
+                __ALIAS_STACK=alias_stack
             ):
                 r = self.f(self.args, sp_stdin, sp_stdout, sp_stderr, spec, spec.stack)
         except SystemExit as e:
diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py
--- a/xonsh/procs/specs.py
+++ b/xonsh/procs/specs.py
@@ -355,6 +355,8 @@ def __init__(
         # pure attrs
         self.args = list(cmd)
         self.alias = None
+        self.alias_name = None
+        self.alias_stack = builtins.__xonsh__.env.get("__ALIAS_STACK", "").split(":")
         self.binary_loc = None
         self.is_proxy = False
         self.background = False
@@ -442,6 +444,7 @@ def run(self, *, pipeline_group=None):
         kwargs = {n: getattr(self, n) for n in self.kwnames}
         self.prep_env(kwargs)
         if callable(self.alias):
+            kwargs["env"]["__ALIAS_NAME"] = self.alias_name
             p = self.cls(self.alias, self.cmd, **kwargs)
         else:
             self.prep_preexec_fn(kwargs, pipeline_group=pipeline_group)
@@ -589,17 +592,29 @@ def redirect_trailing(self):
     def resolve_alias(self):
         """Sets alias in command, if applicable."""
         cmd0 = self.cmd[0]
+
+        if cmd0 in self.alias_stack:
+            # Disabling the alias resolving to prevent infinite loop in call stack
+            # and futher using binary_loc to resolve the alias name.
+            self.alias = None
+            return
+
         if callable(cmd0):
             alias = cmd0
         else:
             alias = builtins.aliases.get(cmd0, None)
+            if alias is not None:
+                self.alias_name = cmd0
         self.alias = alias
 
     def resolve_binary_loc(self):
         """Sets the binary location"""
         alias = self.alias
         if alias is None:
-            binary_loc = xenv.locate_binary(self.cmd[0])
+            cmd0 = self.cmd[0]
+            binary_loc = xenv.locate_binary(cmd0)
+            if binary_loc == cmd0 and cmd0 in self.alias_stack:
+                raise Exception(f'Recursive calls to "{cmd0}" alias.')
         elif callable(alias):
             binary_loc = None
         else:


Test patch:
diff --git a/tests/test_integrations.py b/tests/test_integrations.py
--- a/tests/test_integrations.py
+++ b/tests/test_integrations.py
@@ -472,15 +472,57 @@ def _echo(args):
     ),
 ]
 
+UNIX_TESTS = [
+    # testing alias stack: lambda function
+    (
+        """
+def _echo():
+    echo hello
+
+aliases['echo'] = _echo
+echo
+""",
+        "hello\n",
+        0,
+    ),
+    # testing alias stack: ExecAlias
+    (
+        """
+aliases['echo'] = "echo @('hello')"
+echo
+""",
+        "hello\n",
+        0,
+    ),
+    # testing alias stack: callable alias (ExecAlias) + no binary location + infinite loop
+    (
+        """
+aliases['first'] = "second @(1)"
+aliases['second'] = "first @(1)"
+first
+""",
+        lambda out: 'Recursive calls to "first" alias.' in out,
+        0,
+    ),
+]
 
 @skip_if_no_xonsh
 @pytest.mark.parametrize("case", ALL_PLATFORMS)
 def test_script(case):
     script, exp_out, exp_rtn = case
     out, err, rtn = run_xonsh(script)
-    assert exp_out == out
+    if callable(exp_out):
+        assert exp_out(out)
+    else:
+        assert exp_out == out
     assert exp_rtn == rtn
 
+@skip_if_no_xonsh
+@skip_if_on_windows
+@pytest.mark.parametrize("case", UNIX_TESTS)
+def test_unix_tests(case):
+    test_script(case)
+
 
 ALL_PLATFORMS_STDERR = [
     # test redirecting a function alias