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