# -*- coding: utf-8 -*-
"""Directory stack and associated utilities for the xonsh shell."""
import os
import glob
import argparse
import builtins
from xonsh.lazyasd import lazyobject
from xonsh.tools import get_sep
DIRSTACK = []
"""A list containing the currently remembered directories."""
def _get_cwd():
try:
return os.getcwd()
except (OSError, FileNotFoundError):
return None
def _change_working_directory(newdir):
env = builtins.__xonsh_env__
old = env['PWD']
new = os.path.join(old, newdir)
try:
os.chdir(os.path.abspath(new))
except (OSError, FileNotFoundError):
if new.endswith(get_sep()):
new = new[:-1]
if os.path.basename(new) == '..':
env['PWD'] = new
return
if old is not None:
env['OLDPWD'] = old
if new is not None:
env['PWD'] = os.path.abspath(new)
def _try_cdpath(apath):
# NOTE: this CDPATH implementation differs from the bash one.
# In bash if a CDPATH is set, an unqualified local folder
# is considered after all CDPATHs, example:
# CDPATH=$HOME/src (with src/xonsh/ inside)
# $ cd xonsh -> src/xonsh (whith xonsh/xonsh)
# a second $ cd xonsh has no effects, to move in the nested xonsh
# in bash a full $ cd ./xonsh is needed.
# In xonsh a relative folder is allways preferred.
env = builtins.__xonsh_env__
cdpaths = env.get('CDPATH')
for cdp in cdpaths:
globber = builtins.__xonsh_expand_path__(os.path.join(cdp, apath))
for cdpath_prefixed_path in glob.iglob(globber):
return cdpath_prefixed_path
return apath
[docs]def cd(args, stdin=None):
"""Changes the directory.
If no directory is specified (i.e. if `args` is None) then this
changes to the current user's home directory.
"""
env = builtins.__xonsh_env__
oldpwd = env.get('OLDPWD', None)
cwd = env['PWD']
if len(args) == 0:
d = os.path.expanduser('~')
elif len(args) == 1:
d = os.path.expanduser(args[0])
if not os.path.isdir(d):
if d == '-':
if oldpwd is not None:
d = oldpwd
else:
return '', 'cd: no previous directory stored\n', 1
elif d.startswith('-'):
try:
num = int(d[1:])
except ValueError:
return '', 'cd: Invalid destination: {0}\n'.format(d), 1
if num == 0:
return None, None, 0
elif num < 0:
return '', 'cd: Invalid destination: {0}\n'.format(d), 1
elif num > len(DIRSTACK):
e = 'cd: Too few elements in dirstack ({0} elements)\n'
return '', e.format(len(DIRSTACK)), 1
else:
d = DIRSTACK[num - 1]
else:
d = _try_cdpath(d)
else:
return '', 'cd takes 0 or 1 arguments, not {0}\n'.format(len(args)), 1
if not os.path.exists(d):
return '', 'cd: no such file or directory: {0}\n'.format(d), 1
if not os.path.isdir(d):
return '', 'cd: {0} is not a directory\n'.format(d), 1
if not os.access(d, os.X_OK):
return '', 'cd: permission denied: {0}\n'.format(d), 1
# now, push the directory onto the dirstack if AUTO_PUSHD is set
if cwd is not None and env.get('AUTO_PUSHD'):
pushd(['-n', '-q', cwd])
_change_working_directory(d)
return None, None, 0
@lazyobject
def pushd_parser():
parser = argparse.ArgumentParser(prog="pushd")
parser.add_argument('dir', nargs='?')
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')
parser.add_argument('-q',
dest='quiet',
help='Do not call dirs, regardless of $PUSHD_SILENT',
action='store_true')
return parser
[docs]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, 1
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, 1
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), 1
if num < 0:
e = 'Invalid argument to pushd: {0}\n'
return None, e.format(args.dir), 1
if num > len(DIRSTACK):
e = 'Too few elements in dirstack ({0} elements)\n'
return None, e.format(len(DIRSTACK)), 1
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), 1
if new_pwd is not None:
if args.cd:
DIRSTACK.insert(0, os.path.expanduser(pwd))
_change_working_directory(new_pwd)
else:
DIRSTACK.insert(0, os.path.expanduser(new_pwd))
maxsize = env.get('DIRSTACK_SIZE')
if len(DIRSTACK) > maxsize:
DIRSTACK = DIRSTACK[:maxsize]
if not args.quiet and not env.get('PUSHD_SILENT'):
return dirs([], None)
return None, None, 0
@lazyobject
def popd_parser():
parser = argparse.ArgumentParser(prog="popd")
parser.add_argument('dir', nargs='?')
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')
parser.add_argument('-q',
dest='quiet',
help='Do not call dirs, regardless of $PUSHD_SILENT',
action='store_true')
return parser
[docs]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, 1
env = builtins.__xonsh_env__
if env.get('PUSHD_MINUS'):
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, 1
else:
try:
num = int(args.dir[1:])
except ValueError:
e = 'Invalid argument to popd: {0}\n'
return None, e.format(args.dir), 1
if num < 0:
e = 'Invalid argument to popd: {0}\n'
return None, e.format(args.dir), 1
if num > len(DIRSTACK):
e = 'Too few elements in dirstack ({0} elements)\n'
return None, e.format(len(DIRSTACK)), 1
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), 1
if new_pwd is not None:
e = None
if args.cd:
_change_working_directory(new_pwd)
if not args.quiet and not env.get('PUSHD_SILENT'):
return dirs([], None)
return None, None, 0
@lazyobject
def dirs_parser():
parser = argparse.ArgumentParser(prog="dirs")
parser.add_argument('N', nargs='?')
parser.add_argument('-c',
dest='clear',
help='Clears the directory stack by deleting all of'
' the entries.',
action='store_true')
parser.add_argument('-p',
dest='print_long',
help='Print the directory stack with one entry per'
' line.',
action='store_true')
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')
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')
return parser
[docs]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
try:
args = dirs_parser.parse_args(args)
except SystemExit:
return None, None
env = builtins.__xonsh_env__
dirstack = [os.path.expanduser(env['PWD'])] + DIRSTACK
if env.get('PUSHD_MINUS'):
BACKWARD = '-'
FORWARD = '+'
else:
BACKWARD = '-'
FORWARD = '+'
if args.clear:
DIRSTACK = []
return None, None, 0
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), 1
if num < 0:
e = 'Invalid argument to dirs: {0}\n'
return None, e.format(len(o)), 1
if num >= len(o):
e = 'Too few elements in dirstack ({0} elements)\n'
return None, e.format(len(o)), 1
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), 1
out = o[idx]
return out + '\n', None, 0