Repository: xonsh/xonsh
Pull request: Alias that returns  modified command
### Motivation

After deep dive into [How make sudo expand aliases?](https://github.com/xonsh/xonsh/issues/2618) I understood that we want to modify command before execution but we can't achieve this because all we can is to use callable alias to wrap the command into it. But:
* Callable alias is a complex "process wrapper" and using it with subprocess operators has requirements on managing threading, capturing and std manually. Implementing this is [high bar](https://github.com/xonsh/xonsh/issues/2893) for inexperienced users who just want to play with command itself. 
* We need the way between simple string aliases and callable aliases that will works good with alias resolving.
 
I implemented this way as "Alias that returns modified command".

### Before

No way to create alias that does logic and returns modified command without using callable alias wrapper.

### After

```xsh
@aliases.register
@aliases.return_command
def _vi(args):
     """Universal vi editor."""
     if $(which vim 2>/dev/null):
         return ['vim'] + args
     else:
         return ['vi'] + args
```

```xsh
@aliases.register
@aliases.return_command
def _xsudo(args):
    return ['sudo', '--', *aliases.eval_alias(args)]

aliases['cow'] = 'echo mooo'  

xsudo cow tooo
# Password:
# mooo tooo

xsudo cow tooo | grep m
# mooo tooo
```

Closes #2618 #2893

### Notes

* It's uncomfortable work with aliases object when we have dict aliases for Windows in tests. We need to change it some day: #5452

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**


Problem statement:
Question: how make sudo expand aliases?
What is the official way to make sudo expand aliases, like `alias sudo='sudo '` on bash?

This is what I tried:
```xsh
$ACTUAL_SUDO = $(which sudo)
def sudo_expanding_aliases(args):
    $ACTUAL_SUDO @(aliases.eval_alias(args))
aliases['sudo'] = sudo_expanding_aliases
del sudo_expanding_aliases
```

This works, but it's kinda verbose and pollutes the environment.

```xsh
def sudo_expanding_aliases_clojure():
    actual_sudo = $(which sudo)
    def sudo_expanding_aliases(args):
        @(actual_sudo) @(aliases.eval_alias(args))
    return sudo_expanding_aliases
aliases['sudo'] = sudo_expanding_aliases_clojure()
del sudo_expanding_aliases
```

That one is a syntax error around the `return sudo_expanding_aliases` and I have no idea why. It's also even more verbose and ugly.

Am I thinking in the correct direction? Is it a good idea in general? Am I doing the expansion right? How can I reduce environmental pollution?

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**

Hints from issue discussion:

Commit message: Merge branch 'main' into command_alias

Patch to review:
diff --git a/docs/tutorial.rst b/docs/tutorial.rst
index 30cf8a1eaa..67b7ff92b5 100644
--- a/docs/tutorial.rst
+++ b/docs/tutorial.rst
@@ -1275,7 +1275,7 @@ functions. If you don't know what these do, you probably don't need them.
 
 
 Aliases
-==============================
+=======
 Another important xonsh built-in is the ``aliases`` mapping.  This is
 like a dictionary that affects how subprocess commands are run.  If you are
 familiar with the Bash ``alias`` built-in, this is similar.  Alias command
@@ -1305,6 +1305,44 @@ If you were to run ``gco feature-fabulous`` with the above aliases in effect,
 the command would reduce to ``['git', 'checkout', 'feature-fabulous']`` before
 being executed.
 
+Alias to modify command
+-----------------------
+
+The best way to modify command on the fly is to use alias that returns modified command.
+One of the most interesting application is expanding an aliases:
+
+.. code-block:: xonshcon
+
+    >>> @aliases.register
+    ... @aliases.return_command
+    ... def _xsudo(args):
+    ...     """Sudo with expanding aliases."""
+    ...     return ['sudo', '--', *aliases.eval_alias(args)]
+    ...
+    >>> aliases['install'] = "apt install cowsay"
+    >>> xsudo install
+    # Password:
+    # Install cowsay
+
+Or implement logic to run the right command:
+
+.. code-block:: xonshcon
+
+    >>> @aliases.register
+    ... @aliases.return_command
+    ... def _vi(args):
+    ...     """Universal vi editor."""
+    ...     if $(which vim 2>/dev/null):
+    ...         return ['vim'] + args
+    ...     else:
+    ...         return ['vi'] + args
+    ...
+    >>> vi file
+
+
+ExecAlias
+---------
+
 If the string is representing a block of xonsh code, the alias will be registered
 as an ``ExecAlias``, which is a callable alias. This block of code will then be
 executed whenever the alias is run. The arguments are available in the list ``$args``
diff --git a/news/alias_return_cmd.rst b/news/alias_return_cmd.rst
new file mode 100644
index 0000000000..2167c7d26a
--- /dev/null
+++ b/news/alias_return_cmd.rst
@@ -0,0 +1,23 @@
+**Added:**
+
+* Added ``@aliases.return_command`` decorator to eliminate the need to wrap the logic for modifying command into callable alias wrapper (#5473).
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* <news item>
+
+**Security:**
+
+* <news item>
diff --git a/tests/procs/test_specs.py b/tests/procs/test_specs.py
index 1cf74dbc5b..b307fc070a 100644
--- a/tests/procs/test_specs.py
+++ b/tests/procs/test_specs.py
@@ -183,6 +183,7 @@ def test_interrupted_process_returncode(xonsh_session, captured, interactive):
 
 
 @skip_if_on_windows
+@pytest.mark.flaky(reruns=3, reruns_delay=1)
 def test_proc_raise_subproc_error(xonsh_session):
     xonsh_session.env["RAISE_SUBPROC_ERROR"] = False
 
@@ -469,3 +470,110 @@ def alias(cls, args, stdin, stdout):
     xession.aliases["alias_with_partial_args"] = Class.alias
     out = run_subproc([["alias_with_partial_args"]], captured="stdout")
     assert out == "ok"
+
+
+def test_alias_return_command_alone(xession):
+    @xession.aliases.register("wakka")
+    @xession.aliases.return_command
+    def _wakka(args):
+        return ["echo"] + args
+
+    cmds = [
+        ["wakka"],
+    ]
+    spec = cmds_to_specs(cmds, captured="object")[-1]
+    assert spec.cmd == ["echo"]
+    assert spec.alias_name == "wakka"
+
+
+def test_alias_return_command_alone_args(xession):
+    @xession.aliases.register("wakka")
+    @xession.aliases.return_command
+    def _wakka(args):
+        return ["echo", "e0", "e1"] + args
+
+    cmds = [
+        ["wakka", "0", "1"],
+    ]
+    spec = cmds_to_specs(cmds, captured="object")[-1]
+    assert spec.cmd == ["echo", "e0", "e1", "0", "1"]
+    assert spec.alias_name == "wakka"
+
+
+def test_alias_return_command_chain(xession):
+    xession.aliases["foreground"] = "midground f0 f1"
+
+    @xession.aliases.register("midground")
+    @xession.aliases.return_command
+    def _midground(args):
+        return ["ground", "m0", "m1"] + args
+
+    xession.aliases["ground"] = "background g0 g1"
+    xession.aliases["background"] = "echo b0 b1"
+
+    cmds = [
+        ["foreground", "0", "1"],
+    ]
+    spec = cmds_to_specs(cmds, captured="object")[-1]
+    assert spec.cmd == [
+        "echo",
+        "b0",
+        "b1",
+        "g0",
+        "g1",
+        "m0",
+        "m1",
+        "f0",
+        "f1",
+        "0",
+        "1",
+    ]
+    assert spec.alias_name == "foreground"
+
+
+def test_alias_return_command_chain_spec_modifiers(xession):
+    xession.aliases["foreground"] = "midground f0 f1"
+
+    xession.aliases["xunthread"] = SpecAttrModifierAlias(
+        {"threadable": False, "force_threadable": False}
+    )
+
+    @xession.aliases.register("midground")
+    @xession.aliases.return_command
+    def _midground(args):
+        return ["ground", "m0", "m1"]
+
+    xession.aliases["ground"] = "background g0 g1"
+    xession.aliases["background"] = "xunthread echo b0 b1"
+
+    cmds = [
+        ["foreground", "0", "1"],
+    ]
+    spec = cmds_to_specs(cmds, captured="object")[-1]
+    assert spec.cmd == ["echo", "b0", "b1", "g0", "g1", "m0", "m1"]
+    assert spec.alias_name == "foreground"
+    assert spec.threadable is False
+
+
+def test_alias_return_command_eval_inside(xession):
+    xession.aliases["xthread"] = SpecAttrModifierAlias(
+        {"threadable": True, "force_threadable": True}
+    )
+
+    @xession.aliases.register("xsudo")
+    @xession.aliases.return_command
+    def _midground(args, spec_modifiers=None):
+        return [
+            "sudo",
+            *xession.aliases.eval_alias(args, spec_modifiers=spec_modifiers),
+        ]
+
+    xession.aliases["cmd"] = "xthread echo 1"
+
+    cmds = [
+        ["xsudo", "cmd"],
+    ]
+    spec = cmds_to_specs(cmds, captured="object")[-1]
+    assert spec.cmd == ["sudo", "echo", "1"]
+    assert spec.alias_name == "xsudo"
+    assert spec.threadable is True
diff --git a/tests/test_aliases.py b/tests/test_aliases.py
index a2c0e45a34..b672a180c7 100644
--- a/tests/test_aliases.py
+++ b/tests/test_aliases.py
@@ -6,7 +6,7 @@
 
 import pytest
 
-from xonsh.aliases import Aliases, ExecAlias
+from xonsh.aliases import Aliases, ExecAlias, run_alias_by_params
 
 
 def cd(args, stdin=None):
@@ -53,10 +53,17 @@ def test_eval_recursive(xession):
     assert ales.get("color_ls") == ["ls", "-  -", "--color=true"]
 
 
+def test_eval_callable(xession):
+    ales = make_aliases()
+    resolved = ales.get(["cd", "tmp"])
+    assert callable(resolved[0])
+    assert isinstance(resolved[1], str)
+
+
 def test_eval_recursive_callable_partial(xonsh_execer, xession):
     ales = make_aliases()
     xession.env["HOME"] = os.path.expanduser("~")
-    assert ales.get("indirect_cd")(["arg2", "arg3"]) == ["..", "arg2", "arg3"]
+    assert ales.get(["indirect_cd", "arg2", "arg3"])[1:] == ["..", "arg2", "arg3"]
 
 
 def _return_to_sender_all(args, stdin, stdout, stderr, spec, stack):
@@ -74,9 +81,11 @@ def _return_to_sender_all(args, stdin, stdout, stderr, spec, stack):
 
 def test_recursive_callable_partial_all(xession):
     ales = Aliases({"rtn": _return_to_sender_all, "rtn-recurse": ["rtn", "arg1"]})
-    alias = ales.get("rtn-recurse")
+    alias = ales.get("rtn-recurse")[0]
     assert callable(alias)
-    args, obs = alias(["arg2"], stdin="a", stdout="b", stderr="c", spec="d", stack="e")
+    args, obs = alias(
+        ["arg1", "arg2"], stdin="a", stdout="b", stderr="c", spec="d", stack="e"
+    )
     assert args == ["arg1", "arg2"]
     assert len(obs) == 5
     exp = {"stdin": "a", "stdout": "b", "stderr": "c", "spec": "d", "stack": "e"}
@@ -89,9 +98,9 @@ def _return_to_sender_handles(args, stdin, stdout, stderr):
 
 def test_recursive_callable_partial_handles(xession):
     ales = Aliases({"rtn": _return_to_sender_handles, "rtn-recurse": ["rtn", "arg1"]})
-    alias = ales.get("rtn-recurse")
+    alias = ales.get("rtn-recurse")[0]
     assert callable(alias)
-    args, obs = alias(["arg2"], stdin="a", stdout="b", stderr="c")
+    args, obs = alias(["arg1", "arg2"], stdin="a", stdout="b", stderr="c")
     assert args == ["arg1", "arg2"]
     assert len(obs) == 3
     exp = {"stdin": "a", "stdout": "b", "stderr": "c"}
@@ -104,7 +113,7 @@ def _return_to_sender_none():
 
 def test_recursive_callable_partial_none(xession):
     ales = Aliases({"rtn": _return_to_sender_none, "rtn-recurse": ["rtn"]})
-    alias = ales.get("rtn-recurse")
+    alias = ales.get("rtn-recurse")[0]
     assert callable(alias)
     args, obs = alias()
     assert args == "wakka"
@@ -214,3 +223,26 @@ def with_options(): ...
     def _private(): ...
 
     assert set(aliases) == {"debug", "name", "private"}
+
+
+def test_run_alias_by_params():
+    def alias_named_params(args, stdout):
+        return (args, stdout)
+
+    def alias_named_params_rev(stdout, args):
+        return (args, stdout)
+
+    def alias_list_params(a, i, o, e):
+        return (a, i, o, e)
+
+    assert run_alias_by_params(alias_named_params, {"args": 1, "stdout": 2}) == (1, 2)
+    assert run_alias_by_params(alias_named_params_rev, {"args": 1, "stdout": 2}) == (
+        1,
+        2,
+    )
+    assert run_alias_by_params(alias_list_params, {"args": 1, "stderr": 4}) == (
+        1,
+        None,
+        None,
+        4,
+    )
diff --git a/xonsh/aliases.py b/xonsh/aliases.py
index bc0e4b0703..721e7e40d8 100644
--- a/xonsh/aliases.py
+++ b/xonsh/aliases.py
@@ -1,7 +1,6 @@
 """Aliases for the xonsh shell."""
 
 import argparse
-import collections.abc as cabc
 import functools
 import inspect
 import os
@@ -10,6 +9,8 @@
 import sys
 import types
 import typing as tp
+from collections import OrderedDict
+from collections import abc as cabc
 
 import xonsh.completers._aliases as xca
 import xonsh.history.main as xhm
@@ -42,6 +43,7 @@
     argvquote,
     escape_windows_cmd_string,
     print_color,
+    print_exception,
     strip_simple_quotes,
     swap_values,
     to_repr_pretty_,
@@ -59,8 +61,9 @@ def EXEC_ALIAS_RE():
 class FuncAlias:
     """Provides a callable alias for xonsh commands."""
 
-    attributes_show = ["__xonsh_threadable__", "__xonsh_capturable__"]
+    attributes_show = ["__xonsh_threadable__", "__xonsh_capturable__", "return_command"]
     attributes_inherit = attributes_show + ["__doc__"]
+    return_command = False
 
     def __init__(self, name, func=None):
         self.__name__ = self.name = name
@@ -79,12 +82,27 @@ def __repr__(self):
         return f"FuncAlias({repr(r)})"
 
     def __call__(
-        self, args=None, stdin=None, stdout=None, stderr=None, spec=None, stack=None
+        self,
+        args=None,
+        stdin=None,
+        stdout=None,
+        stderr=None,
+        spec=None,
+        stack=None,
+        spec_modifiers=None,
     ):
-        func_args = [args, stdin, stdout, stderr, spec, stack][
-            : len(inspect.signature(self.func).parameters)
-        ]
-        return self.func(*func_args)
+        return run_alias_by_params(
+            self.func,
+            {
+                "args": args,
+                "stdin": stdin,
+                "stdout": stdout,
+                "stderr": stderr,
+                "spec": spec,
+                "stack": stack,
+                "spec_modifiers": spec_modifiers,
+            },
+        )
 
 
 class Aliases(cabc.MutableMapping):
@@ -132,28 +150,18 @@ def wrapper(func):
 
         return wrapper
 
-    def get(self, key, default=None, spec_modifiers=None):
-        """Returns the (possibly modified) value. If the key is not present,
-        then `default` is returned.
-        If the value is callable, it is returned without modification. If it
-        is an iterable of strings it will be evaluated recursively to expand
-        other aliases, resulting in a new list or a "partially applied"
-        callable.
-        """
-        spec_modifiers = spec_modifiers if spec_modifiers is not None else []
-        val = self._raw.get(key)
-        if val is None:
-            return default
-        elif isinstance(val, cabc.Iterable) or callable(val):
-            return self.eval_alias(
-                val, seen_tokens={key}, spec_modifiers=spec_modifiers
-            )
-        else:
-            msg = "alias of {!r} has an inappropriate type: {!r}"
-            raise TypeError(msg.format(key, val))
+    def return_command(self, f):
+        """Decorator that switches alias from returning result to return in new command for execution."""
+        f.return_command = True
+        return f
 
     def eval_alias(
-        self, value, seen_tokens=frozenset(), acc_args=(), spec_modifiers=None
+        self,
+        value,
+        seen_tokens=frozenset(),
+        acc_args=(),
+        spec_modifiers=None,
+        found_return_command=None,
     ):
         """
         "Evaluates" the alias ``value``, by recursively looking up the leftmost
@@ -166,6 +174,9 @@ def eval_alias(
         ``["-al", "arg"]``.
         """
         spec_modifiers = spec_modifiers if spec_modifiers is not None else []
+        found_return_command = (
+            found_return_command if found_return_command is not None else []
+        )
         # Beware of mutability: default values for keyword args are evaluated
         # only once.
         if (
@@ -182,8 +193,19 @@ def eval_alias(
                     break
             value = value[i:]
 
+        if callable(value) and getattr(value, "return_command", False):
+            try:
+                found_return_command.append(value.__name__)
+                value = value(acc_args, spec_modifiers=spec_modifiers)
+                acc_args = []
+            except Exception as e:
+                print_exception(f"Exception inside alias {value}: {e}")
+                return None
+            if not len(value):
+                raise ValueError("return_command alias: zero arguments.")
+
         if callable(value):
-            return partial_eval_alias(value, acc_args=acc_args)
+            return [value] + list(acc_args)
         else:
             expand_path = XSH.expand_path
             token, *rest = map(expand_path, value)
@@ -203,8 +225,65 @@ def eval_alias(
                     seen_tokens,
                     acc_args,
                     spec_modifiers=spec_modifiers,
+                    found_return_command=found_return_command,
                 )
 
+    def get(
+        self,
+        key,
+        default=None,
+        spec_modifiers=None,
+        found_return_command=None,
+    ):
+        """
+        Returns list that represent command with resolved aliases.
+        The ``key`` can be string with alias name or list for a command.
+        In the first position will be the resolved command name or callable alias.
+        If the key is not present, then `default` is returned.
+
+        ``spec_modifiers`` is the list of SpecModifier objects that found during
+        resolving aliases (#5443).
+        ``found_return_command`` is the list of aliases with return command functionality that found
+        during resolving aliases (#5473).
+
+        Note! The return value is always list because during resolving
+        we can find return_command alias that can completely replace
+        command and add new arguments.
+        """
+        spec_modifiers = spec_modifiers if spec_modifiers is not None else []
+        found_return_command = (
+            found_return_command if found_return_command is not None else []
+        )
+        args = []
+        if isinstance(key, list):
+            args = key[1:]
+            key = key[0]
+        val = self._raw.get(key)
+        if callable(val) and getattr(val, "return_command", False):
+            try:
+                found_return_command.append(val.__name__)
+                val = val(args, spec_modifiers=spec_modifiers)
+                args = []
+            except Exception as e:
+                print_exception(f"Exception inside alias {key!r}: {e}")
+                return None
+            if not len(val):
+                raise ValueError("return_command alias: zero arguments.")
+
+        if val is None:
+            return default
+        elif isinstance(val, cabc.Iterable) or callable(val):
+            return self.eval_alias(
+                val,
+                seen_tokens={key},
+                spec_modifiers=spec_modifiers,
+                acc_args=args,
+                found_return_command=found_return_command,
+            )
+        else:
+            msg = "alias of {!r} has an inappropriate type: {!r}"
+            raise TypeError(msg.format(key, val))
+
     def expand_alias(self, line: str, cursor_index: int) -> str:
         """Expands any aliases present in line if alias does not point to a
         builtin function and if alias is only a single command.
@@ -408,6 +487,21 @@ def __call__(
         return self.f(args, stdin, stdout, stderr, spec, stack)
 
 
+class PartialEvalAlias7(PartialEvalAliasBase):
+    def __call__(
+        self,
+        args,
+        stdin=None,
+        stdout=None,
+        stderr=None,
+        spec=None,
+        stack=None,
+        spec_modifiers=None,
+    ):
+        args = list(self.acc_args) + args
+        return self.f(args, stdin, stdout, stderr, spec, stack, spec_modifiers)
+
+
 PARTIAL_EVAL_ALIASES = (
     PartialEvalAlias0,
     PartialEvalAlias1,
@@ -416,6 +510,7 @@ def __call__(
     PartialEvalAlias4,
     PartialEvalAlias5,
     PartialEvalAlias6,
+    PartialEvalAlias7,
 )
 
 
@@ -436,13 +531,50 @@ def partial_eval_alias(f, acc_args=()):
             numargs += 1
         elif name in ALIAS_KWARG_NAMES and param.kind == param.KEYWORD_ONLY:
             numargs += 1
-    if numargs < 7:
+    if numargs < 8:
         return PARTIAL_EVAL_ALIASES[numargs](f, acc_args=acc_args)
     else:
-        e = "Expected proxy with 6 or fewer arguments for {}, not {}"
+        e = "Expected proxy with 7 or fewer arguments for {}, not {}"
         raise XonshError(e.format(", ".join(ALIAS_KWARG_NAMES), numargs))
 
 
+def run_alias_by_params(func: tp.Callable, params: dict[str, tp.Any]):
+    """
+    Run alias function based on signature and params.
+    If function param names are in alias signature fill them.
+    If function params have unknown names fill using alias signature order.
+    """
+    alias_params = OrderedDict(
+        {
+            "args": None,
+            "stdin": None,
+            "stdout": None,
+            "stderr": None,
+            "spec": None,
+            "stack": None,
+            "spec_modifiers": None,
+        }
+    )
+    alias_params |= params
+    sign = inspect.signature(func)
+    func_params = sign.parameters.items()
+    kwargs = {
+        name: alias_params[name] for name, p in func_params if name in alias_params
+    }
+
+    if len(kwargs) != len(func_params):
+        # There is unknown param. Switch to positional mode.
+        kwargs = OrderedDict()
+        vals = list(alias_params.values())
+        len_vals = len(vals)
+        i = 0
+        for namep in func_params:
+            kwargs[namep[0]] = vals[i]
+            if (i := i + 1) == len_vals:
+                break
+    return func(**kwargs)
+
+
 #
 # Actual aliases below
 #
diff --git a/xonsh/completers/_aliases.py b/xonsh/completers/_aliases.py
index 6aeb069dcf..2723710e68 100644
--- a/xonsh/completers/_aliases.py
+++ b/xonsh/completers/_aliases.py
@@ -158,7 +158,7 @@ def complete_aliases(command: CommandContext):
     if cmd not in XSH.aliases:
         # only complete aliases
         return
-    alias = XSH.aliases.get(cmd)  # type: ignore
+    alias = XSH.aliases.get(cmd)[0]  # type: ignore
 
     completer = getattr(alias, "xonsh_complete", None)
     if not completer:
diff --git a/xonsh/procs/specs.py b/xonsh/procs/specs.py
index 6df5c96d6a..492a1d2be4 100644
--- a/xonsh/procs/specs.py
+++ b/xonsh/procs/specs.py
@@ -408,6 +408,7 @@ def __init__(
         self.args = _flatten_cmd_redirects(cmd)
         self.alias = None
         self.alias_name = None
+        self.alias_return_command = None
         self.alias_stack = XSH.env.get("__ALIAS_STACK", "").split(":")
         self.binary_loc = None
         self.is_proxy = False
@@ -707,7 +708,6 @@ def resolve_redirects(self):
     def resolve_alias(self):
         """Sets alias in command, if applicable."""
         cmd0 = self.cmd[0]
-        spec_modifiers = []
         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.
@@ -715,19 +715,34 @@ def resolve_alias(self):
             return
 
         if callable(cmd0):
-            alias = cmd0
+            self.alias = cmd0
         else:
+            found_spec_modifiers = []
+            found_return_command = []
             if isinstance(XSH.aliases, dict):
                 # Windows tests
                 alias = XSH.aliases.get(cmd0, None)
+                if alias is not None:
+                    alias = alias + self.cmd[1:]
             else:
-                alias = XSH.aliases.get(cmd0, None, spec_modifiers=spec_modifiers)
+                alias = XSH.aliases.get(
+                    self.cmd,
+                    None,
+                    spec_modifiers=found_spec_modifiers,
+                    found_return_command=found_return_command,
+                )
             if alias is not None:
                 self.alias_name = cmd0
-        self.alias = alias
-        if spec_modifiers:
-            for mod in spec_modifiers:
-                self.add_spec_modifier(mod)
+                if callable(alias[0]):
+                    self.alias = alias[0]
+                    self.cmd = [cmd0] + alias[1:]
+                else:
+                    self.alias = alias
+
+            self.alias_return_command = bool(found_return_command)
+            if found_spec_modifiers:
+                for mod in found_spec_modifiers:
+                    self.add_spec_modifier(mod)
 
     def resolve_binary_loc(self):
         """Sets the binary location"""
@@ -765,8 +780,7 @@ def resolve_executable_commands(self):
             self.cmd.pop(0)
             return
         else:
-            self.cmd = alias + self.cmd[1:]
-            # resolve any redirects the aliases may have applied
+            self.cmd = alias
             self.resolve_redirects()
         if self.binary_loc is None:
             return
@@ -971,7 +985,13 @@ def _trace_specs(trace_mode, specs, cmds, captured):
                 }
                 p |= {
                     a: getattr(s, a, None)
-                    for a in ["alias_name", "binary_loc", "threadable", "background"]
+                    for a in [
+                        "alias_name",
+                        "alias",
+                        "binary_loc",
+                        "threadable",
+                        "background",
+                    ]
                 }
                 if trace_mode == 3:
                     p |= {


Code review comments:
diff_hunk: @@ -59,8 +61,9 @@ def EXEC_ALIAS_RE():
 class FuncAlias:
     """Provides a callable alias for xonsh commands."""
 
-    attributes_show = ["__xonsh_threadable__", "__xonsh_capturable__"]
+    attributes_show = ["__xonsh_threadable__", "__xonsh_capturable__", "return_command"]
     attributes_inherit = attributes_show + ["__doc__"]
+    return_command = False
path: xonsh/aliases.py
text: @user1:
I try to avoid using boolean flags. They're pretty limiting and under-descriptive. In this case, for example, the reader knows that `return_command = True` means to return the command, but they might be left wondering, "what is returned if `return_command = False`?"

Consider instead:

```
return_what : Literal["command", "result"] = "result"
```

This approach is more descriptive, moves the selector (command or not) out of the variable name and into the value, and is more extensible should a new use-case arise for another variant of what to return.

@author:
Awesome! Fixed. Thanks!

diff_hunk: @@ -436,13 +531,50 @@ def partial_eval_alias(f, acc_args=()):
             numargs += 1
         elif name in ALIAS_KWARG_NAMES and param.kind == param.KEYWORD_ONLY:
             numargs += 1
-    if numargs < 7:
+    if numargs < 8:
         return PARTIAL_EVAL_ALIASES[numargs](f, acc_args=acc_args)
     else:
-        e = "Expected proxy with 6 or fewer arguments for {}, not {}"
+        e = "Expected proxy with 7 or fewer arguments for {}, not {}"
         raise XonshError(e.format(", ".join(ALIAS_KWARG_NAMES), numargs))
 
 
+def run_alias_by_params(func: tp.Callable, params: dict[str, tp.Any]):
+    """
+    Run alias function based on signature and params.
+    If function param names are in alias signature fill them.
+    If function params have unknown names fill using alias signature order.
+    """
+    alias_params = OrderedDict(
+        {
+            "args": None,
+            "stdin": None,
+            "stdout": None,
+            "stderr": None,
+            "spec": None,
+            "stack": None,
+            "spec_modifiers": None,
+        }
+    )
+    alias_params |= params
+    sign = inspect.signature(func)
+    func_params = sign.parameters.items()
+    kwargs = {
+        name: alias_params[name] for name, p in func_params if name in alias_params
+    }
+
+    if len(kwargs) != len(func_params):
+        # There is unknown param. Switch to positional mode.
+        kwargs = OrderedDict()
+        vals = list(alias_params.values())
+        len_vals = len(vals)
+        i = 0
+        for namep in func_params:
+            kwargs[namep[0]] = vals[i]
+            if (i := i + 1) == len_vals:
+                break
path: xonsh/aliases.py
text: @user1:
I'm pretty sure this behavior is expressable as a single expression

```suggestion
        kwargs = {
            namep[0]: val
            for namep, val in zip(func_params, alias_params.values())
        }
```

or

```suggestion
        kwargs = dict(zip(map(operator.itemgetter(0), func_params), alias_params.values()))
```

And of course, you can use `OrderedDict` if you want, but with Python 3.6+, the std dict will suffice.