1# -*- coding: utf-8 -*-
2"""The main xonsh script."""
3import os
4import sys
5import enum
6import argparse
7import builtins
8import contextlib
9import signal
10import traceback
11
12from xonsh import __version__
13from xonsh.timings import setup_timings
14from xonsh.lazyasd import lazyobject
15from xonsh.shell import Shell
16from xonsh.pretty import pretty
17from xonsh.execer import Execer
18from xonsh.proc import HiddenCommandPipeline
19from xonsh.jobs import ignore_sigtstp
20from xonsh.tools import setup_win_unicode_console, print_color, to_bool_or_int
21from xonsh.platform import HAS_PYGMENTS, ON_WINDOWS
22from xonsh.codecache import run_script_with_cache, run_code_with_cache
23from xonsh.xonfig import print_welcome_screen
24from xonsh.lazyimps import pygments, pyghooks
25from xonsh.imphooks import install_import_hooks
26from xonsh.events import events
27from xonsh.environ import xonshrc_context, make_args_env
28from xonsh.xontribs import xontribs_load
29
30
31events.transmogrify("on_post_init", "LoadEvent")
32events.doc(
33    "on_post_init",
34    """
35on_post_init() -> None
36
37Fired after all initialization is finished and we're ready to do work.
38
39NOTE: This is fired before the wizard is automatically started.
40""",
41)
42
43events.transmogrify("on_exit", "LoadEvent")
44events.doc(
45    "on_exit",
46    """
47on_exit() -> None
48
49Fired after all commands have been executed, before tear-down occurs.
50
51NOTE: All the caveats of the ``atexit`` module also apply to this event.
52""",
53)
54
55
56events.transmogrify("on_pre_cmdloop", "LoadEvent")
57events.doc(
58    "on_pre_cmdloop",
59    """
60on_pre_cmdloop() -> None
61
62Fired just before the command loop is started, if it is.
63""",
64)
65
66events.transmogrify("on_post_cmdloop", "LoadEvent")
67events.doc(
68    "on_post_cmdloop",
69    """
70on_post_cmdloop() -> None
71
72Fired just after the command loop finishes, if it is.
73
74NOTE: All the caveats of the ``atexit`` module also apply to this event.
75""",
76)
77
78events.transmogrify("on_pre_rc", "LoadEvent")
79events.doc(
80    "on_pre_rc",
81    """
82on_pre_rc() -> None
83
84Fired just before rc files are loaded, if they are.
85""",
86)
87
88events.transmogrify("on_post_rc", "LoadEvent")
89events.doc(
90    "on_post_rc",
91    """
92on_post_rc() -> None
93
94Fired just after rc files are loaded, if they are.
95""",
96)
97
98
99def get_setproctitle():
100    """Proxy function for loading process title"""
101    try:
102        from setproctitle import setproctitle as spt
103    except ImportError:
104        return
105    return spt
106
107
108def path_argument(s):
109    """Return a path only if the path is actually legal
110
111    This is very similar to argparse.FileType, except that it doesn't return
112    an open file handle, but rather simply validates the path."""
113
114    s = os.path.abspath(os.path.expanduser(s))
115    if not os.path.isfile(s):
116        msg = "{0!r} must be a valid path to a file".format(s)
117        raise argparse.ArgumentTypeError(msg)
118    return s
119
120
121@lazyobject
122def parser():
123    p = argparse.ArgumentParser(description="xonsh", add_help=False)
124    p.add_argument(
125        "-h",
126        "--help",
127        dest="help",
128        action="store_true",
129        default=False,
130        help="show help and exit",
131    )
132    p.add_argument(
133        "-V",
134        "--version",
135        dest="version",
136        action="store_true",
137        default=False,
138        help="show version information and exit",
139    )
140    p.add_argument(
141        "-c",
142        help="Run a single command and exit",
143        dest="command",
144        required=False,
145        default=None,
146    )
147    p.add_argument(
148        "-i",
149        "--interactive",
150        help="force running in interactive mode",
151        dest="force_interactive",
152        action="store_true",
153        default=False,
154    )
155    p.add_argument(
156        "-l",
157        "--login",
158        help="run as a login shell",
159        dest="login",
160        action="store_true",
161        default=False,
162    )
163    p.add_argument(
164        "--config-path",
165        help="DEPRECATED: static configuration files may now be used "
166        "in the XONSHRC file list, see the --rc option.",
167        dest="config_path",
168        default=None,
169        type=path_argument,
170    )
171    p.add_argument(
172        "--rc",
173        help="The xonshrc files to load, these may be either xonsh "
174        "files or JSON-based static configuration files.",
175        dest="rc",
176        nargs="+",
177        type=path_argument,
178        default=None,
179    )
180    p.add_argument(
181        "--no-rc",
182        help="Do not load the .xonshrc files",
183        dest="norc",
184        action="store_true",
185        default=False,
186    )
187    p.add_argument(
188        "--no-script-cache",
189        help="Do not cache scripts as they are run",
190        dest="scriptcache",
191        action="store_false",
192        default=True,
193    )
194    p.add_argument(
195        "--cache-everything",
196        help="Use a cache, even for interactive commands",
197        dest="cacheall",
198        action="store_true",
199        default=False,
200    )
201    p.add_argument(
202        "-D",
203        dest="defines",
204        help="define an environment variable, in the form of "
205        "-DNAME=VAL. May be used many times.",
206        metavar="ITEM",
207        action="append",
208        default=None,
209    )
210    p.add_argument(
211        "--shell-type",
212        help="What kind of shell should be used. "
213        "Possible options: readline, prompt_toolkit, random. "
214        "Warning! If set this overrides $SHELL_TYPE variable.",
215        dest="shell_type",
216        choices=tuple(Shell.shell_type_aliases.keys()),
217        default=None,
218    )
219    p.add_argument(
220        "--timings",
221        help="Prints timing information before the prompt is shown. "
222        "This is useful while tracking down performance issues "
223        "and investigating startup times.",
224        dest="timings",
225        action="store_true",
226        default=None,
227    )
228    p.add_argument(
229        "file",
230        metavar="script-file",
231        help="If present, execute the script in script-file" " and exit",
232        nargs="?",
233        default=None,
234    )
235    p.add_argument(
236        "args",
237        metavar="args",
238        help="Additional arguments to the script specified " "by script-file",
239        nargs=argparse.REMAINDER,
240        default=[],
241    )
242    return p
243
244
245def _pprint_displayhook(value):
246    if value is None:
247        return
248    builtins._ = None  # Set '_' to None to avoid recursion
249    if isinstance(value, HiddenCommandPipeline):
250        builtins._ = value
251        return
252    env = builtins.__xonsh_env__
253    if env.get("PRETTY_PRINT_RESULTS"):
254        printed_val = pretty(value)
255    else:
256        printed_val = repr(value)
257    if HAS_PYGMENTS and env.get("COLOR_RESULTS"):
258        tokens = list(pygments.lex(printed_val, lexer=pyghooks.XonshLexer()))
259        print_color(tokens)
260    else:
261        print(printed_val)  # black & white case
262    builtins._ = value
263
264
265class XonshMode(enum.Enum):
266    single_command = 0
267    script_from_file = 1
268    script_from_stdin = 2
269    interactive = 3
270
271
272def start_services(shell_kwargs, args):
273    """Starts up the essential services in the proper order.
274    This returns the environment instance as a convenience.
275    """
276    install_import_hooks()
277    # create execer, which loads builtins
278    ctx = shell_kwargs.get("ctx", {})
279    debug = to_bool_or_int(os.getenv("XONSH_DEBUG", "0"))
280    events.on_timingprobe.fire(name="pre_execer_init")
281    execer = Execer(
282        xonsh_ctx=ctx,
283        debug_level=debug,
284        scriptcache=shell_kwargs.get("scriptcache", True),
285        cacheall=shell_kwargs.get("cacheall", False),
286    )
287    events.on_timingprobe.fire(name="post_execer_init")
288    # load rc files
289    login = shell_kwargs.get("login", True)
290    env = builtins.__xonsh_env__
291    rc = shell_kwargs.get("rc", None)
292    rc = env.get("XONSHRC") if rc is None else rc
293    if args.mode != XonshMode.interactive and not args.force_interactive:
294        #  Don't load xonshrc if not interactive shell
295        rc = None
296    events.on_pre_rc.fire()
297    xonshrc_context(rcfiles=rc, execer=execer, ctx=ctx, env=env, login=login)
298    events.on_post_rc.fire()
299    # create shell
300    builtins.__xonsh_shell__ = Shell(execer=execer, **shell_kwargs)
301    ctx["__name__"] = "__main__"
302    return env
303
304
305def premain(argv=None):
306    """Setup for main xonsh entry point. Returns parsed arguments."""
307    if argv is None:
308        argv = sys.argv[1:]
309    setup_timings()
310    setproctitle = get_setproctitle()
311    if setproctitle is not None:
312        setproctitle(" ".join(["xonsh"] + argv))
313    builtins.__xonsh_ctx__ = {}
314    args = parser.parse_args(argv)
315    if args.help:
316        parser.print_help()
317        parser.exit()
318    if args.version:
319        version = "/".join(("xonsh", __version__))
320        print(version)
321        parser.exit()
322    shell_kwargs = {
323        "shell_type": args.shell_type,
324        "completer": False,
325        "login": False,
326        "scriptcache": args.scriptcache,
327        "cacheall": args.cacheall,
328        "ctx": builtins.__xonsh_ctx__,
329    }
330    if args.login:
331        shell_kwargs["login"] = True
332    if args.norc:
333        shell_kwargs["rc"] = ()
334    elif args.rc:
335        shell_kwargs["rc"] = args.rc
336    setattr(sys, "displayhook", _pprint_displayhook)
337    if args.command is not None:
338        args.mode = XonshMode.single_command
339        shell_kwargs["shell_type"] = "none"
340    elif args.file is not None:
341        args.mode = XonshMode.script_from_file
342        shell_kwargs["shell_type"] = "none"
343    elif not sys.stdin.isatty() and not args.force_interactive:
344        args.mode = XonshMode.script_from_stdin
345        shell_kwargs["shell_type"] = "none"
346    else:
347        args.mode = XonshMode.interactive
348        shell_kwargs["completer"] = True
349        shell_kwargs["login"] = True
350    env = start_services(shell_kwargs, args)
351    env["XONSH_LOGIN"] = shell_kwargs["login"]
352    if args.defines is not None:
353        env.update([x.split("=", 1) for x in args.defines])
354    env["XONSH_INTERACTIVE"] = args.force_interactive or (
355        args.mode == XonshMode.interactive
356    )
357    if ON_WINDOWS:
358        setup_win_unicode_console(env.get("WIN_UNICODE_CONSOLE", True))
359    return args
360
361
362def _failback_to_other_shells(args, err):
363    # only failback for interactive shell; if we cannot tell, treat it
364    # as an interactive one for safe.
365    if hasattr(args, "mode") and args.mode != XonshMode.interactive:
366        raise err
367    foreign_shell = None
368    shells_file = "/etc/shells"
369    if not os.path.exists(shells_file):
370        # right now, it will always break here on Windows
371        raise err
372    excluded_list = ["xonsh", "screen"]
373    with open(shells_file) as f:
374        for line in f:
375            line = line.strip()
376            if not line or line.startswith("#"):
377                continue
378            if "/" not in line:
379                continue
380            _, shell = line.rsplit("/", 1)
381            if shell in excluded_list:
382                continue
383            if not os.path.exists(line):
384                continue
385            foreign_shell = line
386            break
387    if foreign_shell:
388        traceback.print_exc()
389        print("Xonsh encountered an issue during launch", file=sys.stderr)
390        print("Failback to {}".format(foreign_shell), file=sys.stderr)
391        os.execlp(foreign_shell, foreign_shell)
392    else:
393        raise err
394
395
396def main(argv=None):
397    args = None
398    try:
399        args = premain(argv)
400        return main_xonsh(args)
401    except Exception as err:
402        _failback_to_other_shells(args, err)
403
404
405def main_xonsh(args):
406    """Main entry point for xonsh cli."""
407    if not ON_WINDOWS:
408
409        def func_sig_ttin_ttou(n, f):
410            pass
411
412        signal.signal(signal.SIGTTIN, func_sig_ttin_ttou)
413        signal.signal(signal.SIGTTOU, func_sig_ttin_ttou)
414
415    events.on_post_init.fire()
416    env = builtins.__xonsh_env__
417    shell = builtins.__xonsh_shell__
418    try:
419        if args.mode == XonshMode.interactive:
420            # enter the shell
421            env["XONSH_INTERACTIVE"] = True
422            ignore_sigtstp()
423            if env["XONSH_INTERACTIVE"] and not any(
424                os.path.isfile(i) for i in env["XONSHRC"]
425            ):
426                print_welcome_screen()
427            events.on_pre_cmdloop.fire()
428            try:
429                shell.shell.cmdloop()
430            finally:
431                events.on_post_cmdloop.fire()
432        elif args.mode == XonshMode.single_command:
433            # run a single command and exit
434            run_code_with_cache(args.command.lstrip(), shell.execer, mode="single")
435        elif args.mode == XonshMode.script_from_file:
436            # run a script contained in a file
437            path = os.path.abspath(os.path.expanduser(args.file))
438            if os.path.isfile(path):
439                sys.argv = [args.file] + args.args
440                env.update(make_args_env())  # $ARGS is not sys.argv
441                env["XONSH_SOURCE"] = path
442                shell.ctx.update({"__file__": args.file, "__name__": "__main__"})
443                run_script_with_cache(
444                    args.file, shell.execer, glb=shell.ctx, loc=None, mode="exec"
445                )
446            else:
447                print("xonsh: {0}: No such file or directory.".format(args.file))
448        elif args.mode == XonshMode.script_from_stdin:
449            # run a script given on stdin
450            code = sys.stdin.read()
451            run_code_with_cache(
452                code, shell.execer, glb=shell.ctx, loc=None, mode="exec"
453            )
454    finally:
455        events.on_exit.fire()
456    postmain(args)
457
458
459def postmain(args=None):
460    """Teardown for main xonsh entry point, accepts parsed arguments."""
461    if ON_WINDOWS:
462        setup_win_unicode_console(enable=False)
463    if hasattr(builtins, "__xonsh_shell__"):
464        del builtins.__xonsh_shell__
465
466
467@contextlib.contextmanager
468def main_context(argv=None):
469    """Generator that runs pre- and post-main() functions. This has two iterations.
470    The first yields the shell. The second returns None but cleans
471    up the shell.
472    """
473    args = premain(argv)
474    yield builtins.__xonsh_shell__
475    postmain(args)
476
477
478def setup(
479    ctx=None,
480    shell_type="none",
481    env=(("RAISE_SUBPROC_ERROR", True),),
482    aliases=(),
483    xontribs=(),
484    threadable_predictors=(),
485):
486    """Starts up a new xonsh shell. Calling this in function in another
487    packages __init__.py will allow xonsh to be fully used in the
488    package in headless or headed mode. This function is primarily indended to
489    make starting up xonsh for 3rd party packages easier.
490
491    Parameters
492    ----------
493    ctx : dict-like or None, optional
494        The xonsh context to start with. If None, an empty dictionary
495        is provided.
496    shell_type : str, optional
497        The type of shell to start. By default this is 'none', indicating
498        we should start in headless mode.
499    env : dict-like, optional
500        Environment to update the current environment with after the shell
501        has been initialized.
502    aliases : dict-like, optional
503        Aliases to add after the shell has been initialized.
504    xontribs : iterable of str, optional
505        Xontrib names to load.
506    threadable_predictors : dict-like, optional
507        Threadable predictors to start up with. These overide the defaults.
508    """
509    ctx = {} if ctx is None else ctx
510    # setup xonsh ctx and execer
511    builtins.__xonsh_ctx__ = ctx
512    builtins.__xonsh_execer__ = Execer(xonsh_ctx=ctx)
513    builtins.__xonsh_shell__ = Shell(
514        builtins.__xonsh_execer__, ctx=ctx, shell_type=shell_type
515    )
516    builtins.__xonsh_env__.update(env)
517    install_import_hooks()
518    builtins.aliases.update(aliases)
519    if xontribs:
520        xontribs_load(xontribs)
521    tp = builtins.__xonsh_commands_cache__.threadable_predictors
522    tp.update(threadable_predictors)
523