1# -*- coding: utf-8 -*- 2"""Directory stack and associated utilities for the xonsh shell.""" 3import os 4import glob 5import argparse 6import builtins 7import subprocess 8 9from xonsh.lazyasd import lazyobject 10from xonsh.tools import get_sep 11from xonsh.events import events 12from xonsh.platform import ON_WINDOWS 13 14DIRSTACK = [] 15"""A list containing the currently remembered directories.""" 16_unc_tempDrives = {} 17""" drive: sharePath for temp drive letters we create for UNC mapping""" 18 19 20def _unc_check_enabled() -> bool: 21 """Check whether CMD.EXE is enforcing no-UNC-as-working-directory check. 22 23 Check can be disabled by setting {HKCU, HKLM}/SOFTWARE\Microsoft\Command Processor\DisableUNCCheck:REG_DWORD=1 24 25 Returns: 26 True if `CMD.EXE` is enforcing the check (default Windows situation) 27 False if check is explicitly disabled. 28 """ 29 if not ON_WINDOWS: 30 return 31 32 import winreg 33 34 wval = None 35 36 try: 37 key = winreg.OpenKey( 38 winreg.HKEY_CURRENT_USER, r"software\microsoft\command processor" 39 ) 40 wval, wtype = winreg.QueryValueEx(key, "DisableUNCCheck") 41 winreg.CloseKey(key) 42 except OSError as e: 43 pass 44 45 if wval is None: 46 try: 47 key2 = winreg.OpenKey( 48 winreg.HKEY_LOCAL_MACHINE, r"software\microsoft\command processor" 49 ) 50 wval, wtype = winreg.QueryValueEx(key2, "DisableUNCCheck") 51 winreg.CloseKey(key2) 52 except OSError as e: # NOQA 53 pass 54 55 return False if wval else True 56 57 58def _is_unc_path(some_path) -> bool: 59 """True if path starts with 2 backward (or forward, due to python path hacking) slashes.""" 60 return ( 61 len(some_path) > 1 62 and some_path[0] == some_path[1] 63 and some_path[0] in (os.sep, os.altsep) 64 ) 65 66 67def _unc_map_temp_drive(unc_path) -> str: 68 69 """Map a new temporary drive letter for each distinct share, 70 unless `CMD.EXE` is not insisting on non-UNC working directory. 71 72 Emulating behavior of `CMD.EXE` `pushd`, create a new mapped drive (starting from Z: towards A:, skipping existing 73 drive letters) for each new UNC path user selects. 74 75 Args: 76 unc_path: the path specified by user. Assumed to be a UNC path of form \\<server>\share... 77 78 Returns: 79 a replacement for `unc_path` to be used as the actual new working directory. 80 Note that the drive letter may be a the same as one already mapped if the server and share portion of `unc_path` 81 is the same as one still active on the stack. 82 """ 83 global _unc_tempDrives 84 assert unc_path[1] in (os.sep, os.altsep), "unc_path is UNC form of path" 85 86 if not _unc_check_enabled(): 87 return unc_path 88 else: 89 unc_share, rem_path = os.path.splitdrive(unc_path) 90 unc_share = unc_share.casefold() 91 for d in _unc_tempDrives: 92 if _unc_tempDrives[d] == unc_share: 93 return os.path.join(d, rem_path) 94 95 for dord in range(ord("z"), ord("a"), -1): 96 d = chr(dord) + ":" 97 if not os.path.isdir(d): # find unused drive letter starting from z: 98 subprocess.check_output( 99 ["NET", "USE", d, unc_share], universal_newlines=True 100 ) 101 _unc_tempDrives[d] = unc_share 102 return os.path.join(d, rem_path) 103 104 105def _unc_unmap_temp_drive(left_drive, cwd): 106 """Unmap a temporary drive letter if it is no longer needed. 107 Called after popping `DIRSTACK` and changing to new working directory, so we need stack *and* 108 new current working directory to be sure drive letter no longer needed. 109 110 Args: 111 left_drive: driveletter (and colon) of working directory we just left 112 cwd: full path of new current working directory 113""" 114 115 global _unc_tempDrives 116 117 if left_drive not in _unc_tempDrives: # if not one we've mapped, don't unmap it 118 return 119 120 for p in DIRSTACK + [cwd]: # if still in use , don't unmap it. 121 if p.casefold().startswith(left_drive): 122 return 123 124 _unc_tempDrives.pop(left_drive) 125 subprocess.check_output( 126 ["NET", "USE", left_drive, "/delete"], universal_newlines=True 127 ) 128 129 130events.doc( 131 "on_chdir", 132 """ 133on_chdir(olddir: str, newdir: str) -> None 134 135Fires when the current directory is changed for any reason. 136""", 137) 138 139 140def _get_cwd(): 141 try: 142 return os.getcwd() 143 except (OSError, FileNotFoundError): 144 return None 145 146 147def _change_working_directory(newdir, follow_symlinks=False): 148 env = builtins.__xonsh_env__ 149 old = env["PWD"] 150 new = os.path.join(old, newdir) 151 absnew = os.path.abspath(new) 152 153 if follow_symlinks: 154 absnew = os.path.realpath(absnew) 155 156 try: 157 os.chdir(absnew) 158 except (OSError, FileNotFoundError): 159 if new.endswith(get_sep()): 160 new = new[:-1] 161 if os.path.basename(new) == "..": 162 env["PWD"] = new 163 else: 164 if old is not None: 165 env["OLDPWD"] = old 166 if new is not None: 167 env["PWD"] = absnew 168 169 # Fire event if the path actually changed 170 if old != env["PWD"]: 171 events.on_chdir.fire(olddir=old, newdir=env["PWD"]) 172 173 174def _try_cdpath(apath): 175 # NOTE: this CDPATH implementation differs from the bash one. 176 # In bash if a CDPATH is set, an unqualified local folder 177 # is considered after all CDPATHs, example: 178 # CDPATH=$HOME/src (with src/xonsh/ inside) 179 # $ cd xonsh -> src/xonsh (with xonsh/xonsh) 180 # a second $ cd xonsh has no effects, to move in the nested xonsh 181 # in bash a full $ cd ./xonsh is needed. 182 # In xonsh a relative folder is always preferred. 183 env = builtins.__xonsh_env__ 184 cdpaths = env.get("CDPATH") 185 for cdp in cdpaths: 186 globber = builtins.__xonsh_expand_path__(os.path.join(cdp, apath)) 187 for cdpath_prefixed_path in glob.iglob(globber): 188 return cdpath_prefixed_path 189 return apath 190 191 192def cd(args, stdin=None): 193 """Changes the directory. 194 195 If no directory is specified (i.e. if `args` is None) then this 196 changes to the current user's home directory. 197 """ 198 env = builtins.__xonsh_env__ 199 oldpwd = env.get("OLDPWD", None) 200 cwd = env["PWD"] 201 202 follow_symlinks = False 203 if len(args) > 0 and args[0] == "-P": 204 follow_symlinks = True 205 del args[0] 206 207 if len(args) == 0: 208 d = os.path.expanduser("~") 209 elif len(args) == 1: 210 d = os.path.expanduser(args[0]) 211 if not os.path.isdir(d): 212 if d == "-": 213 if oldpwd is not None: 214 d = oldpwd 215 else: 216 return "", "cd: no previous directory stored\n", 1 217 elif d.startswith("-"): 218 try: 219 num = int(d[1:]) 220 except ValueError: 221 return "", "cd: Invalid destination: {0}\n".format(d), 1 222 if num == 0: 223 return None, None, 0 224 elif num < 0: 225 return "", "cd: Invalid destination: {0}\n".format(d), 1 226 elif num > len(DIRSTACK): 227 e = "cd: Too few elements in dirstack ({0} elements)\n" 228 return "", e.format(len(DIRSTACK)), 1 229 else: 230 d = DIRSTACK[num - 1] 231 else: 232 d = _try_cdpath(d) 233 else: 234 return ( 235 "", 236 ( 237 "cd takes 0 or 1 arguments, not {0}. An additional `-P` " 238 "flag can be passed in first position to follow symlinks." 239 "\n".format(len(args)) 240 ), 241 1, 242 ) 243 if not os.path.exists(d): 244 return "", "cd: no such file or directory: {0}\n".format(d), 1 245 if not os.path.isdir(d): 246 return "", "cd: {0} is not a directory\n".format(d), 1 247 if not os.access(d, os.X_OK): 248 return "", "cd: permission denied: {0}\n".format(d), 1 249 if ( 250 ON_WINDOWS 251 and _is_unc_path(d) 252 and _unc_check_enabled() 253 and (not env.get("AUTO_PUSHD")) 254 ): 255 return ( 256 "", 257 "cd: can't cd to UNC path on Windows, unless $AUTO_PUSHD set or reg entry " 258 + r"HKCU\SOFTWARE\MICROSOFT\Command Processor\DisableUNCCheck:DWORD = 1" 259 + "\n", 260 1, 261 ) 262 263 # now, push the directory onto the dirstack if AUTO_PUSHD is set 264 if cwd is not None and env.get("AUTO_PUSHD"): 265 pushd(["-n", "-q", cwd]) 266 if ON_WINDOWS and _is_unc_path(d): 267 d = _unc_map_temp_drive(d) 268 _change_working_directory(d, follow_symlinks) 269 return None, None, 0 270 271 272@lazyobject 273def pushd_parser(): 274 parser = argparse.ArgumentParser(prog="pushd") 275 parser.add_argument("dir", nargs="?") 276 parser.add_argument( 277 "-n", 278 dest="cd", 279 help="Suppresses the normal change of directory when" 280 " adding directories to the stack, so that only the" 281 " stack is manipulated.", 282 action="store_false", 283 ) 284 parser.add_argument( 285 "-q", 286 dest="quiet", 287 help="Do not call dirs, regardless of $PUSHD_SILENT", 288 action="store_true", 289 ) 290 return parser 291 292 293def pushd(args, stdin=None): 294 """xonsh command: pushd 295 296 Adds a directory to the top of the directory stack, or rotates the stack, 297 making the new top of the stack the current working directory. 298 299 On Windows, if the path is a UNC path (begins with `\\<server>\<share>`) and if the `DisableUNCCheck` registry 300 value is not enabled, creates a temporary mapped drive letter and sets the working directory there, emulating 301 behavior of `PUSHD` in `CMD.EXE` 302 """ 303 global DIRSTACK 304 305 try: 306 args = pushd_parser.parse_args(args) 307 except SystemExit: 308 return None, None, 1 309 310 env = builtins.__xonsh_env__ 311 312 pwd = env["PWD"] 313 314 if env.get("PUSHD_MINUS", False): 315 BACKWARD = "-" 316 FORWARD = "+" 317 else: 318 BACKWARD = "+" 319 FORWARD = "-" 320 321 if args.dir is None: 322 try: 323 new_pwd = DIRSTACK.pop(0) 324 except IndexError: 325 e = "pushd: Directory stack is empty\n" 326 return None, e, 1 327 elif os.path.isdir(args.dir): 328 new_pwd = args.dir 329 else: 330 try: 331 num = int(args.dir[1:]) 332 except ValueError: 333 e = "Invalid argument to pushd: {0}\n" 334 return None, e.format(args.dir), 1 335 336 if num < 0: 337 e = "Invalid argument to pushd: {0}\n" 338 return None, e.format(args.dir), 1 339 340 if num > len(DIRSTACK): 341 e = "Too few elements in dirstack ({0} elements)\n" 342 return None, e.format(len(DIRSTACK)), 1 343 elif args.dir.startswith(FORWARD): 344 if num == len(DIRSTACK): 345 new_pwd = None 346 else: 347 new_pwd = DIRSTACK.pop(len(DIRSTACK) - 1 - num) 348 elif args.dir.startswith(BACKWARD): 349 if num == 0: 350 new_pwd = None 351 else: 352 new_pwd = DIRSTACK.pop(num - 1) 353 else: 354 e = "Invalid argument to pushd: {0}\n" 355 return None, e.format(args.dir), 1 356 if new_pwd is not None: 357 if ON_WINDOWS and _is_unc_path(new_pwd): 358 new_pwd = _unc_map_temp_drive(new_pwd) 359 if args.cd: 360 DIRSTACK.insert(0, os.path.expanduser(pwd)) 361 _change_working_directory(new_pwd) 362 else: 363 DIRSTACK.insert(0, os.path.expanduser(new_pwd)) 364 365 maxsize = env.get("DIRSTACK_SIZE") 366 if len(DIRSTACK) > maxsize: 367 DIRSTACK = DIRSTACK[:maxsize] 368 369 if not args.quiet and not env.get("PUSHD_SILENT"): 370 return dirs([], None) 371 372 return None, None, 0 373 374 375@lazyobject 376def popd_parser(): 377 parser = argparse.ArgumentParser(prog="popd") 378 parser.add_argument("dir", nargs="?") 379 parser.add_argument( 380 "-n", 381 dest="cd", 382 help="Suppresses the normal change of directory when" 383 " adding directories to the stack, so that only the" 384 " stack is manipulated.", 385 action="store_false", 386 ) 387 parser.add_argument( 388 "-q", 389 dest="quiet", 390 help="Do not call dirs, regardless of $PUSHD_SILENT", 391 action="store_true", 392 ) 393 return parser 394 395 396def popd(args, stdin=None): 397 """ 398 xonsh command: popd 399 400 Removes entries from the directory stack. 401 """ 402 global DIRSTACK 403 404 try: 405 args = pushd_parser.parse_args(args) 406 except SystemExit: 407 return None, None, 1 408 409 env = builtins.__xonsh_env__ 410 411 if env.get("PUSHD_MINUS"): 412 BACKWARD = "-" 413 FORWARD = "+" 414 else: 415 BACKWARD = "-" 416 FORWARD = "+" 417 418 if args.dir is None: 419 try: 420 new_pwd = DIRSTACK.pop(0) 421 except IndexError: 422 e = "popd: Directory stack is empty\n" 423 return None, e, 1 424 else: 425 try: 426 num = int(args.dir[1:]) 427 except ValueError: 428 e = "Invalid argument to popd: {0}\n" 429 return None, e.format(args.dir), 1 430 431 if num < 0: 432 e = "Invalid argument to popd: {0}\n" 433 return None, e.format(args.dir), 1 434 435 if num > len(DIRSTACK): 436 e = "Too few elements in dirstack ({0} elements)\n" 437 return None, e.format(len(DIRSTACK)), 1 438 elif args.dir.startswith(FORWARD): 439 if num == len(DIRSTACK): 440 new_pwd = DIRSTACK.pop(0) 441 else: 442 new_pwd = None 443 DIRSTACK.pop(len(DIRSTACK) - 1 - num) 444 elif args.dir.startswith(BACKWARD): 445 if num == 0: 446 new_pwd = DIRSTACK.pop(0) 447 else: 448 new_pwd = None 449 DIRSTACK.pop(num - 1) 450 else: 451 e = "Invalid argument to popd: {0}\n" 452 return None, e.format(args.dir), 1 453 454 if new_pwd is not None: 455 e = None 456 if args.cd: 457 env = builtins.__xonsh_env__ 458 pwd = env["PWD"] 459 460 _change_working_directory(new_pwd) 461 462 if ON_WINDOWS: 463 drive, rem_path = os.path.splitdrive(pwd) 464 _unc_unmap_temp_drive(drive.casefold(), new_pwd) 465 466 if not args.quiet and not env.get("PUSHD_SILENT"): 467 return dirs([], None) 468 469 return None, None, 0 470 471 472@lazyobject 473def dirs_parser(): 474 parser = argparse.ArgumentParser(prog="dirs") 475 parser.add_argument("N", nargs="?") 476 parser.add_argument( 477 "-c", 478 dest="clear", 479 help="Clears the directory stack by deleting all of" " the entries.", 480 action="store_true", 481 ) 482 parser.add_argument( 483 "-p", 484 dest="print_long", 485 help="Print the directory stack with one entry per" " line.", 486 action="store_true", 487 ) 488 parser.add_argument( 489 "-v", 490 dest="verbose", 491 help="Print the directory stack with one entry per" 492 " line, prefixing each entry with its index in the" 493 " stack.", 494 action="store_true", 495 ) 496 parser.add_argument( 497 "-l", 498 dest="long", 499 help="Produces a longer listing; the default listing" 500 " format uses a tilde to denote the home directory.", 501 action="store_true", 502 ) 503 return parser 504 505 506def dirs(args, stdin=None): 507 """xonsh command: dirs 508 509 Displays the list of currently remembered directories. Can also be used 510 to clear the directory stack. 511 """ 512 global DIRSTACK 513 try: 514 args = dirs_parser.parse_args(args) 515 except SystemExit: 516 return None, None 517 518 env = builtins.__xonsh_env__ 519 dirstack = [os.path.expanduser(env["PWD"])] + DIRSTACK 520 521 if env.get("PUSHD_MINUS"): 522 BACKWARD = "-" 523 FORWARD = "+" 524 else: 525 BACKWARD = "-" 526 FORWARD = "+" 527 528 if args.clear: 529 DIRSTACK = [] 530 return None, None, 0 531 532 if args.long: 533 o = dirstack 534 else: 535 d = os.path.expanduser("~") 536 o = [i.replace(d, "~") for i in dirstack] 537 538 if args.verbose: 539 out = "" 540 pad = len(str(len(o) - 1)) 541 for (ix, e) in enumerate(o): 542 blanks = " " * (pad - len(str(ix))) 543 out += "\n{0}{1} {2}".format(blanks, ix, e) 544 out = out[1:] 545 elif args.print_long: 546 out = "\n".join(o) 547 else: 548 out = " ".join(o) 549 550 N = args.N 551 if N is not None: 552 try: 553 num = int(N[1:]) 554 except ValueError: 555 e = "Invalid argument to dirs: {0}\n" 556 return None, e.format(N), 1 557 558 if num < 0: 559 e = "Invalid argument to dirs: {0}\n" 560 return None, e.format(len(o)), 1 561 562 if num >= len(o): 563 e = "Too few elements in dirstack ({0} elements)\n" 564 return None, e.format(len(o)), 1 565 566 if N.startswith(BACKWARD): 567 idx = num 568 elif N.startswith(FORWARD): 569 idx = len(o) - 1 - num 570 else: 571 e = "Invalid argument to dirs: {0}\n" 572 return None, e.format(N), 1 573 574 out = o[idx] 575 576 return out + "\n", None, 0 577