# -*- coding: utf-8 -*-
"""Aliases for the xonsh shell."""
import os
import sys
import shlex
import argparse
import builtins
import collections.abc as abc
from xonsh.lazyasd import lazyobject
from xonsh.dirstack import cd, pushd, popd, dirs, _get_cwd
from xonsh.environ import locate_binary
from xonsh.foreign_shells import foreign_shell_data
from xonsh.jobs import jobs, fg, bg, clean_jobs
from xonsh.history import history_main
from xonsh.platform import (ON_ANACONDA, ON_DARWIN, ON_WINDOWS, ON_FREEBSD,
scandir)
from xonsh.proc import foreground
from xonsh.replay import replay_main
from xonsh.timings import timeit_alias
from xonsh.tools import (XonshError, argvquote, escape_windows_cmd_string,
to_bool)
from xonsh.xontribs import xontribs_main
from xonsh.xoreutils import _which
from xonsh.completers._aliases import completer_alias
[docs]class Aliases(abc.MutableMapping):
"""Represents a location to hold and look up aliases."""
def __init__(self, *args, **kwargs):
self._raw = {}
self.update(*args, **kwargs)
[docs] def get(self, key, default=None):
"""Returns the (possibly modified) value. If the key is not present,
then `default` is returned.
If the value is callable, it is returned without modification. If it
is an iterable of strings it will be evaluated recursively to expand
other aliases, resulting in a new list or a "partially applied"
callable.
"""
val = self._raw.get(key)
if val is None:
return default
elif isinstance(val, abc.Iterable) or callable(val):
return self.eval_alias(val, seen_tokens={key})
else:
msg = 'alias of {!r} has an inappropriate type: {!r}'
raise TypeError(msg.format(key, val))
[docs] def eval_alias(self, value, seen_tokens=frozenset(), acc_args=()):
"""
"Evaluates" the alias `value`, by recursively looking up the leftmost
token and "expanding" if it's also an alias.
A value like ["cmd", "arg"] might transform like this:
> ["cmd", "arg"] -> ["ls", "-al", "arg"] -> callable()
where `cmd=ls -al` and `ls` is an alias with its value being a
callable. The resulting callable will be "partially applied" with
["-al", "arg"].
"""
# Beware of mutability: default values for keyword args are evaluated
# only once.
if callable(value):
if acc_args: # Partial application
def _alias(args, stdin=None):
args = list(acc_args) + args
return value(args, stdin=stdin)
return _alias
else:
return value
else:
expand_path = builtins.__xonsh_expand_path__
token, *rest = map(expand_path, value)
if token in seen_tokens or token not in self._raw:
# ^ Making sure things like `egrep=egrep --color=auto` works,
# and that `l` evals to `ls --color=auto -CF` if `l=ls -CF`
# and `ls=ls --color=auto`
rtn = [token]
rtn.extend(rest)
rtn.extend(acc_args)
return rtn
else:
seen_tokens = seen_tokens | {token}
acc_args = rest + list(acc_args)
return self.eval_alias(self._raw[token], seen_tokens, acc_args)
[docs] def expand_alias(self, line):
"""Expands any aliases present in line if alias does not point to a
builtin function and if alias is only a single command.
"""
word = line.split(' ', 1)[0]
if word in builtins.aliases and isinstance(self.get(word),
abc.Sequence):
word_idx = line.find(word)
expansion = ' '.join(self.get(word))
line = line[:word_idx] + expansion + line[word_idx+len(word):]
return line
#
# Mutable mapping interface
#
def __getitem__(self, key):
return self._raw[key]
def __setitem__(self, key, val):
if isinstance(val, str):
self._raw[key] = shlex.split(val)
else:
self._raw[key] = val
def __delitem__(self, key):
del self._raw[key]
[docs] def update(self, *args, **kwargs):
for key, val in dict(*args, **kwargs).items():
self[key] = val
def __iter__(self):
yield from self._raw
def __len__(self):
return len(self._raw)
def __str__(self):
return str(self._raw)
def __repr__(self):
return '{0}.{1}({2})'.format(self.__class__.__module__,
self.__class__.__name__, self._raw)
def _repr_pretty_(self, p, cycle):
name = '{0}.{1}'.format(self.__class__.__module__,
self.__class__.__name__)
with p.group(0, name + '(', ')'):
if cycle:
p.text('...')
elif len(self):
p.break_()
p.pretty(dict(self))
[docs]def xonsh_exit(args, stdin=None):
"""Sends signal to exit shell."""
if not clean_jobs():
# Do not exit if jobs not cleaned up
return None, None
builtins.__xonsh_exit__ = True
print() # gimme a newline
return None, None
@lazyobject
def _SOURCE_FOREIGN_PARSER():
desc = "Sources a file written in a foreign shell language."
parser = argparse.ArgumentParser('source-foreign', description=desc)
parser.add_argument('shell', help='Name or path to the foreign shell')
parser.add_argument('files_or_code', nargs='+',
help='file paths to source or code in the target '
'language.')
parser.add_argument('-i', '--interactive', type=to_bool, default=True,
help='whether the sourced shell should be interactive',
dest='interactive')
parser.add_argument('-l', '--login', type=to_bool, default=False,
help='whether the sourced shell should be login',
dest='login')
parser.add_argument('--envcmd', default=None, dest='envcmd',
help='command to print environment')
parser.add_argument('--aliascmd', default=None, dest='aliascmd',
help='command to print aliases')
parser.add_argument('--extra-args', default=(), dest='extra_args',
type=(lambda s: tuple(s.split())),
help='extra arguments needed to run the shell')
parser.add_argument('-s', '--safe', type=to_bool, default=True,
help='whether the source shell should be run safely, '
'and not raise any errors, even if they occur.',
dest='safe')
parser.add_argument('-p', '--prevcmd', default=None, dest='prevcmd',
help='command(s) to run before any other commands, '
'replaces traditional source.')
parser.add_argument('--postcmd', default='', dest='postcmd',
help='command(s) to run after all other commands')
parser.add_argument('--funcscmd', default=None, dest='funcscmd',
help='code to find locations of all native functions '
'in the shell language.')
parser.add_argument('--sourcer', default=None, dest='sourcer',
help='the source command in the target shell '
'language, default: source.')
parser.add_argument('--use-tmpfile', type=to_bool, default=False,
help='whether the commands for source shell should be '
'written to a temporary file.',
dest='use_tmpfile')
parser.add_argument('--seterrprevcmd', default=None, dest='seterrprevcmd',
help='command(s) to set exit-on-error before any'
'other commands.')
parser.add_argument('--seterrpostcmd', default=None, dest='seterrpostcmd',
help='command(s) to set exit-on-error after all'
'other commands.')
return parser
[docs]def source_foreign(args, stdin=None):
"""Sources a file written in a foreign shell language."""
ns = _SOURCE_FOREIGN_PARSER.parse_args(args)
if ns.prevcmd is not None:
pass # don't change prevcmd if given explicitly
elif os.path.isfile(ns.files_or_code[0]):
# we have filename to source
ns.prevcmd = '{} "{}"'.format(ns.sourcer, '" "'.join(ns.files_or_code))
elif ns.prevcmd is None:
ns.prevcmd = ' '.join(ns.files_or_code) # code to run, no files
foreign_shell_data.cache_clear() # make sure that we don't get prev src
fsenv, fsaliases = foreign_shell_data(shell=ns.shell, login=ns.login,
interactive=ns.interactive,
envcmd=ns.envcmd,
aliascmd=ns.aliascmd,
extra_args=ns.extra_args,
safe=ns.safe, prevcmd=ns.prevcmd,
postcmd=ns.postcmd,
funcscmd=ns.funcscmd,
sourcer=ns.sourcer,
use_tmpfile=ns.use_tmpfile,
seterrprevcmd=ns.seterrprevcmd,
seterrpostcmd=ns.seterrpostcmd)
if fsenv is None:
return (None, 'xonsh: error: Source failed: '
'{}\n'.format(ns.prevcmd), 1)
# apply results
env = builtins.__xonsh_env__
denv = env.detype()
for k, v in fsenv.items():
if k in denv and v == denv[k]:
continue # no change from original
env[k] = v
# Remove any env-vars that were unset by the script.
for k in denv:
if k not in fsenv:
env.pop(k, None)
# Update aliases
baliases = builtins.aliases
for k, v in fsaliases.items():
if k in baliases and v == baliases[k]:
continue # no change from original
baliases[k] = v
[docs]def source_alias(args, stdin=None):
"""Executes the contents of the provided files in the current context.
If sourced file isn't found in cwd, search for file along $PATH to source
instead"""
for fname in args:
if not os.path.isfile(fname):
fname = locate_binary(fname)
with open(fname, 'r') as fp:
builtins.execx(fp.read(), 'exec', builtins.__xonsh_ctx__)
[docs]def source_cmd(args, stdin=None):
"""Simple cmd.exe-specific wrapper around source-foreign."""
args = list(args)
fpath = locate_binary(args[0])
args[0] = fpath if fpath else args[0]
if not os.path.isfile(args[0]):
return (None, 'xonsh: error: File not found: {}\n'.format(args[0]), 1)
prevcmd = 'call '
prevcmd += ' '.join([argvquote(arg, force=True) for arg in args])
prevcmd = escape_windows_cmd_string(prevcmd)
args.append('--prevcmd={}'.format(prevcmd))
args.insert(0, 'cmd')
args.append('--interactive=0')
args.append('--sourcer=call')
args.append('--envcmd=set')
args.append('--seterrpostcmd=if errorlevel 1 exit 1')
args.append('--use-tmpfile=1')
with builtins.__xonsh_env__.swap(PROMPT='$P$G'):
return source_foreign(args, stdin=stdin)
[docs]def xexec(args, stdin=None):
"""Replaces current process with command specified and passes in the
current xonsh environment.
"""
env = builtins.__xonsh_env__
denv = env.detype()
if len(args) > 0:
try:
os.execvpe(args[0], args, denv)
except FileNotFoundError as e:
return (None, 'xonsh: exec: file not found: {}: {}'
'\n'.format(e.args[1], args[0]), 1)
else:
return (None, 'xonsh: exec: no args specified\n', 1)
@lazyobject
def _BANG_N_PARSER():
parser = argparse.ArgumentParser('!n', usage='!n <n>',
description="Re-runs the nth command as specified in the "
"argument.")
parser.add_argument('n', type=int, help='the command to rerun, may be '
'negative')
return parser
[docs]def bang_n(args, stdin=None):
"""Re-runs the nth command as specified in the argument."""
ns = _BANG_N_PARSER.parse_args(args)
hist = builtins.__xonsh_history__
nhist = len(hist)
n = nhist + ns.n if ns.n < 0 else ns.n
if n < 0 or n >= nhist:
raise IndexError('n out of range, {0} for history len {1}'.format(ns.n, nhist))
cmd = hist.inps[n]
if cmd.startswith('!'):
raise XonshError('xonsh: error: recursive call to !n')
builtins.execx(cmd)
[docs]def bang_bang(args, stdin=None):
"""Re-runs the last command. Just a wrapper around bang_n."""
return bang_n(['-1'])
[docs]class AWitchAWitch(argparse.Action):
SUPPRESS = '==SUPPRESS=='
def __init__(self, option_strings, version=None, dest=SUPPRESS,
default=SUPPRESS, **kwargs):
super().__init__(option_strings=option_strings, dest=dest,
default=default, nargs=0, **kwargs)
def __call__(self, parser, namespace, values, option_string=None):
import webbrowser
webbrowser.open('https://github.com/xonsh/xonsh/commit/f49b400')
parser.exit()
[docs]def which(args, stdin=None, stdout=None, stderr=None):
"""
Checks if each arguments is a xonsh aliases, then if it's an executable,
then finally return an error code equal to the number of misses.
If '-a' flag is passed, run both to return both `xonsh` match and
`which` match.
"""
desc = "Parses arguments to which wrapper"
parser = argparse.ArgumentParser('which', description=desc)
parser.add_argument('args', type=str, nargs='+',
help='The executables or aliases to search for')
parser.add_argument('-a','--all', action='store_true', dest='all',
help='Show all matches in $PATH and xonsh.aliases')
parser.add_argument('-s', '--skip-alias', action='store_true',
help='Do not search in xonsh.aliases', dest='skip')
parser.add_argument('-V', '--version', action='version',
version='{}'.format(_which.__version__),
help='Display the version of the python which module '
'used by xonsh')
parser.add_argument('-v', '--verbose', action='store_true', dest='verbose',
help='Print out how matches were located and show '
'near misses on stderr')
parser.add_argument('-p', '--plain', action='store_true', dest='plain',
help='Do not display alias expansions or location of '
'where binaries are found. This is the '
'default behavior, but the option can be used to '
'override the --verbose option')
parser.add_argument('--very-small-rocks', action=AWitchAWitch)
if ON_WINDOWS:
parser.add_argument('-e', '--exts', nargs='*', type=str,
help='Specify a list of extensions to use instead '
'of the standard list for this system. This can '
'effectively be used as an optimization to, for '
'example, avoid stat\'s of "foo.vbs" when '
'searching for "foo" and you know it is not a '
'VisualBasic script but ".vbs" is on PATHEXT. '
'This option is only supported on Windows',
dest='exts')
if len(args) == 0:
parser.print_usage(file=stderr)
return -1
pargs = parser.parse_args(args)
if pargs.all:
pargs.verbose = True
if ON_WINDOWS:
if pargs.exts:
exts = pargs.exts
else:
exts = builtins.__xonsh_env__['PATHEXT']
else:
exts = None
failures = []
for arg in pargs.args:
nmatches = 0
# skip alias check if user asks to skip
if (arg in builtins.aliases and not pargs.skip):
if pargs.plain or not pargs.verbose:
if isinstance(builtins.aliases[arg], list):
print(' '.join(builtins.aliases[arg]), file=stdout)
else:
print(arg, file=stdout)
else:
print("aliases['{}'] = {}".format(arg, builtins.aliases[arg]), file=stdout)
nmatches += 1
if not pargs.all:
continue
# which.whichgen gives the nicest 'verbose' output if PATH is taken
# from os.environ so we temporarily override it with
# __xosnh_env__['PATH']
original_os_path = os.environ['PATH']
os.environ['PATH'] = builtins.__xonsh_env__.detype()['PATH']
matches = _which.whichgen(arg, exts=exts, verbose=pargs.verbose)
for abs_name, from_where in matches:
if ON_WINDOWS:
# Use list dir to get correct case for the filename
# i.e. windows is case insensitive but case preserving
p, f = os.path.split(abs_name)
f = next(s.name for s in scandir(p) if s.name.lower() == f.lower())
abs_name = os.path.join(p, f)
if builtins.__xonsh_env__.get('FORCE_POSIX_PATHS', False):
abs_name.replace(os.sep, os.altsep)
if pargs.plain or not pargs.verbose:
print(abs_name, file=stdout)
else:
print('{} ({})'.format(abs_name, from_where), file=stdout)
nmatches += 1
if not pargs.all:
break
os.environ['PATH'] = original_os_path
if not nmatches:
failures.append(arg)
if len(failures) == 0:
return 0
else:
print('{} not in $PATH'.format(', '.join(failures)), file=stderr, end='')
if not pargs.skip:
print(' or xonsh.builtins.aliases', file=stderr, end='')
print('', end='\n')
return len(failures)
[docs]def xonfig(args, stdin=None):
"""Runs the xonsh configuration utility."""
from xonsh.xonfig import xonfig_main # lazy import
return xonfig_main(args)
@foreground
[docs]def trace(args, stdin=None):
"""Runs the xonsh tracer utility."""
from xonsh.tracer import tracermain # lazy import
try:
return tracermain(args)
except SystemExit:
pass
[docs]def showcmd(args, stdin=None):
"""usage: showcmd [-h|--help|cmd args]
Displays the command and arguments as a list of strings that xonsh would
run in subprocess mode. This is useful for determining how xonsh evaluates
your commands and arguments prior to running these commands.
optional arguments:
-h, --help show this help message and exit
example:
>>> showcmd echo $USER can't hear "the sea"
['echo', 'I', "can't", 'hear', 'the sea']
"""
if len(args) == 0 or (len(args) == 1 and args[0] in {'-h', '--help'}):
print(showcmd.__doc__.rstrip().replace('\n ', '\n'))
else:
sys.displayhook(args)
[docs]def make_default_aliases():
"""Creates a new default aliases dictionary."""
default_aliases = {
'cd': cd,
'pushd': pushd,
'popd': popd,
'dirs': dirs,
'jobs': jobs,
'fg': fg,
'bg': bg,
'EOF': xonsh_exit,
'exit': xonsh_exit,
'quit': xonsh_exit,
'xexec': xexec,
'source': source_alias,
'source-zsh': ['source-foreign', 'zsh', '--sourcer=source'],
'source-bash': ['source-foreign', 'bash', '--sourcer=source'],
'source-cmd': source_cmd,
'source-foreign': source_foreign,
'history': history_main,
'replay': replay_main,
'!!': bang_bang,
'!n': bang_n,
'trace': trace,
'timeit': timeit_alias,
'xonfig': xonfig,
'scp-resume': ['rsync', '--partial', '-h', '--progress', '--rsh=ssh'],
'showcmd': showcmd,
'ipynb': ['jupyter', 'notebook', '--no-browser'],
'which': which,
'xontrib': xontribs_main,
'completer': completer_alias
}
if ON_WINDOWS:
# Borrow builtin commands from cmd.exe.
windows_cmd_aliases = {
'cls',
'copy',
'del',
'dir',
'erase',
'md',
'mkdir',
'mklink',
'move',
'rd',
'ren',
'rename',
'rmdir',
'time',
'type',
'vol'
}
for alias in windows_cmd_aliases:
default_aliases[alias] = ['cmd', '/c', alias]
default_aliases['call'] = ['source-cmd']
default_aliases['source-bat'] = ['source-cmd']
default_aliases['clear'] = 'cls'
if ON_ANACONDA:
# Add aliases specific to the Anaconda python distribution.
default_aliases['activate'] = ['source-cmd', 'activate.bat']
default_aliases['deactivate'] = ['source-cmd', 'deactivate.bat']
if not locate_binary('sudo'):
import xonsh.winutils as winutils
def sudo(args, sdin=None):
if len(args) < 1:
print('You need to provide an executable to run as '
'Administrator.')
return
cmd = args[0]
if locate_binary(cmd):
return winutils.sudo(cmd, args[1:])
elif cmd.lower() in windows_cmd_aliases:
args = ['/D', '/C', 'CD', _get_cwd(), '&&'] + args
return winutils.sudo('cmd', args)
else:
msg = 'Cannot find the path for executable "{0}".'
print(msg.format(cmd))
default_aliases['sudo'] = sudo
elif ON_DARWIN:
default_aliases['ls'] = ['ls', '-G']
elif ON_FREEBSD:
default_aliases['grep'] = ['grep', '--color=auto']
default_aliases['egrep'] = ['egrep', '--color=auto']
default_aliases['fgrep'] = ['fgrep', '--color=auto']
default_aliases['ls'] = ['ls', '-G']
else:
default_aliases['grep'] = ['grep', '--color=auto']
default_aliases['egrep'] = ['egrep', '--color=auto']
default_aliases['fgrep'] = ['fgrep', '--color=auto']
default_aliases['ls'] = ['ls', '--color=auto', '-v']
return default_aliases