"""Aliases for the xonsh shell."""
import argparse
import collections.abc as cabc
import functools
import inspect
import os
import re
import sys
import types
import typing as tp

import xonsh.completers._aliases as xca
import xonsh.history.main as xhm
import xonsh.xoreutils.which as xxw
from xonsh.ast import isexpression
from xonsh.built_ins import XSH
from xonsh.cli_utils import Annotated, Arg, ArgParserAlias
from xonsh.dirstack import _get_cwd, cd, dirs, popd, pushd
from xonsh.environ import locate_binary, make_args_env
from xonsh.foreign_shells import foreign_shell_data
from xonsh.jobs import bg, clean_jobs, disown, fg, jobs
from xonsh.lazyasd import lazyobject
from xonsh.platform import (
    IN_APPIMAGE,
    ON_ANACONDA,
    ON_DARWIN,
    ON_DRAGONFLY,
    ON_FREEBSD,
    ON_NETBSD,
    ON_OPENBSD,
    ON_WINDOWS,
)
from xonsh.timings import timeit_alias
from xonsh.tools import (
    ALIAS_KWARG_NAMES,
    XonshError,
    adjust_shlvl,
    argvquote,
    escape_windows_cmd_string,
    print_color,
    strip_simple_quotes,
    swap_values,
    to_repr_pretty_,
    to_shlvl,
    unthreadable,
)
from xonsh.xontribs import xontribs_main


@lazyobject
def EXEC_ALIAS_RE():
    return re.compile(r"@\(|\$\(|!\(|\$\[|!\[|\&\&|\|\||\s+and\s+|\s+or\s+|[>|<]")


class Aliases(cabc.MutableMapping):
    """Represents a location to hold and look up aliases."""

    def __init__(self, *args, **kwargs):
        self._raw = {}
        self.update(*args, **kwargs)

    @staticmethod
    def _get_func_name(func):
        name = func.__name__

        # Strip leading underscore
        if name.startswith("_"):
            name = name[1:]
        return name

    def _register(self, func, name="", dash_case=True):
        name = name or self._get_func_name(func)

        if dash_case:
            name = name.replace("_", "-")

        self[name] = func
        return func

    @tp.overload
    def register(self, func: types.FunctionType) -> types.FunctionType:
        """simple usage"""

    @tp.overload
    def register(
        self, name: str, *, dash_case: bool = True
    ) -> tp.Callable[[types.FunctionType], types.FunctionType]:
        ...

    def register(self, func_or_name, name=None, dash_case=True):
        """Decorator to register the given function by name."""

        if isinstance(func_or_name, types.FunctionType):
            return self._register(func_or_name, name, dash_case)

        def wrapper(func):
            return self._register(func, func_or_name, dash_case)

        return wrapper

    def get(self, key, default=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.
        """
        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})
        else:
            msg = "alias of {!r} has an inappropriate type: {!r}"
            raise TypeError(msg.format(key, val))

    def eval_alias(self, value, seen_tokens=frozenset(), acc_args=()):
        """
        "Evaluates" the alias ``value``, by recursively looking up the leftmost
        token and "expanding" if it's also an alias.

        A value like ``["cmd", "arg"]`` might transform like this:
        ``> ["cmd", "arg"] -> ["ls", "-al", "arg"] -> callable()``
        where ``cmd=ls -al`` and ``ls`` is an alias with its value being a
        callable.  The resulting callable will be "partially applied" with
        ``["-al", "arg"]``.
        """
        # Beware of mutability: default values for keyword args are evaluated
        # only once.
        if callable(value):
            return partial_eval_alias(value, acc_args=acc_args)
        else:
            expand_path = XSH.expand_path
            token, *rest = map(expand_path, value)
            if token in seen_tokens or token not in self._raw:
                # ^ Making sure things like `egrep=egrep --color=auto` works,
                # and that `l` evals to `ls --color=auto -CF` if `l=ls -CF`
                # and `ls=ls --color=auto`
                rtn = [token]
                rtn.extend(rest)
                rtn.extend(acc_args)
                return rtn
            else:
                seen_tokens = seen_tokens | {token}
                acc_args = rest + list(acc_args)
                return self.eval_alias(self._raw[token], seen_tokens, acc_args)

    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.
        The command won't be expanded if the cursor's inside/behind it.
        """
        word = (line.split(maxsplit=1) or [""])[0]
        if word in XSH.aliases and isinstance(self.get(word), cabc.Sequence):  # type: ignore
            word_idx = line.find(word)
            word_edge = word_idx + len(word)
            if cursor_index > word_edge:
                # the cursor isn't inside/behind the word
                expansion = " ".join(self.get(word))
                line = line[:word_idx] + expansion + line[word_edge:]
        return line

    #
    # Mutable mapping interface
    #

    def __getitem__(self, key):
        return self._raw[key]

    def __setitem__(self, key, val):
        if isinstance(val, str):
            f = "<exec-alias:" + key + ">"
            if EXEC_ALIAS_RE.search(val) is not None:
                # We have a sub-command (e.g. $(cmd)) or IO redirect (e.g. >>)
                self._raw[key] = ExecAlias(val, filename=f)
            elif isexpression(val):
                # expansion substitution
                lexer = XSH.execer.parser.lexer
                self._raw[key] = list(map(strip_simple_quotes, lexer.split(val)))
            else:
                # need to exec alias
                self._raw[key] = ExecAlias(val, filename=f)
        else:
            self._raw[key] = val

    def _common_or(self, other):
        new_dict = self._raw.copy()
        for key in dict(other):
            new_dict[key] = other[key]
        return Aliases(new_dict)

    def __or__(self, other):
        return self._common_or(other)

    def __ror__(self, other):
        return self._common_or(other)

    def __ior__(self, other):
        for key in dict(other):
            self[key] = other[key]
        return self

    def __delitem__(self, key):
        del self._raw[key]

    def update(self, *args, **kwargs):
        for key, val in dict(*args, **kwargs).items():
            self[key] = val

    def __iter__(self):
        yield from self._raw

    def __len__(self):
        return len(self._raw)

    def __str__(self):
        return str(self._raw)

    def __repr__(self):
        return "{}.{}({})".format(
            self.__class__.__module__, self.__class__.__name__, self._raw
        )

    _repr_pretty_ = to_repr_pretty_


class ExecAlias:
    """Provides a callable alias for xonsh source code."""

    def __init__(self, src, filename="<exec-alias>"):
        """
        Parameters
        ----------
        src : str
            Source code that will be
        """
        self.src = src
        self.filename = filename

    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        execer = XSH.execer
        frame = stack[0][0]  # execute as though we are at the call site

        alias_args = {"args": args}
        for i, a in enumerate(args):
            alias_args[f"arg{i}"] = a

        with XSH.env.swap(alias_args):
            execer.exec(
                self.src,
                glbs=frame.f_globals,
                locs=frame.f_locals,
                filename=self.filename,
            )
        if XSH.history is not None:
            return XSH.history.last_cmd_rtn

    def __repr__(self):
        return f"ExecAlias({self.src!r}, filename={self.filename!r})"


class PartialEvalAliasBase:
    """Partially evaluated alias."""

    def __init__(self, f, acc_args=()):
        """
        Parameters
        ----------
        f : callable
            A function to dispatch to.
        acc_args : sequence of strings, optional
            Additional arguments to prepent to the argument list passed in
            when the alias is called.
        """
        self.f = f
        self.acc_args = acc_args
        self.__name__ = getattr(f, "__name__", self.__class__.__name__)

    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr, spec, stack)

    def __repr__(self):
        return "{name}({f!r}, acc_args={acc_args!r})".format(
            name=self.__class__.__name__, f=self.f, acc_args=self.acc_args
        )


