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