diff --git a/docs/api/dirstack.rst b/docs/api/dirstack.rst new file mode 100644 index 000000000..881bc908d --- /dev/null +++ b/docs/api/dirstack.rst @@ -0,0 +1,10 @@ +.. _xonsh_dirstack: + +****************************************************** +Directory Stack (``xonsh.dirstack``) +****************************************************** + +.. automodule:: xonsh.dirstack + :members: + :undoc-members: + :inherited-members: diff --git a/docs/api/index.rst b/docs/api/index.rst index 407ad45ba..f0f3f9b14 100644 --- a/docs/api/index.rst +++ b/docs/api/index.rst @@ -24,6 +24,7 @@ For those of you who want the gritty details. built_ins environ aliases + dirstack inspectors completer shell diff --git a/xonsh/aliases.py b/xonsh/aliases.py index 92649f7fb..2b99f452e 100644 --- a/xonsh/aliases.py +++ b/xonsh/aliases.py @@ -7,6 +7,8 @@ import subprocess import shlex from warnings import warn +from xonsh.dirstack import dirs, pushd, popd + def cd(args, stdin=None): """Changes the directory. @@ -14,7 +16,7 @@ def cd(args, stdin=None): changes to the current user's home directory. """ env = builtins.__xonsh_env__ - cur_oldpwd = env.get('OLDPWD') or os.getcwd() + cur_oldpwd = env.get('OLDPWD', os.getcwd()) if len(args) == 0: d = os.path.expanduser('~') elif len(args) == 1: @@ -27,6 +29,7 @@ def cd(args, stdin=None): return '', 'cd: no such file or directory: {0}\n'.format(d) if not os.path.isdir(d): return '', 'cd: {0} is not a directory\n'.format(d) + env['OLDPWD'] = os.getcwd() os.chdir(d) env['PWD'] = os.getcwd() @@ -87,6 +90,9 @@ def bash_aliases(): DEFAULT_ALIASES = { 'cd': cd, + 'pushd':pushd, + 'popd':popd, + 'dirs':dirs, 'EOF': exit, 'exit': exit, 'quit': exit, diff --git a/xonsh/built_ins.py b/xonsh/built_ins.py index e0602a316..99d1d2175 100644 --- a/xonsh/built_ins.py +++ b/xonsh/built_ins.py @@ -414,7 +414,7 @@ def load_builtins(execer=None): builtins.evalx = None if execer is None else execer.eval builtins.execx = None if execer is None else execer.exec builtins.compilex = None if execer is None else execer.compile - builtins.aliases = Aliases(DEFAULT_ALIASES) + builtins.default_aliases = builtins.aliases = Aliases(DEFAULT_ALIASES) builtins.aliases.update(bash_aliases()) BUILTINS_LOADED = True @@ -437,6 +437,7 @@ def unload_builtins(): '__xonsh_pyexit__', '__xonsh_pyquit__', '__xonsh_subproc_captured__', '__xonsh_subproc_uncaptured__', '__xonsh_execer__', 'evalx', 'execx', 'compilex', + 'default_aliases' ] for name in names: if hasattr(builtins, name): diff --git a/xonsh/dirstack.py b/xonsh/dirstack.py new file mode 100644 index 000000000..dc86622ff --- /dev/null +++ b/xonsh/dirstack.py @@ -0,0 +1,291 @@ +"""Directory stack and associated utilities for the xonsh shell. +""" +import os +import builtins +from argparse import ArgumentParser + +DIRSTACK = [] +""" +A list containing the currently remembered directories. +""" + + +def pushd(args, stdin=None): + """ + xonsh command: pushd + + Adds a directory to the top of the directory stack, or rotates the stack, + making the new top of the stack the current working directory. + """ + global DIRSTACK + + try: + args = pushd_parser.parse_args(args) + except SystemExit: + return None, None + + env = builtins.__xonsh_env__ + + pwd = env['PWD'] + + if env.get('PUSHD_MINUS', False): + BACKWARD = '-' + FORWARD = '+' + else: + BACKWARD = '+' + FORWARD = '-' + + if args.dir is None: + try: + new_pwd = DIRSTACK.pop(0) + except IndexError: + e = 'pushd: Directory stack is empty\n' + return None, e + elif os.path.isdir(args.dir): + new_pwd = args.dir + else: + try: + num = int(args.dir[1:]) + except ValueError: + e = 'Invalid argument to pushd: {0}\n' + return None, e.format(args.dir) + + if num < 0: + e = 'Invalid argument to pushd: {0}\n' + return None, e.format(args.dir) + + if num > len(DIRSTACK): + e = 'Too few elements in dirstack ({0} elements)\n' + return None, e.format(len(DIRSTACK)) + elif args.dir.startswith(FORWARD): + if num == len(DIRSTACK): + new_pwd = None + else: + new_pwd = DIRSTACK.pop(len(DIRSTACK)-1-num) + elif args.dir.startswith(BACKWARD): + if num == 0: + new_pwd = None + else: + new_pwd = DIRSTACK.pop(num-1) + else: + e = 'Invalid argument to pushd: {0}\n' + return None, e.format(args.dir) + if new_pwd is not None: + o = None + e = None + if args.cd: + DIRSTACK.insert(0, os.path.expanduser(pwd)) + o, e = builtins.default_aliases['cd']([new_pwd], None) + else: + DIRSTACK.insert(0, os.path.expanduser(os.path.abspath(new_pwd))) + + if e is not None: + return None, e + + maxsize = env.get('DIRSTACK_SIZE', 20) + if len(DIRSTACK) > maxsize: + DIRSTACK = DIRSTACK[:maxsize] + + if not args.quiet and not env.get('PUSHD_SILENT', False): + return dirs([], None) + + return None, None + + +def popd(args, stdin=None): + """ + xonsh command: popd + + Removes entries from the directory stack. + """ + global DIRSTACK + + try: + args = pushd_parser.parse_args(args) + except SystemExit: + return None, None + + env = builtins.__xonsh_env__ + + if env.get('PUSHD_MINUS', False): + BACKWARD = '-' + FORWARD = '+' + else: + BACKWARD = '-' + FORWARD = '+' + + if args.dir is None: + try: + new_pwd = DIRSTACK.pop(0) + except IndexError: + e = 'popd: Directory stack is empty\n' + return None, e + else: + try: + num = int(args.dir[1:]) + except ValueError: + e = 'Invalid argument to popd: {0}\n' + return None, e.format(args.dir) + + if num < 0: + e = 'Invalid argument to popd: {0}\n' + return None, e.format(args.dir) + + if num > len(DIRSTACK): + e = 'Too few elements in dirstack ({0} elements)\n' + return None, e.format(len(DIRSTACK)) + elif args.dir.startswith(FORWARD): + if num == len(DIRSTACK): + new_pwd = DIRSTACK.pop(0) + else: + new_pwd = None + DIRSTACK.pop(len(DIRSTACK)-1-num) + elif args.dir.startswith(BACKWARD): + if num == 0: + new_pwd = DIRSTACK.pop(0) + else: + new_pwd = None + DIRSTACK.pop(num-1) + else: + e = 'Invalid argument to popd: {0}\n' + return None, e.format(args.dir) + + if new_pwd is not None: + o = None + e = None + if args.cd: + o, e = builtins.default_aliases['cd']([new_pwd], None) + + if e is not None: + return None, e + + if not args.quiet and not env.get('PUSHD_SILENT', False): + return dirs([], None) + + return None, None + + +def dirs(args, stdin=None): + """ + xonsh command: dirs + + Displays the list of currently remembered directories. Can also be used + to clear the directory stack. + """ + global DIRSTACK + dirstack = [os.path.expanduser(builtins.__xonsh_env__['PWD'])] + DIRSTACK + + try: + args = dirs_parser.parse_args(args) + except SystemExit: + return None, None + + env = builtins.__xonsh_env__ + + if env.get('PUSHD_MINUS', False): + BACKWARD = '-' + FORWARD = '+' + else: + BACKWARD = '-' + FORWARD = '+' + + if args.clear: + dirstack = [] + return None, None + + if args.long: + o = dirstack + else: + d = os.path.expanduser('~') + o = [i.replace(d, '~') for i in dirstack] + + if args.verbose: + out = '' + pad = len(str(len(o)-1)) + for (ix, e) in enumerate(o): + blanks = ' ' * (pad - len(str(ix))) + out += '\n{0}{1} {2}'.format(blanks, ix, e) + out = out[1:] + elif args.print_long: + out = '\n'.join(o) + else: + out = ' '.join(o) + + N = args.N + if N is not None: + try: + num = int(N[1:]) + except ValueError: + e = 'Invalid argument to dirs: {0}\n' + return None, e.format(N) + + if num < 0: + e = 'Invalid argument to dirs: {0}\n' + return None, e.format(len(o)) + + if num >= len(o): + e = 'Too few elements in dirstack ({0} elements)\n' + return None, e.format(len(o)) + + if N.startswith(BACKWARD): + idx = num + elif N.startswith(FORWARD): + idx = len(o)-1-num + else: + e = 'Invalid argument to dirs: {0}\n' + return None, e.format(N) + + out = o[idx] + + return out+'\n', None + + +pushd_parser = ArgumentParser(prog="pushd") +pushd_parser.add_argument('dir', nargs='?') +pushd_parser.add_argument('-n', + dest='cd', + help='Suppresses the normal change of directory when' + ' adding directories to the stack, so that only the' + ' stack is manipulated.', + action='store_false') +pushd_parser.add_argument('-q', + dest='quiet', + help='Do not call dirs, regardless of $PUSHD_SILENT', + action='store_true') + +popd_parser = ArgumentParser(prog="popd") +popd_parser.add_argument('dir', nargs='?') +popd_parser.add_argument('-n', + dest='cd', + help='Suppresses the normal change of directory when' + ' adding directories to the stack, so that only the' + ' stack is manipulated.', + action='store_false') +popd_parser.add_argument('-q', + dest='quiet', + help='Do not call dirs, regardless of $PUSHD_SILENT', + action='store_true') + +dirs_parser = ArgumentParser(prog="dirs") +dirs_parser.add_argument('N', nargs='?') +dirs_parser.add_argument('-c', + dest='clear', + help='Clears the directory stack by deleting all of' + ' the entries.', + action='store_true') +dirs_parser.add_argument('-p', + dest='print_long', + help='Print the directory stack with one entry per' + ' line.', + action='store_true') +dirs_parser.add_argument('-v', + dest='verbose', + help='Print the directory stack with one entry per' + ' line, prefixing each entry with its index in the' + ' stack.', + action='store_true') +dirs_parser.add_argument('-l', + dest='long', + help='Produces a longer listing; the default listing' + ' format uses a tilde to denote the home directory.', + action='store_true')