1# -*- coding: utf-8 -*-
2"""Aliases for the xonsh shell."""
3import os
4import sys
5import shlex
6import inspect
7import argparse
8import builtins
9import collections.abc as cabc
10
11from xonsh.lazyasd import lazyobject
12from xonsh.dirstack import cd, pushd, popd, dirs, _get_cwd
13from xonsh.environ import locate_binary, make_args_env
14from xonsh.foreign_shells import foreign_shell_data
15from xonsh.jobs import jobs, fg, bg, clean_jobs
16from xonsh.platform import ON_ANACONDA, ON_DARWIN, ON_WINDOWS, ON_FREEBSD, ON_NETBSD
17from xonsh.tools import unthreadable, print_color
18from xonsh.replay import replay_main
19from xonsh.timings import timeit_alias
20from xonsh.tools import argvquote, escape_windows_cmd_string, to_bool, swap_values
21from xonsh.xontribs import xontribs_main
22
23import xonsh.completers._aliases as xca
24import xonsh.history.main as xhm
25import xonsh.xoreutils.which as xxw
26
27
28class Aliases(cabc.MutableMapping):
29    """Represents a location to hold and look up aliases."""
30
31    def __init__(self, *args, **kwargs):
32        self._raw = {}
33        self.update(*args, **kwargs)
34
35    def get(self, key, default=None):
36        """Returns the (possibly modified) value. If the key is not present,
37        then `default` is returned.
38        If the value is callable, it is returned without modification. If it
39        is an iterable of strings it will be evaluated recursively to expand
40        other aliases, resulting in a new list or a "partially applied"
41        callable.
42        """
43        val = self._raw.get(key)
44        if val is None:
45            return default
46        elif isinstance(val, cabc.Iterable) or callable(val):
47            return self.eval_alias(val, seen_tokens={key})
48        else:
49            msg = "alias of {!r} has an inappropriate type: {!r}"
50            raise TypeError(msg.format(key, val))
51
52    def eval_alias(self, value, seen_tokens=frozenset(), acc_args=()):
53        """
54        "Evaluates" the alias `value`, by recursively looking up the leftmost
55        token and "expanding" if it's also an alias.
56
57        A value like ["cmd", "arg"] might transform like this:
58        > ["cmd", "arg"] -> ["ls", "-al", "arg"] -> callable()
59        where `cmd=ls -al` and `ls` is an alias with its value being a
60        callable.  The resulting callable will be "partially applied" with
61        ["-al", "arg"].
62        """
63        # Beware of mutability: default values for keyword args are evaluated
64        # only once.
65        if callable(value):
66            if acc_args:  # Partial application
67
68                def _alias(args, stdin=None):
69                    args = list(acc_args) + args
70                    return value(args, stdin=stdin)
71
72                return _alias
73            else:
74                return value
75        else:
76            expand_path = builtins.__xonsh_expand_path__
77            token, *rest = map(expand_path, value)
78            if token in seen_tokens or token not in self._raw:
79                # ^ Making sure things like `egrep=egrep --color=auto` works,
80                # and that `l` evals to `ls --color=auto -CF` if `l=ls -CF`
81                # and `ls=ls --color=auto`
82                rtn = [token]
83                rtn.extend(rest)
84                rtn.extend(acc_args)
85                return rtn
86            else:
87                seen_tokens = seen_tokens | {token}
88                acc_args = rest + list(acc_args)
89                return self.eval_alias(self._raw[token], seen_tokens, acc_args)
90
91    def expand_alias(self, line):
92        """Expands any aliases present in line if alias does not point to a
93        builtin function and if alias is only a single command.
94        """
95        word = line.split(" ", 1)[0]
96        if word in builtins.aliases and isinstance(self.get(word), cabc.Sequence):
97            word_idx = line.find(word)
98            expansion = " ".join(self.get(word))
99            line = line[:word_idx] + expansion + line[word_idx + len(word) :]
100        return line
101
102    #
103    # Mutable mapping interface
104    #
105
106    def __getitem__(self, key):
107        return self._raw[key]
108
109    def __setitem__(self, key, val):
110        if isinstance(val, str):
111            self._raw[key] = shlex.split(val)
112        else:
113            self._raw[key] = val
114
115    def __delitem__(self, key):
116        del self._raw[key]
117
118    def update(self, *args, **kwargs):
119        for key, val in dict(*args, **kwargs).items():
120            self[key] = val
121
122    def __iter__(self):
123        yield from self._raw
124
125    def __len__(self):
126        return len(self._raw)
127
128    def __str__(self):
129        return str(self._raw)
130
131    def __repr__(self):
132        return "{0}.{1}({2})".format(
133            self.__class__.__module__, self.__class__.__name__, self._raw
134        )
135
136    def _repr_pretty_(self, p, cycle):
137        name = "{0}.{1}".format(self.__class__.__module__, self.__class__.__name__)
138        with p.group(0, name + "(", ")"):
139            if cycle:
140                p.text("...")
141            elif len(self):
142                p.break_()
143                p.pretty(dict(self))
144
145
146def xonsh_exit(args, stdin=None):
147    """Sends signal to exit shell."""
148    if not clean_jobs():
149        # Do not exit if jobs not cleaned up
150        return None, None
151    builtins.__xonsh_exit__ = True
152    print()  # gimme a newline
153    return None, None
154
155
156def xonsh_reset(args, stdin=None):
157    """ Clears __xonsh_ctx__"""
158    builtins.__xonsh_ctx__.clear()
159
160
161@lazyobject
162def _SOURCE_FOREIGN_PARSER():
163    desc = "Sources a file written in a foreign shell language."
164    parser = argparse.ArgumentParser("source-foreign", description=desc)
165    parser.add_argument("shell", help="Name or path to the foreign shell")
166    parser.add_argument(
167        "files_or_code",
168        nargs="+",
169        help="file paths to source or code in the target " "language.",
170    )
171    parser.add_argument(
172        "-i",
173        "--interactive",
174        type=to_bool,
175        default=True,
176        help="whether the sourced shell should be interactive",
177        dest="interactive",
178    )
179    parser.add_argument(
180        "-l",
181        "--login",
182        type=to_bool,
183        default=False,
184        help="whether the sourced shell should be login",
185        dest="login",
186    )
187    parser.add_argument(
188        "--envcmd", default=None, dest="envcmd", help="command to print environment"
189    )
190    parser.add_argument(
191        "--aliascmd", default=None, dest="aliascmd", help="command to print aliases"
192    )
193    parser.add_argument(
194        "--extra-args",
195        default=(),
196        dest="extra_args",
197        type=(lambda s: tuple(s.split())),
198        help="extra arguments needed to run the shell",
199    )
200    parser.add_argument(
201        "-s",
202        "--safe",
203        type=to_bool,
204        default=True,
205        help="whether the source shell should be run safely, "
206        "and not raise any errors, even if they occur.",
207        dest="safe",
208    )
209    parser.add_argument(
210        "-p",
211        "--prevcmd",
212        default=None,
213        dest="prevcmd",
214        help="command(s) to run before any other commands, "
215        "replaces traditional source.",
216    )
217    parser.add_argument(
218        "--postcmd",
219        default="",
220        dest="postcmd",
221        help="command(s) to run after all other commands",
222    )
223    parser.add_argument(
224        "--funcscmd",
225        default=None,
226        dest="funcscmd",
227        help="code to find locations of all native functions " "in the shell language.",
228    )
229    parser.add_argument(
230        "--sourcer",
231        default=None,
232        dest="sourcer",
233        help="the source command in the target shell " "language, default: source.",
234    )
235    parser.add_argument(
236        "--use-tmpfile",
237        type=to_bool,
238        default=False,
239        help="whether the commands for source shell should be "
240        "written to a temporary file.",
241        dest="use_tmpfile",
242    )
243    parser.add_argument(
244        "--seterrprevcmd",
245        default=None,
246        dest="seterrprevcmd",
247        help="command(s) to set exit-on-error before any" "other commands.",
248    )
249    parser.add_argument(
250        "--seterrpostcmd",
251        default=None,
252        dest="seterrpostcmd",
253        help="command(s) to set exit-on-error after all" "other commands.",
254    )
255    parser.add_argument(
256        "--overwrite-aliases",
257        default=False,
258        action="store_true",
259        dest="overwrite_aliases",
260        help="flag for whether or not sourced aliases should "
261        "replace the current xonsh aliases.",
262    )
263    parser.add_argument(
264        "--suppress-skip-message",
265        default=None,
266        action="store_true",
267        dest="suppress_skip_message",
268        help="flag for whether or not skip messages should be suppressed.",
269    )
270    parser.add_argument(
271        "--show",
272        default=False,
273        action="store_true",
274        dest="show",
275        help="Will show the script output.",
276    )
277    parser.add_argument(
278        "-d",
279        "--dry-run",
280        default=False,
281        action="store_true",
282        dest="dryrun",
283        help="Will not actually source the file.",
284    )
285    return parser
286
287
288def source_foreign(args, stdin=None, stdout=None, stderr=None):
289    """Sources a file written in a foreign shell language."""
290    env = builtins.__xonsh_env__
291    ns = _SOURCE_FOREIGN_PARSER.parse_args(args)
292    ns.suppress_skip_message = (
293        env.get("FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE")
294        if ns.suppress_skip_message is None
295        else ns.suppress_skip_message
296    )
297    if ns.prevcmd is not None:
298        pass  # don't change prevcmd if given explicitly
299    elif os.path.isfile(ns.files_or_code[0]):
300        # we have filename to source
301        ns.prevcmd = '{} "{}"'.format(ns.sourcer, '" "'.join(ns.files_or_code))
302    elif ns.prevcmd is None:
303        ns.prevcmd = " ".join(ns.files_or_code)  # code to run, no files
304    foreign_shell_data.cache_clear()  # make sure that we don't get prev src
305    fsenv, fsaliases = foreign_shell_data(
306        shell=ns.shell,
307        login=ns.login,
308        interactive=ns.interactive,
309        envcmd=ns.envcmd,
310        aliascmd=ns.aliascmd,
311        extra_args=ns.extra_args,
312        safe=ns.safe,
313        prevcmd=ns.prevcmd,
314        postcmd=ns.postcmd,
315        funcscmd=ns.funcscmd,
316        sourcer=ns.sourcer,
317        use_tmpfile=ns.use_tmpfile,
318        seterrprevcmd=ns.seterrprevcmd,
319        seterrpostcmd=ns.seterrpostcmd,
320        show=ns.show,
321        dryrun=ns.dryrun,
322    )
323    if fsenv is None:
324        if ns.dryrun:
325            return
326        else:
327            msg = "xonsh: error: Source failed: {0!r}\n".format(ns.prevcmd)
328            msg += "xonsh: error: Possible reasons: File not found or syntax error\n"
329            return (None, msg, 1)
330    # apply results
331    denv = env.detype()
332    for k, v in fsenv.items():
333        if k in denv and v == denv[k]:
334            continue  # no change from original
335        env[k] = v
336    # Remove any env-vars that were unset by the script.
337    for k in denv:
338        if k not in fsenv:
339            env.pop(k, None)
340    # Update aliases
341    baliases = builtins.aliases
342    for k, v in fsaliases.items():
343        if k in baliases and v == baliases[k]:
344            continue  # no change from original
345        elif ns.overwrite_aliases or k not in baliases:
346            baliases[k] = v
347        elif ns.suppress_skip_message:
348            pass
349        else:
350            msg = (
351                "Skipping application of {0!r} alias from {1!r} "
352                "since it shares a name with an existing xonsh alias. "
353                'Use "--overwrite-alias" option to apply it anyway.'
354                'You may prevent this message with "--suppress-skip-message" or '
355                '"$FOREIGN_ALIASES_SUPPRESS_SKIP_MESSAGE = True".'
356            )
357            print(msg.format(k, ns.shell), file=stderr)
358
359
360def source_alias(args, stdin=None):
361    """Executes the contents of the provided files in the current context.
362    If sourced file isn't found in cwd, search for file along $PATH to source
363    instead.
364    """
365    env = builtins.__xonsh_env__
366    encoding = env.get("XONSH_ENCODING")
367    errors = env.get("XONSH_ENCODING_ERRORS")
368    for i, fname in enumerate(args):
369        fpath = fname
370        if not os.path.isfile(fpath):
371            fpath = locate_binary(fname)
372            if fpath is None:
373                if env.get("XONSH_DEBUG"):
374                    print("source: {}: No such file".format(fname), file=sys.stderr)
375                if i == 0:
376                    raise RuntimeError(
377                        "must source at least one file, " + fname + "does not exist."
378                    )
379                break
380        _, fext = os.path.splitext(fpath)
381        if fext and fext != ".xsh" and fext != ".py":
382            raise RuntimeError(
383                "attempting to source non-xonsh file! If you are "
384                "trying to source a file in another language, "
385                "then please use the appropriate source command. "
386                "For example, source-bash script.sh"
387            )
388        with open(fpath, "r", encoding=encoding, errors=errors) as fp:
389            src = fp.read()
390        if not src.endswith("\n"):
391            src += "\n"
392        ctx = builtins.__xonsh_ctx__
393        updates = {"__file__": fpath, "__name__": os.path.abspath(fpath)}
394        with env.swap(**make_args_env(args[i + 1 :])), swap_values(ctx, updates):
395            try:
396                builtins.execx(src, "exec", ctx, filename=fpath)
397            except Exception:
398                print_color(
399                    "{RED}You may be attempting to source non-xonsh file! "
400                    "{NO_COLOR}If you are trying to source a file in "
401                    "another language, then please use the appropriate "
402                    "source command. For example, {GREEN}source-bash "
403                    "script.sh{NO_COLOR}",
404                    file=sys.stderr,
405                )
406                raise
407
408
409def source_cmd(args, stdin=None):
410    """Simple cmd.exe-specific wrapper around source-foreign."""
411    args = list(args)
412    fpath = locate_binary(args[0])
413    args[0] = fpath if fpath else args[0]
414    if not os.path.isfile(args[0]):
415        return (None, "xonsh: error: File not found: {}\n".format(args[0]), 1)
416    prevcmd = "call "
417    prevcmd += " ".join([argvquote(arg, force=True) for arg in args])
418    prevcmd = escape_windows_cmd_string(prevcmd)
419    args.append("--prevcmd={}".format(prevcmd))
420    args.insert(0, "cmd")
421    args.append("--interactive=0")
422    args.append("--sourcer=call")
423    args.append("--envcmd=set")
424    args.append("--seterrpostcmd=if errorlevel 1 exit 1")
425    args.append("--use-tmpfile=1")
426    with builtins.__xonsh_env__.swap(PROMPT="$P$G"):
427        return source_foreign(args, stdin=stdin)
428
429
430def xexec(args, stdin=None):
431    """exec [-h|--help] command [args...]
432
433    exec (also aliased as xexec) uses the os.execvpe() function to
434    replace the xonsh process with the specified program. This provides
435    the functionality of the bash 'exec' builtin::
436
437        >>> exec bash -l -i
438        bash $
439
440    The '-h' and '--help' options print this message and exit.
441
442    Notes
443    -----
444    This command **is not** the same as the Python builtin function
445    exec(). That function is for running Python code. This command,
446    which shares the same name as the sh-lang statement, is for launching
447    a command directly in the same process. In the event of a name conflict,
448    please use the xexec command directly or dive into subprocess mode
449    explicitly with ![exec command]. For more details, please see
450    http://xon.sh/faq.html#exec.
451    """
452    if len(args) == 0:
453        return (None, "xonsh: exec: no args specified\n", 1)
454    elif args[0] == "-h" or args[0] == "--help":
455        return inspect.getdoc(xexec)
456    else:
457        denv = builtins.__xonsh_env__.detype()
458        try:
459            os.execvpe(args[0], args, denv)
460        except FileNotFoundError as e:
461            return (
462                None,
463                "xonsh: exec: file not found: {}: {}" "\n".format(e.args[1], args[0]),
464                1,
465            )
466
467
468class AWitchAWitch(argparse.Action):
469    SUPPRESS = "==SUPPRESS=="
470
471    def __init__(
472        self, option_strings, version=None, dest=SUPPRESS, default=SUPPRESS, **kwargs
473    ):
474        super().__init__(
475            option_strings=option_strings, dest=dest, default=default, nargs=0, **kwargs
476        )
477
478    def __call__(self, parser, namespace, values, option_string=None):
479        import webbrowser
480
481        webbrowser.open("https://github.com/xonsh/xonsh/commit/f49b400")
482        parser.exit()
483
484
485def xonfig(args, stdin=None):
486    """Runs the xonsh configuration utility."""
487    from xonsh.xonfig import xonfig_main  # lazy import
488
489    return xonfig_main(args)
490
491
492@unthreadable
493def trace(args, stdin=None, stdout=None, stderr=None, spec=None):
494    """Runs the xonsh tracer utility."""
495    from xonsh.tracer import tracermain  # lazy import
496
497    try:
498        return tracermain(args, stdin=stdin, stdout=stdout, stderr=stderr, spec=spec)
499    except SystemExit:
500        pass
501
502
503def showcmd(args, stdin=None):
504    """usage: showcmd [-h|--help|cmd args]
505
506    Displays the command and arguments as a list of strings that xonsh would
507    run in subprocess mode. This is useful for determining how xonsh evaluates
508    your commands and arguments prior to running these commands.
509
510    optional arguments:
511      -h, --help            show this help message and exit
512
513    example:
514      >>> showcmd echo $USER can't hear "the sea"
515      ['echo', 'I', "can't", 'hear', 'the sea']
516    """
517    if len(args) == 0 or (len(args) == 1 and args[0] in {"-h", "--help"}):
518        print(showcmd.__doc__.rstrip().replace("\n    ", "\n"))
519    else:
520        sys.displayhook(args)
521
522
523def detect_xpip_alias():
524    """
525    Determines the correct invocation to get xonsh's pip
526    """
527    if not getattr(sys, "executable", None):
528        return lambda args, stdin=None: (
529            "",
530            "Sorry, unable to run pip on your system (missing sys.executable)",
531            1,
532        )
533
534    basecmd = [sys.executable, "-m", "pip"]
535    try:
536        if ON_WINDOWS:
537            # XXX: Does windows have an installation mode that requires UAC?
538            return basecmd
539        elif not os.access(os.path.dirname(sys.executable), os.W_OK):
540            return ["sudo"] + basecmd
541        else:
542            return basecmd
543    except Exception:
544        # Something freaky happened, return something that'll probably work
545        return basecmd
546
547
548def make_default_aliases():
549    """Creates a new default aliases dictionary."""
550    default_aliases = {
551        "cd": cd,
552        "pushd": pushd,
553        "popd": popd,
554        "dirs": dirs,
555        "jobs": jobs,
556        "fg": fg,
557        "bg": bg,
558        "EOF": xonsh_exit,
559        "exit": xonsh_exit,
560        "quit": xonsh_exit,
561        "exec": xexec,
562        "xexec": xexec,
563        "source": source_alias,
564        "source-zsh": ["source-foreign", "zsh", "--sourcer=source"],
565        "source-bash": ["source-foreign", "bash", "--sourcer=source"],
566        "source-cmd": source_cmd,
567        "source-foreign": source_foreign,
568        "history": xhm.history_main,
569        "replay": replay_main,
570        "trace": trace,
571        "timeit": timeit_alias,
572        "xonfig": xonfig,
573        "scp-resume": ["rsync", "--partial", "-h", "--progress", "--rsh=ssh"],
574        "showcmd": showcmd,
575        "ipynb": ["jupyter", "notebook", "--no-browser"],
576        "which": xxw.which,
577        "xontrib": xontribs_main,
578        "completer": xca.completer_alias,
579        "xpip": detect_xpip_alias(),
580        "xonsh-reset": xonsh_reset,
581    }
582    if ON_WINDOWS:
583        # Borrow builtin commands from cmd.exe.
584        windows_cmd_aliases = {
585            "cls",
586            "copy",
587            "del",
588            "dir",
589            "echo",
590            "erase",
591            "md",
592            "mkdir",
593            "mklink",
594            "move",
595            "rd",
596            "ren",
597            "rename",
598            "rmdir",
599            "time",
600            "type",
601            "vol",
602        }
603        for alias in windows_cmd_aliases:
604            default_aliases[alias] = ["cmd", "/c", alias]
605        default_aliases["call"] = ["source-cmd"]
606        default_aliases["source-bat"] = ["source-cmd"]
607        default_aliases["clear"] = "cls"
608        if ON_ANACONDA:
609            # Add aliases specific to the Anaconda python distribution.
610            default_aliases["activate"] = ["source-cmd", "activate.bat"]
611            default_aliases["deactivate"] = ["source-cmd", "deactivate.bat"]
612        if not locate_binary("sudo"):
613            import xonsh.winutils as winutils
614
615            def sudo(args):
616                if len(args) < 1:
617                    print(
618                        "You need to provide an executable to run as " "Administrator."
619                    )
620                    return
621                cmd = args[0]
622                if locate_binary(cmd):
623                    return winutils.sudo(cmd, args[1:])
624                elif cmd.lower() in windows_cmd_aliases:
625                    args = ["/D", "/C", "CD", _get_cwd(), "&&"] + args
626                    return winutils.sudo("cmd", args)
627                else:
628                    msg = 'Cannot find the path for executable "{0}".'
629                    print(msg.format(cmd))
630
631            default_aliases["sudo"] = sudo
632    elif ON_DARWIN:
633        default_aliases["ls"] = ["ls", "-G"]
634    elif ON_FREEBSD:
635        default_aliases["grep"] = ["grep", "--color=auto"]
636        default_aliases["egrep"] = ["egrep", "--color=auto"]
637        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
638        default_aliases["ls"] = ["ls", "-G"]
639    elif ON_NETBSD:
640        default_aliases["grep"] = ["grep", "--color=auto"]
641        default_aliases["egrep"] = ["egrep", "--color=auto"]
642        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
643    else:
644        default_aliases["grep"] = ["grep", "--color=auto"]
645        default_aliases["egrep"] = ["egrep", "--color=auto"]
646        default_aliases["fgrep"] = ["fgrep", "--color=auto"]
647        default_aliases["ls"] = ["ls", "--color=auto", "-v"]
648    return default_aliases
649