class PartialEvalAlias0(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        if args:
            msg = "callable alias {f!r} takes no arguments, but {args!f} provided. "
            msg += "Of these {acc_args!r} were partially applied."
            raise XonshError(msg.format(f=self.f, args=args, acc_args=self.acc_args))
        return self.f()


class PartialEvalAlias1(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args)


class PartialEvalAlias2(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin)


class PartialEvalAlias3(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout)


class PartialEvalAlias4(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr)


class PartialEvalAlias5(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr, spec)


class PartialEvalAlias6(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr, spec, stack)


PARTIAL_EVAL_ALIASES = (
    PartialEvalAlias0,
    PartialEvalAlias1,
    PartialEvalAlias2,
    PartialEvalAlias3,
    PartialEvalAlias4,
    PartialEvalAlias5,
    PartialEvalAlias6,
)


def partial_eval_alias(f, acc_args=()):
    """Dispatches the appropriate eval alias based on the number of args to the original callable alias
    and how many arguments to apply.
    """
    # no partial needed if no extra args
    if not acc_args:
        return f
    # need to dispatch
    numargs = 0
    for name, param in inspect.signature(f).parameters.items():
        if (
            param.kind == param.POSITIONAL_ONLY
            or param.kind == param.POSITIONAL_OR_KEYWORD
        ):
            numargs += 1
        elif name in ALIAS_KWARG_NAMES and param.kind == param.KEYWORD_ONLY:
            numargs += 1
    if numargs < 7:
        return PARTIAL_EVAL_ALIASES[numargs](f, acc_args=acc_args)
    else:
        e = "Expected proxy with 6 or fewer arguments for {}, not {}"
        raise XonshError(e.format(", ".join(ALIAS_KWARG_NAMES), numargs))


#
# Actual aliases below
#


def xonsh_exit(args, stdin=None):
    """Sends signal to exit shell."""
    if not clean_jobs():
        # Do not exit if jobs not cleaned up
        return None, None
    XSH.exit = True
    print()  # gimme a newline
    return None, None


def xonsh_reset(args, stdin=None):
    """Clears __xonsh__.ctx"""
    XSH.ctx.clear()


def source_foreign_fn(
    shell: str,
    files_or_code: Annotated[tp.List[str], Arg(nargs="+")],
    interactive=True,
    login=False,
    envcmd=None,
    aliascmd=None,
    extra_args="",
    safe=True,
    prevcmd="",
    postcmd="",
    funcscmd="",
    sourcer=None,
    use_tmpfile=False,
    seterrprevcmd=None,
    seterrpostcmd=None,
    overwrite_aliases=False,
    suppress_skip_message=False,
    show=False,
    dryrun=False,
    _stderr=None,
):
    """Sources a file written in a foreign shell language.

    Parameters
    ----------
    shell
        Name or path to the foreign shell
    files_or_code
        file paths to source or code in the target language.
    interactive : -n, --non-interactive
        whether the sourced shell should be interactive
    login : -l, --login
        whether the sourced shell should be login
    envcmd : --envcmd
        command to print environment
    aliascmd : --aliascmd
        command to print aliases
    extra_args : --extra-args
        extra arguments needed to run the shell
    safe : -u, --unsafe
        whether the source shell should be run safely, and not raise any errors, even if they occur.
    prevcmd : -p, --prevcmd
        command(s) to run before any other commands, replaces traditional source.
    postcmd : --postcmd
        command(s) to run after all other commands
    funcscmd : --funcscmd
        code to find locations of all native functions in the shell language.
    sourcer : --sourcer
        the source command in the target shell language.
        If this is not set, a default value will attempt to be
        looked up based on the shell name.
    use_tmpfile : --use-tmpfile
        whether the commands for source shell should be written to a temporary file.
    seterrprevcmd : --seterrprevcmd
        command(s) to set exit-on-error before any other commands.
    seterrpostcmd : --seterrpostcmd
        command(s) to set exit-on-error after all other commands.
    overwrite_aliases : --overwrite-aliases
        flag for whether or not sourced aliases should replace the current xonsh aliases.
    suppress_skip_message : --suppress-skip-message
        flag for whether or not skip messages should be suppressed.
    show : --show
        show the script output.
    dryrun : -d, --dry-run
        Will not actually source the file.
    """
    extra_args = tuple(extra_args.split())
    env = XSH.env
    suppress_skip_message = (
        env.get("FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE")
        if not suppress_skip_message
        else suppress_skip_message
    )
    files: tp.Tuple[str, ...] = ()
    if prevcmd:
        pass  # don't change prevcmd if given explicitly
    elif os.path.isfile(files_or_code[0]):
        if not sourcer:
            return (None, "xonsh: error: `sourcer` command is not mentioned.\n", 1)
        # we have filenames to source
        prevcmd = "".join([f"{sourcer} {f}\n" for f in files_or_code])
        files = tuple(files_or_code)
    elif not prevcmd:
        prevcmd = " ".join(files_or_code)  # code to run, no files
    foreign_shell_data.cache_clear()  # make sure that we don't get prev src
    fsenv, fsaliases = foreign_shell_data(
        shell=shell,
        login=login,
        interactive=interactive,
        envcmd=envcmd,
        aliascmd=aliascmd,
        extra_args=extra_args,
        safe=safe,
        prevcmd=prevcmd,
        postcmd=postcmd,
        funcscmd=funcscmd or None,  # the default is None in the called function
        sourcer=sourcer,
        use_tmpfile=use_tmpfile,
        seterrprevcmd=seterrprevcmd,
        seterrpostcmd=seterrpostcmd,
        show=show,
        dryrun=dryrun,
        files=files,
    )
    if fsenv is None:
        if dryrun:
            return
        else:
            msg = f"xonsh: error: Source failed: {prevcmd!r}\n"
            msg += "xonsh: error: Possible reasons: File not found or syntax error\n"
            return (None, msg, 1)
    # apply results
    denv = env.detype()
    for k, v in fsenv.items():
        if k == "SHLVL":  # ignore $SHLVL as sourcing should not change $SHLVL
            continue
        if k in denv and v == denv[k]:
            continue  # no change from original
        env[k] = v
    # Remove any env-vars that were unset by the script.
    for k in denv:
        if k not in fsenv:
            env.pop(k, None)
    # Update aliases
    baliases = XSH.aliases
    for k, v in fsaliases.items():
        if k in baliases and v == baliases[k]:
            continue  # no change from original
        elif overwrite_aliases or k not in baliases:
            baliases[k] = v
        elif suppress_skip_message:
            pass
        else:
            msg = (
                "Skipping application of {0!r} alias from {1!r} "
                "since it shares a name with an existing xonsh alias. "
                'Use "--overwrite-alias" option to apply it anyway.'
                'You may prevent this message with "--suppress-skip-message" or '
                '"$FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE = True".'
            )
            print(msg.format(k, shell), file=_stderr)


source_foreign = ArgParserAlias(
    func=source_foreign_fn, has_args=True, prog="source-foreign"
)


@unthreadable
def source_alias(args, stdin=None):
    """Executes the contents of the provided files in the current context.
    If sourced file isn't found in cwd, search for file along $PATH to source
    instead.
    """
    env = XSH.env
    encoding = env.get("XONSH_ENCODING")
    errors = env.get("XONSH_ENCODING_ERRORS")
    for i, fname in enumerate(args):
        fpath = fname
        if not os.path.isfile(fpath):
            fpath = locate_binary(fname)
            if fpath is None:
                if env.get("XONSH_DEBUG"):
                    print(f"source: {fname}: No such file", file=sys.stderr)
                if i == 0:
                    raise RuntimeError(
                        "must source at least one file, " + fname + " does not exist."
                    )
                break
        _, fext = os.path.splitext(fpath)
        if fext and fext != ".xsh" and fext != ".py":
            raise RuntimeError(
                "attempting to source non-xonsh file! If you are "
                "trying to source a file in another language, "
                "then please use the appropriate source command. "
                "For example, source-bash script.sh"
            )
        with open(fpath, encoding=encoding, errors=errors) as fp:
            src = fp.read()
        if not src.endswith("\n"):
            src += "\n"
        ctx = XSH.ctx
        updates = {"__file__": fpath, "__name__": os.path.abspath(fpath)}
        with env.swap(**make_args_env(args[i + 1 :])), swap_values(ctx, updates):
            try:
                XSH.builtins.execx(src, "exec", ctx, filename=fpath)
            except Exception:
                print_color(
                    "{RED}You may be attempting to source non-xonsh file! "
                    "{RESET}If you are trying to source a file in "
                    "another language, then please use the appropriate "
                    "source command. For example, {GREEN}source-bash "
                    "script.sh{RESET}",
                    file=sys.stderr,
                )
                raise


def source_cmd_fn(
    files: Annotated[tp.List[str], Arg(nargs="+")],
    login=False,
    aliascmd=None,
    extra_args="",
    safe=True,
    postcmd="",
    funcscmd="",
    seterrprevcmd=None,
    overwrite_aliases=False,
    suppress_skip_message=False,
    show=False,
    dryrun=False,
    _stderr=None,
):
    """
        Source cmd.exe files

    Parameters
    ----------
    files
        paths to source files.
    login : -l, --login
        whether the sourced shell should be login
    envcmd : --envcmd
        command to print environment
    aliascmd : --aliascmd
        command to print aliases
    extra_args : --extra-args
        extra arguments needed to run the shell
    safe : -s, --safe
        whether the source shell should be run safely, and not raise any errors, even if they occur.
    postcmd : --postcmd
        command(s) to run after all other commands
    funcscmd : --funcscmd
        code to find locations of all native functions in the shell language.
    seterrprevcmd : --seterrprevcmd
        command(s) to set exit-on-error before any other commands.
    overwrite_aliases : --overwrite-aliases
        flag for whether or not sourced aliases should replace the current xonsh aliases.
    suppress_skip_message : --suppress-skip-message
        flag for whether or not skip messages should be suppressed.
    show : --show
        show the script output.
    dryrun : -d, --dry-run
        Will not actually source the file.
    """
    args = list(files)
    fpath = locate_binary(args[0])
    args[0] = fpath if fpath else args[0]
    if not os.path.isfile(args[0]):
        return (None, f"xonsh: error: File not found: {args[0]}\n", 1)
    prevcmd = "call "
    prevcmd += " ".join([argvquote(arg, force=True) for arg in args])
    prevcmd = escape_windows_cmd_string(prevcmd)
    with XSH.env.swap(PROMPT="$P$G"):
        return source_foreign_fn(
            shell="cmd",
            files_or_code=args,
            interactive=True,
            sourcer="call",
            envcmd="set",
            seterrpostcmd="if errorlevel 1 exit 1",
            use_tmpfile=True,
            prevcmd=prevcmd,
            #     from this function
            login=login,
            aliascmd=aliascmd,
            extra_args=extra_args,
            safe=safe,
            postcmd=postcmd,
            funcscmd=funcscmd,
            seterrprevcmd=seterrprevcmd,
            overwrite_aliases=overwrite_aliases,
            suppress_skip_message=suppress_skip_message,
            show=show,
            dryrun=dryrun,
        )


source_cmd = ArgParserAlias(func=source_cmd_fn, has_args=True, prog="source-cmd")


def xexec_fn(
    command: Annotated[tp.List[str], Arg(nargs=argparse.REMAINDER)],
    login=False,
    clean=False,
    name="",
    _stdin=None,
):
    """exec (also aliased as xexec) uses the os.execvpe() function to
    replace the xonsh process with the specified program.

    This provides the functionality of the bash 'exec' builtin::

        >>> exec bash -l -i
        bash $

    Parameters
    ----------
    command
        program to launch along its arguments
    login : -l, --login
        the shell places a dash at the
        beginning of the zeroth argument passed to command to simulate login
        shell.
    clean : -c, --clean
        causes command to be executed with an empty environment.
    name : -a, --name
        the shell passes name as the zeroth argument
        to the executed command.

    Notes
    -----
    This command **is not** the same as the Python builtin function
    exec(). That function is for running Python code. This command,
    which shares the same name as the sh-lang statement, is for launching
    a command directly in the same process. In the event of a name conflict,
    please use the xexec command directly or dive into subprocess mode
    explicitly with ![exec command]. For more details, please see
    http://xon.sh/faq.html#exec.
    """
    if len(command) == 0:
        return (None, "xonsh: exec: no command specified\n", 1)

    cmd = command[0]
    if name:
        command[0] = name
    if login:
        command[0] = f"-{command[0]}"

    denv = {}
    if not clean:
        denv = XSH.env.detype()

        # decrement $SHLVL to mirror bash's behaviour
        if "SHLVL" in denv:
            old_shlvl = to_shlvl(denv["SHLVL"])
            denv["SHLVL"] = str(adjust_shlvl(old_shlvl, -1))

    try:
        os.execvpe(cmd, command, denv)
    except FileNotFoundError as e:
        return (
            None,
            "xonsh: exec: file not found: {}: {}" "\n".format(e.args[1], command[0]),
            1,
        )


xexec = ArgParserAlias(func=xexec_fn, has_args=True, prog="xexec")


@lazyobject
def xonfig():
    """Runs the xonsh configuration utility."""
    from xonsh.xonfig import xonfig_main  # lazy import

    return xonfig_main


@unthreadable
def trace(args, stdin=None, stdout=None, stderr=None, spec=None):
    """Runs the xonsh tracer utility."""
    from xonsh.tracer import tracermain  # lazy import

    try:
        return tracermain(args, stdin=stdin, stdout=stdout, stderr=stderr, spec=spec)
    except SystemExit:
        pass


def showcmd(args, stdin=None):
    """usage: showcmd [-h|--help|cmd args]

    Displays the command and arguments as a list of strings that xonsh would
    run in subprocess mode. This is useful for determining how xonsh evaluates
    your commands and arguments prior to running these commands.

    optional arguments:
      -h, --help            show this help message and exit

    Examples
    --------
      >>> showcmd echo $USER "can't" hear "the sea"
      ['echo', 'I', "can't", 'hear', 'the sea']
    """
    if len(args) == 0 or (len(args) == 1 and args[0] in {"-h", "--help"}):
        print(showcmd.__doc__.rstrip().replace("\n    ", "\n"))
    else:
        sys.displayhook(args)


def detect_xpip_alias():
    """
    Determines the correct invocation to get xonsh's pip
    """
    if not getattr(sys, "executable", None):
        return lambda args, stdin=None: (
            "",
            "Sorry, unable to run pip on your system (missing sys.executable)",
            1,
        )

    basecmd = [sys.executable, "-m", "pip"]
    try:
        if ON_WINDOWS or IN_APPIMAGE:
            # XXX: Does windows have an installation mode that requires UAC?
            return basecmd
        elif not os.access(os.path.dirname(sys.executable), os.W_OK):
            return ["sudo"] + basecmd
        else:
            return basecmd
    except Exception:
        # Something freaky happened, return something that'll probably work
        return basecmd


def make_default_aliases():
    """Creates a new default aliases dictionary."""
    default_aliases = {
        "cd": cd,
        "pushd": pushd,
        "popd": popd,
        "dirs": dirs,
        "jobs": jobs,
        "fg": fg,
        "bg": bg,
        "disown": disown,
        "EOF": xonsh_exit,
        "exit": xonsh_exit,
        "quit": xonsh_exit,
        "exec": xexec,
        "xexec": xexec,
        "source": source_alias,
        "source-zsh": ArgParserAlias(
            func=functools.partial(source_foreign_fn, "zsh", sourcer="source"),
            has_args=True,
            prog="source-zsh",
        ),
        "source-bash": ArgParserAlias(
            func=functools.partial(source_foreign_fn, "bash", sourcer="source"),
            has_args=True,
            prog="source-bash",
        ),
        "source-cmd": source_cmd,
        "source-foreign": source_foreign,
        "history": xhm.history_main,
        "trace": trace,
        "timeit": timeit_alias,
        "xonfig": xonfig,
        "scp-resume": ["rsync", "--partial", "-h", "--progress", "--rsh=ssh"],
        "showcmd": showcmd,
        "ipynb": ["jupyter", "notebook", "--no-browser"],
        "which": xxw.which,
        "xontrib": xontribs_main,
        "completer": xca.completer_alias,
        "xpip": detect_xpip_alias(),
        "xonsh-reset": xonsh_reset,
    }
    if ON_WINDOWS:
        # Borrow builtin commands from cmd.exe.
        windows_cmd_aliases = {
            "cls",
            "copy",
            "del",
            "dir",
            "echo",
            "erase",
            "md",
            "mkdir",
            "mklink",
            "move",
            "rd",
            "ren",
            "rename",
            "rmdir",
            "time",
            "type",
            "vol",
        }
        for alias in windows_cmd_aliases:
            default_aliases[alias] = ["cmd", "/c", alias]
        default_aliases["call"] = ["source-cmd"]
        default_aliases["source-bat"] = ["source-cmd"]
        default_aliases["clear"] = "cls"
        if ON_ANACONDA:
            # Add aliases specific to the Anaconda python distribution.
            default_aliases["activate"] = ["source-cmd", "activate.bat"]
            default_aliases["deactivate"] = ["source-cmd", "deactivate.bat"]
        if not locate_binary("sudo"):
            import xonsh.winutils as winutils

            def sudo(args):
                if len(args) < 1:
                    print(
                        "You need to provide an executable to run as " "Administrator."
                    )
                    return
                cmd = args[0]
                if locate_binary(cmd):
                    return winutils.sudo(cmd, args[1:])
                elif cmd.lower() in windows_cmd_aliases:
                    args = ["/D", "/C", "CD", _get_cwd(), "&&"] + args
                    return winutils.sudo("cmd", args)
                else:
                    msg = 'Cannot find the path for executable "{0}".'
                    print(msg.format(cmd))

            default_aliases["sudo"] = sudo
    elif ON_DARWIN:
        default_aliases["ls"] = ["ls", "-G"]
    elif ON_FREEBSD or ON_DRAGONFLY:
        default_aliases["grep"] = ["grep", "--color=auto"]
        default_aliases["egrep"] = ["egrep", "--color=auto"]
        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
        default_aliases["ls"] = ["ls", "-G"]
    elif ON_NETBSD:
        default_aliases["grep"] = ["grep", "--color=auto"]
        default_aliases["egrep"] = ["egrep", "--color=auto"]
        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
    elif ON_OPENBSD:
        pass
    else:
        default_aliases["grep"] = ["grep", "--color=auto"]
        default_aliases["egrep"] = ["egrep", "--color=auto"]
        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
        default_aliases["ls"] = ["ls", "--color=auto", "-v"]
    return default_aliases

xonsh/aliases.py
"""Aliases for the xonsh shell."""
import argparse
import collections.abc as cabc
import functools
import inspect
import os
import re
import sys
import types
import typing as tp

import xonsh.completers._aliases as xca
import xonsh.history.main as xhm
import xonsh.xoreutils.which as xxw
from xonsh.ast import isexpression
from xonsh.built_ins import XSH
from xonsh.cli_utils import Annotated, Arg, ArgParserAlias
from xonsh.dirstack import _get_cwd, cd, dirs, popd, pushd
from xonsh.environ import locate_binary, make_args_env
from xonsh.foreign_shells import foreign_shell_data
from xonsh.jobs import bg, clean_jobs, disown, fg, jobs
from xonsh.lazyasd import lazyobject
from xonsh.platform import (
    IN_APPIMAGE,
    ON_ANACONDA,
    ON_DARWIN,
    ON_DRAGONFLY,
    ON_FREEBSD,
    ON_NETBSD,
    ON_OPENBSD,
    ON_WINDOWS,
)
from xonsh.timings import timeit_alias
from xonsh.tools import (
    ALIAS_KWARG_NAMES,
    XonshError,
    adjust_shlvl,
    argvquote,
    escape_windows_cmd_string,
    print_color,
    strip_simple_quotes,
    swap_values,
    to_repr_pretty_,
    to_shlvl,
    unthreadable,
)
from xonsh.xontribs import xontribs_main


@lazyobject
def EXEC_ALIAS_RE():
    return re.compile(r"@\(|\$\(|!\(|\$\[|!\[|\&\&|\|\||\s+and\s+|\s+or\s+|[>|<]")


class Aliases(cabc.MutableMapping):
    """Represents a location to hold and look up aliases."""

    def __init__(self, *args, **kwargs):
        self._raw = {}
        self.update(*args, **kwargs)

    @staticmethod
    def _get_func_name(func):
        name = func.__name__

        # Strip leading underscore
        if name.startswith("_"):
            name = name[1:]
        return name

    def _register(self, func, name="", dash_case=True):
        name = name or self._get_func_name(func)

        if dash_case:
            name = name.replace("_", "-")

        self[name] = func
        return func

    @tp.overload
    def register(self, func: types.FunctionType) -> types.FunctionType:
        """simple usage"""

    @tp.overload
    def register(
        self, name: str, *, dash_case: bool = True
    ) -> tp.Callable[[types.FunctionType], types.FunctionType]:
        ...

    def register(self, func_or_name, name=None, dash_case=True):
        """Decorator to register the given function by name."""

        if isinstance(func_or_name, types.FunctionType):
            return self._register(func_or_name, name, dash_case)

        def wrapper(func):
            return self._register(func, func_or_name, dash_case)

        return wrapper

    def get(self, key, default=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.
        """
        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})
        else:
            msg = "alias of {!r} has an inappropriate type: {!r}"
            raise TypeError(msg.format(key, val))

    def eval_alias(self, value, seen_tokens=frozenset(), acc_args=()):
        """
        "Evaluates" the alias ``value``, by recursively looking up the leftmost
        token and "expanding" if it's also an alias.

        A value like ``["cmd", "arg"]`` might transform like this:
        ``> ["cmd", "arg"] -> ["ls", "-al", "arg"] -> callable()``
        where ``cmd=ls -al`` and ``ls`` is an alias with its value being a
        callable.  The resulting callable will be "partially applied" with
        ``["-al", "arg"]``.
        """
        # Beware of mutability: default values for keyword args are evaluated
        # only once.
        if callable(value):
            return partial_eval_alias(value, acc_args=acc_args)
        else:
            expand_path = XSH.expand_path
            token, *rest = map(expand_path, value)
            if token in seen_tokens or token not in self._raw:
                # ^ Making sure things like `egrep=egrep --color=auto` works,
                # and that `l` evals to `ls --color=auto -CF` if `l=ls -CF`
                # and `ls=ls --color=auto`
                rtn = [token]
                rtn.extend(rest)
                rtn.extend(acc_args)
                return rtn
            else:
                seen_tokens = seen_tokens | {token}
                acc_args = rest + list(acc_args)
                return self.eval_alias(self._raw[token], seen_tokens, acc_args)

    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.
        The command won't be expanded if the cursor's inside/behind it.
        """
        word = (line.split(maxsplit=1) or [""])[0]
        if word in XSH.aliases and isinstance(self.get(word), cabc.Sequence):  # type: ignore
            word_idx = line.find(word)
            word_edge = word_idx + len(word)
            if cursor_index > word_edge:
                # the cursor isn't inside/behind the word
                expansion = " ".join(self.get(word))
                line = line[:word_idx] + expansion + line[word_edge:]
        return line

    #
    # Mutable mapping interface
    #

    def __getitem__(self, key):
        return self._raw[key]

    def __setitem__(self, key, val):
        if isinstance(val, str):
            f = "<exec-alias:" + key + ">"
            if EXEC_ALIAS_RE.search(val) is not None:
                # We have a sub-command (e.g. $(cmd)) or IO redirect (e.g. >>)
                self._raw[key] = ExecAlias(val, filename=f)
            elif isexpression(val):
                # expansion substitution
                lexer = XSH.execer.parser.lexer
                self._raw[key] = list(map(strip_simple_quotes, lexer.split(val)))
            else:
                # need to exec alias
                self._raw[key] = ExecAlias(val, filename=f)
        else:
            self._raw[key] = val

    def _common_or(self, other):
        new_dict = self._raw.copy()
        for key in dict(other):
            new_dict[key] = other[key]
        return Aliases(new_dict)

    def __or__(self, other):
        return self._common_or(other)

    def __ror__(self, other):
        return self._common_or(other)

    def __ior__(self, other):
        for key in dict(other):
            self[key] = other[key]
        return self

    def __delitem__(self, key):
        del self._raw[key]

    def update(self, *args, **kwargs):
        for key, val in dict(*args, **kwargs).items():
            self[key] = val

    def __iter__(self):
        yield from self._raw

    def __len__(self):
        return len(self._raw)

    def __str__(self):
        return str(self._raw)

    def __repr__(self):
        return "{}.{}({})".format(
            self.__class__.__module__, self.__class__.__name__, self._raw
        )

    _repr_pretty_ = to_repr_pretty_


class ExecAlias:
    """Provides a callable alias for xonsh source code."""

    def __init__(self, src, filename="<exec-alias>"):
        """
        Parameters
        ----------
        src : str
            Source code that will be
        """
        self.src = src
        self.filename = filename

    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        execer = XSH.execer
        frame = stack[0][0]  # execute as though we are at the call site

        alias_args = {"args": args}
        for i, a in enumerate(args):
            alias_args[f"arg{i}"] = a

        with XSH.env.swap(alias_args):
            execer.exec(
                self.src,
                glbs=frame.f_globals,
                locs=frame.f_locals,
                filename=self.filename,
            )
        if XSH.history is not None:
            return XSH.history.last_cmd_rtn

    def __repr__(self):
        return f"ExecAlias({self.src!r}, filename={self.filename!r})"


class PartialEvalAliasBase:
    """Partially evaluated alias."""

    def __init__(self, f, acc_args=()):
        """
        Parameters
        ----------
        f : callable
            A function to dispatch to.
        acc_args : sequence of strings, optional
            Additional arguments to prepent to the argument list passed in
            when the alias is called.
        """
        self.f = f
        self.acc_args = acc_args
        self.__name__ = getattr(f, "__name__", self.__class__.__name__)

    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr, spec, stack)

    def __repr__(self):
        return "{name}({f!r}, acc_args={acc_args!r})".format(
            name=self.__class__.__name__, f=self.f, acc_args=self.acc_args
        )


class PartialEvalAlias0(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        if args:
            msg = "callable alias {f!r} takes no arguments, but {args!f} provided. "
            msg += "Of these {acc_args!r} were partially applied."
            raise XonshError(msg.format(f=self.f, args=args, acc_args=self.acc_args))
        return self.f()


class PartialEvalAlias1(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args)


class PartialEvalAlias2(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin)


class PartialEvalAlias3(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout)


class PartialEvalAlias4(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr)


class PartialEvalAlias5(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr, spec)


class PartialEvalAlias6(PartialEvalAliasBase):
    def __call__(
        self, args, stdin=None, stdout=None, stderr=None, spec=None, stack=None
    ):
        args = list(self.acc_args) + args
        return self.f(args, stdin, stdout, stderr, spec, stack)


PARTIAL_EVAL_ALIASES = (
    PartialEvalAlias0,
    PartialEvalAlias1,
    PartialEvalAlias2,
    PartialEvalAlias3,
    PartialEvalAlias4,
    PartialEvalAlias5,
    PartialEvalAlias6,
)


def partial_eval_alias(f, acc_args=()):
    """Dispatches the appropriate eval alias based on the number of args to the original callable alias
    and how many arguments to apply.
    """
    # no partial needed if no extra args
    if not acc_args:
        return f
    # need to dispatch
    numargs = 0
    for name, param in inspect.signature(f).parameters.items():
        if (
            param.kind == param.POSITIONAL_ONLY
            or param.kind == param.POSITIONAL_OR_KEYWORD
        ):
            numargs += 1
        elif name in ALIAS_KWARG_NAMES and param.kind == param.KEYWORD_ONLY:
            numargs += 1
    if numargs < 7:
        return PARTIAL_EVAL_ALIASES[numargs](f, acc_args=acc_args)
    else:
        e = "Expected proxy with 6 or fewer arguments for {}, not {}"
        raise XonshError(e.format(", ".join(ALIAS_KWARG_NAMES), numargs))


#
# Actual aliases below
#


def xonsh_exit(args, stdin=None):
    """Sends signal to exit shell."""
    if not clean_jobs():
        # Do not exit if jobs not cleaned up
        return None, None
    XSH.exit = True
    print()  # gimme a newline
    return None, None


def xonsh_reset(args, stdin=None):
    """Clears __xonsh__.ctx"""
    XSH.ctx.clear()


def source_foreign_fn(
    shell: str,
    files_or_code: Annotated[tp.List[str], Arg(nargs="+")],
    interactive=True,
    login=False,
    envcmd=None,
    aliascmd=None,
    extra_args="",
    safe=True,
    prevcmd="",
    postcmd="",
    funcscmd="",
    sourcer=None,
    use_tmpfile=False,
    seterrprevcmd=None,
    seterrpostcmd=None,
    overwrite_aliases=False,
    suppress_skip_message=False,
    show=False,
    dryrun=False,
    _stderr=None,
):
    """Sources a file written in a foreign shell language.

    Parameters
    ----------
    shell
        Name or path to the foreign shell
    files_or_code
        file paths to source or code in the target language.
    interactive : -n, --non-interactive
        whether the sourced shell should be interactive
    login : -l, --login
        whether the sourced shell should be login
    envcmd : --envcmd
        command to print environment
    aliascmd : --aliascmd
        command to print aliases
    extra_args : --extra-args
        extra arguments needed to run the shell
    safe : -u, --unsafe
        whether the source shell should be run safely, and not raise any errors, even if they occur.
    prevcmd : -p, --prevcmd
        command(s) to run before any other commands, replaces traditional source.
    postcmd : --postcmd
        command(s) to run after all other commands
    funcscmd : --funcscmd
        code to find locations of all native functions in the shell language.
    sourcer : --sourcer
        the source command in the target shell language.
        If this is not set, a default value will attempt to be
        looked up based on the shell name.
    use_tmpfile : --use-tmpfile
        whether the commands for source shell should be written to a temporary file.
    seterrprevcmd : --seterrprevcmd
        command(s) to set exit-on-error before any other commands.
    seterrpostcmd : --seterrpostcmd
        command(s) to set exit-on-error after all other commands.
    overwrite_aliases : --overwrite-aliases
        flag for whether or not sourced aliases should replace the current xonsh aliases.
    suppress_skip_message : --suppress-skip-message
        flag for whether or not skip messages should be suppressed.
    show : --show
        show the script output.
    dryrun : -d, --dry-run
        Will not actually source the file.
    """
    extra_args = tuple(extra_args.split())
    env = XSH.env
    suppress_skip_message = (
        env.get("FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE")
        if not suppress_skip_message
        else suppress_skip_message
    )
    files: tp.Tuple[str, ...] = ()
    if prevcmd:
        pass  # don't change prevcmd if given explicitly
    elif os.path.isfile(files_or_code[0]):
        if not sourcer:
            return (None, "xonsh: error: `sourcer` command is not mentioned.\n", 1)
        # we have filenames to source
        prevcmd = "".join([f"{sourcer} {f}\n" for f in files_or_code])
        files = tuple(files_or_code)
    elif not prevcmd:
        prevcmd = " ".join(files_or_code)  # code to run, no files
    foreign_shell_data.cache_clear()  # make sure that we don't get prev src
    fsenv, fsaliases = foreign_shell_data(
        shell=shell,
        login=login,
        interactive=interactive,
        envcmd=envcmd,
        aliascmd=aliascmd,
        extra_args=extra_args,
        safe=safe,
        prevcmd=prevcmd,
        postcmd=postcmd,
        funcscmd=funcscmd or None,  # the default is None in the called function
        sourcer=sourcer,
        use_tmpfile=use_tmpfile,
        seterrprevcmd=seterrprevcmd,
        seterrpostcmd=seterrpostcmd,
        show=show,
        dryrun=dryrun,
        files=files,
    )
    if fsenv is None:
        if dryrun:
            return
        else:
            msg = f"xonsh: error: Source failed: {prevcmd!r}\n"
            msg += "xonsh: error: Possible reasons: File not found or syntax error\n"
            return (None, msg, 1)
    # apply results
    denv = env.detype()
    for k, v in fsenv.items():
        if k == "SHLVL":  # ignore $SHLVL as sourcing should not change $SHLVL
            continue
        if k in denv and v == denv[k]:
            continue  # no change from original
        env[k] = v
    # Remove any env-vars that were unset by the script.
    for k in denv:
        if k not in fsenv:
            env.pop(k, None)
    # Update aliases
    baliases = XSH.aliases
    for k, v in fsaliases.items():
        if k in baliases and v == baliases[k]:
            continue  # no change from original
        elif overwrite_aliases or k not in baliases:
            baliases[k] = v
        elif suppress_skip_message:
            pass
        else:
            msg = (
                "Skipping application of {0!r} alias from {1!r} "
                "since it shares a name with an existing xonsh alias. "
                'Use "--overwrite-alias" option to apply it anyway.'
                'You may prevent this message with "--suppress-skip-message" or '
                '"$FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE = True".'
            )
            print(msg.format(k, shell), file=_stderr)


source_foreign = ArgParserAlias(
    func=source_foreign_fn, has_args=True, prog="source-foreign"
)


@unthreadable
def source_alias(args, stdin=None):
    """Executes the contents of the provided files in the current context.
    If sourced file isn't found in cwd, search for file along $PATH to source
    instead.
    """
    env = XSH.env
    encoding = env.get("XONSH_ENCODING")
    errors = env.get("XONSH_ENCODING_ERRORS")
    for i, fname in enumerate(args):
        fpath = fname
        if not os.path.isfile(fpath):
            fpath = locate_binary(fname)
            if fpath is None:
                if env.get("XONSH_DEBUG"):
                    print(f"source: {fname}: No such file", file=sys.stderr)
                if i == 0:
                    raise RuntimeError(
                        "must source at least one file, " + fname + " does not exist."
                    )
                break
        _, fext = os.path.splitext(fpath)
        if fext and fext != ".xsh" and fext != ".py":
            raise RuntimeError(
                "attempting to source non-xonsh file! If you are "
                "trying to source a file in another language, "
                "then please use the appropriate source command. "
                "For example, source-bash script.sh"
            )
        with open(fpath, encoding=encoding, errors=errors) as fp:
            src = fp.read()
        if not src.endswith("\n"):
            src += "\n"
        ctx = XSH.ctx
        updates = {"__file__": fpath, "__name__": os.path.abspath(fpath)}
        with env.swap(**make_args_env(args[i + 1 :])), swap_values(ctx, updates):
            try:
                XSH.builtins.execx(src, "exec", ctx, filename=fpath)
            except Exception:
                print_color(
                    "{RED}You may be attempting to source non-xonsh file! "
                    "{RESET}If you are trying to source a file in "
                    "another language, then please use the appropriate "
                    "source command. For example, {GREEN}source-bash "
                    "script.sh{RESET}",
                    file=sys.stderr,
                )
                raise


def source_cmd_fn(
    files: Annotated[tp.List[str], Arg(nargs="+")],
    login=False,
    aliascmd=None,
    extra_args="",
    safe=True,
    postcmd="",
    funcscmd="",
    seterrprevcmd=None,
    overwrite_aliases=False,
    suppress_skip_message=False,
    show=False,
    dryrun=False,
    _stderr=None,
):
    """
        Source cmd.exe files

    Parameters
    ----------
    files
        paths to source files.
    login : -l, --login
        whether the sourced shell should be login
    envcmd : --envcmd
        command to print environment
    aliascmd : --aliascmd
        command to print aliases
    extra_args : --extra-args
        extra arguments needed to run the shell
    safe : -s, --safe
        whether the source shell should be run safely, and not raise any errors, even if they occur.
    postcmd : --postcmd
        command(s) to run after all other commands
    funcscmd : --funcscmd
        code to find locations of all native functions in the shell language.
    seterrprevcmd : --seterrprevcmd
        command(s) to set exit-on-error before any other commands.
    overwrite_aliases : --overwrite-aliases
        flag for whether or not sourced aliases should replace the current xonsh aliases.
    suppress_skip_message : --suppress-skip-message
        flag for whether or not skip messages should be suppressed.
    show : --show
        show the script output.
    dryrun : -d, --dry-run
        Will not actually source the file.
    """
    args = list(files)
    fpath = locate_binary(args[0])
    args[0] = fpath if fpath else args[0]
    if not os.path.isfile(args[0]):
        return (None, f"xonsh: error: File not found: {args[0]}\n", 1)
    prevcmd = "call "
    prevcmd += " ".join([argvquote(arg, force=True) for arg in args])
    prevcmd = escape_windows_cmd_string(prevcmd)
    with XSH.env.swap(PROMPT="$P$G"):
        return source_foreign_fn(
            shell="cmd",
            files_or_code=args,
            interactive=True,
            sourcer="call",
            envcmd="set",
            seterrpostcmd="if errorlevel 1 exit 1",
            use_tmpfile=True,
            prevcmd=prevcmd,
            #     from this function
            login=login,
            aliascmd=aliascmd,
            extra_args=extra_args,
            safe=safe,
            postcmd=postcmd,
            funcscmd=funcscmd,
            seterrprevcmd=seterrprevcmd,
            overwrite_aliases=overwrite_aliases,
            suppress_skip_message=suppress_skip_message,
            show=show,
            dryrun=dryrun,
        )


source_cmd = ArgParserAlias(func=source_cmd_fn, has_args=True, prog="source-cmd")


def xexec_fn(
    command: Annotated[tp.List[str], Arg(nargs=argparse.REMAINDER)],
    login=False,
    clean=False,
    name="",
    _stdin=None,
):
    """exec (also aliased as xexec) uses the os.execvpe() function to
    replace the xonsh process with the specified program.

    This provides the functionality of the bash 'exec' builtin::

        >>> exec bash -l -i
        bash $

    Parameters
    ----------
    command
        program to launch along its arguments
    login : -l, --login
        the shell places a dash at the
        beginning of the zeroth argument passed to command to simulate login
        shell.
    clean : -c, --clean
        causes command to be executed with an empty environment.
    name : -a, --name
        the shell passes name as the zeroth argument
        to the executed command.

    Notes
    -----
    This command **is not** the same as the Python builtin function
    exec(). That function is for running Python code. This command,
    which shares the same name as the sh-lang statement, is for launching
    a command directly in the same process. In the event of a name conflict,
    please use the xexec command directly or dive into subprocess mode
    explicitly with ![exec command]. For more details, please see
    http://xon.sh/faq.html#exec.
    """
    if len(command) == 0:
        return (None, "xonsh: exec: no command specified\n", 1)

    cmd = command[0]
    if name:
        command[0] = name
    if login:
        command[0] = f"-{command[0]}"

    denv = {}
    if not clean:
        denv = XSH.env.detype()

        # decrement $SHLVL to mirror bash's behaviour
        if "SHLVL" in denv:
            old_shlvl = to_shlvl(denv["SHLVL"])
            denv["SHLVL"] = str(adjust_shlvl(old_shlvl, -1))

    try:
        os.execvpe(cmd, command, denv)
    except FileNotFoundError as e:
        return (
            None,
            "xonsh: exec: file not found: {}: {}" "\n".format(e.args[1], command[0]),
            1,
        )


xexec = ArgParserAlias(func=xexec_fn, has_args=True, prog="xexec")


@lazyobject
def xonfig():
    """Runs the xonsh configuration utility."""
    from xonsh.xonfig import xonfig_main  # lazy import

    return xonfig_main


@unthreadable
def trace(args, stdin=None, stdout=None, stderr=None, spec=None):
    """Runs the xonsh tracer utility."""
    from xonsh.tracer import tracermain  # lazy import

    try:
        return tracermain(args, stdin=stdin, stdout=stdout, stderr=stderr, spec=spec)
    except SystemExit:
        pass


def showcmd(args, stdin=None):
    """usage: showcmd [-h|--help|cmd args]

    Displays the command and arguments as a list of strings that xonsh would
    run in subprocess mode. This is useful for determining how xonsh evaluates
    your commands and arguments prior to running these commands.

    optional arguments:
      -h, --help            show this help message and exit

    Examples
    --------
      >>> showcmd echo $USER "can't" hear "the sea"
      ['echo', 'I', "can't", 'hear', 'the sea']
    """
    if len(args) == 0 or (len(args) == 1 and args[0] in {"-h", "--help"}):
        print(showcmd.__doc__.rstrip().replace("\n    ", "\n"))
    else:
        sys.displayhook(args)


def detect_xpip_alias():
    """
    Determines the correct invocation to get xonsh's pip
    """
    if not getattr(sys, "executable", None):
        return lambda args, stdin=None: (
            "",
            "Sorry, unable to run pip on your system (missing sys.executable)",
            1,
        )

    basecmd = [sys.executable, "-m", "pip"]
    try:
        if ON_WINDOWS or IN_APPIMAGE:
            # XXX: Does windows have an installation mode that requires UAC?
            return basecmd
        elif not os.access(os.path.dirname(sys.executable), os.W_OK):
            return basecmd.extend(["--user"])
        else:
            return basecmd
    except Exception:
        # Something freaky happened, return something that'll probably work
        return basecmd


def make_default_aliases():
    """Creates a new default aliases dictionary."""
    default_aliases = {
        "cd": cd,
        "pushd": pushd,
        "popd": popd,
        "dirs": dirs,
        "jobs": jobs,
        "fg": fg,
        "bg": bg,
        "disown": disown,
        "EOF": xonsh_exit,
        "exit": xonsh_exit,
        "quit": xonsh_exit,
        "exec": xexec,
        "xexec": xexec,
        "source": source_alias,
        "source-zsh": ArgParserAlias(
            func=functools.partial(source_foreign_fn, "zsh", sourcer="source"),
            has_args=True,
            prog="source-zsh",
        ),
        "source-bash": ArgParserAlias(
            func=functools.partial(source_foreign_fn, "bash", sourcer="source"),
            has_args=True,
            prog="source-bash",
        ),
        "source-cmd": source_cmd,
        "source-foreign": source_foreign,
        "history": xhm.history_main,
        "trace": trace,
        "timeit": timeit_alias,
        "xonfig": xonfig,
        "scp-resume": ["rsync", "--partial", "-h", "--progress", "--rsh=ssh"],
        "showcmd": showcmd,
        "ipynb": ["jupyter", "notebook", "--no-browser"],
        "which": xxw.which,
        "xontrib": xontribs_main,
        "completer": xca.completer_alias,
        "xpip": detect_xpip_alias(),
        "xonsh-reset": xonsh_reset,
    }
    if ON_WINDOWS:
        # Borrow builtin commands from cmd.exe.
        windows_cmd_aliases = {
            "cls",
            "copy",
            "del",
            "dir",
            "echo",
            "erase",
            "md",
            "mkdir",
            "mklink",
            "move",
            "rd",
            "ren",
            "rename",
            "rmdir",
            "time",
            "type",
            "vol",
        }
        for alias in windows_cmd_aliases:
            default_aliases[alias] = ["cmd", "/c", alias]
        default_aliases["call"] = ["source-cmd"]
        default_aliases["source-bat"] = ["source-cmd"]
        default_aliases["clear"] = "cls"
        if ON_ANACONDA:
            # Add aliases specific to the Anaconda python distribution.
            default_aliases["activate"] = ["source-cmd", "activate.bat"]
            default_aliases["deactivate"] = ["source-cmd", "deactivate.bat"]
        if not locate_binary("sudo"):
            import xonsh.winutils as winutils

            def sudo(args):
                if len(args) < 1:
                    print(
                        "You need to provide an executable to run as " "Administrator."
                    )
                    return
                cmd = args[0]
                if locate_binary(cmd):
                    return winutils.sudo(cmd, args[1:])
                elif cmd.lower() in windows_cmd_aliases:
                    args = ["/D", "/C", "CD", _get_cwd(), "&&"] + args
                    return winutils.sudo("cmd", args)
                else:
                    msg = 'Cannot find the path for executable "{0}".'
                    print(msg.format(cmd))

            default_aliases["sudo"] = sudo
    elif ON_DARWIN:
        default_aliases["ls"] = ["ls", "-G"]
    elif ON_FREEBSD or ON_DRAGONFLY:
        default_aliases["grep"] = ["grep", "--color=auto"]
        default_aliases["egrep"] = ["egrep", "--color=auto"]
        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
        default_aliases["ls"] = ["ls", "-G"]
    elif ON_NETBSD:
        default_aliases["grep"] = ["grep", "--color=auto"]
        default_aliases["egrep"] = ["egrep", "--color=auto"]
        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
    elif ON_OPENBSD:
        pass
    else:
        default_aliases["grep"] = ["grep", "--color=auto"]
        default_aliases["egrep"] = ["egrep", "--color=auto"]
        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
        default_aliases["ls"] = ["ls", "--color=auto", "-v"]
    return default_aliases

xonsh/aliases.py
diff --git a/news/xpip_perms.rst b/news/xpip_perms.rst
new file mode 100644
index 0000000000..9921f44628
--- /dev/null
+++ b/news/xpip_perms.rst
@@ -0,0 +1,24 @@
+**Added:**
+
+* <news item>
+
+**Changed:**
+
+* <news item>
+
+**Deprecated:**
+
+* <news item>
+
+**Removed:**
+
+* <news item>
+
+**Fixed:**
+
+* <news item>
+
+**Security:**
+
+* ``xpip`` will never add ``sudo`` under any circumstances and will instead append ``--user`` as needed
+
diff --git a/xonsh/aliases.py b/xonsh/aliases.py
index 913af0a9ff..3fc50a17d9 100644
--- a/xonsh/aliases.py
+++ b/xonsh/aliases.py
@@ -815,7 +815,7 @@ def detect_xpip_alias():
             # XXX: Does windows have an installation mode that requires UAC?
             return basecmd
         elif not os.access(os.path.dirname(sys.executable), os.W_OK):
-            return ["sudo"] + basecmd
+            return basecmd.extend(["--user"])
         else:
             return basecmd
     except Exception:

xpip doesn't detect/support "pip install --user" installs of xonsh
## xonfig

<details>

```
+------------------+----------------------+
| xonsh            | 0.9.27               |
| Git SHA          | 71fe9014             |
| Commit Date      | Jan 29 08:58:58 2021 |
| Python           | 3.9.5                |
| PLY              | 3.11                 |
| have readline    | True                 |
| prompt toolkit   | 3.0.19               |
| shell type       | prompt_toolkit       |
| pygments         | 2.9.0                |
| on posix         | True                 |
| on linux         | True                 |
| distro           | ubuntu               |
| on darwin        | False                |
| on windows       | False                |
| on cygwin        | False                |
| on msys2         | False                |
| is superuser     | False                |
| default encoding | utf-8                |
| xonsh encoding   | utf-8                |
| encoding errors  | surrogateescape      |
| on jupyter       | False                |
| jupyter kernel   | None                 |
| xontrib 1        | apt_tabcomplete      |
| xontrib 2        | direnv               |
| xontrib 3        | kitty                |
| xontrib 4        | linuxbrew            |
+------------------+----------------------+
```

</details>

## Expected Behavior
After installing xonsh via `pip3 install --user xonsh` (and ensuring that `~/.local/bin` is on `$PATH`, etc), xonsh works and runs just fine. Since `xpip` is supposed to manage the Python environment where xonsh itself is defined, I would expect it to wrap a non-root `pip`, ideally invoked in a way to install in the user's dir.

## Current Behavior
```
$ which xpip
sudo /usr/bin/python3 -m pip
```
Instead, `xpip` wraps a `sudo` invocation that will install things globally systemwide, which is not at all how xonsh itself was installed. And, if the user tries to do something "smart" like `xpip install --user xontrib-whatever`, I'm not sure quite what it will do but surely nothing good.

## Steps to Reproduce
1. Install xonsh via `pip3 install --user xonsh`
2. Run `xpip` to install something like a xonfig
3. Sadness and an unexpected `sudo` that might do undesired things to your system

## For community
⬇️  **Please click the 👍 reaction instead of leaving a `+1` or 👍  comment**
