aliases.py 13.4 KB
Newer Older
adam j hartz's avatar
adam j hartz committed
1
# This file is part of tako
2
# Copyright (c) 2015-2017 Adam Hartz <hartz@mit.edu> and contributors
adam j hartz's avatar
adam j hartz committed
3 4 5 6 7 8 9 10 11 12 13 14 15 16
#
# This program is free software: you can redistribute it and/or modify it under
# the terms of the Soopycat License, version 2.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the Soopycat License for more details.
#
# You should have received a copy of the Soopycat License along with this
# program.  If not, see <https://smatz.net/soopycat>.
#
#
# tako is a fork of xonsh (http://xon.sh)
# xonsh is Copyright (c) 2015-2016 the xonsh developers and is licensed under
17
# the 2-Clause BSD license.
adam j hartz's avatar
adam j hartz committed
18

19
# -*- coding: utf-8 -*-
adam j hartz's avatar
adam j hartz committed
20
"""Aliases for the tako shell."""
21

22
import os
Anthony Scopatz's avatar
Anthony Scopatz committed
23
import sys
24
import shlex
adam j hartz's avatar
adam j hartz committed
25
import inspect
26
import pathlib
adam j hartz's avatar
adam j hartz committed
27
import builtins
adam j hartz's avatar
adam j hartz committed
28

adam j hartz's avatar
adam j hartz committed
29
import takoshell.environ
adam j hartz's avatar
adam j hartz committed
30

adam j hartz's avatar
adam j hartz committed
31
from collections.abc import MutableMapping, Iterable, Sequence
adam j hartz's avatar
adam j hartz committed
32
from argparse import ArgumentParser
Anthony Scopatz's avatar
Anthony Scopatz committed
33

adam j hartz's avatar
adam j hartz committed
34
from takoshell.dirstack import cd, pushd, popd, dirs
adam j hartz's avatar
adam j hartz committed
35
from takoshell.jobs import jobs, fg, bg, clean_jobs, disown
adam j hartz's avatar
adam j hartz committed
36 37
from takoshell.xoreutils import _which
from takoshell.completers._aliases import completer_alias
adam j hartz's avatar
adam j hartz committed
38

adam j hartz's avatar
adam j hartz committed
39

adam j hartz's avatar
adam j hartz committed
40

Anthony Scopatz's avatar
Anthony Scopatz committed
41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86
class Aliases(MutableMapping):
    """Represents a location to hold and look up aliases."""

    def __init__(self, *args, **kwargs):
        self._raw = {}
        self.update(*args, **kwargs)

    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, 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))

    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:
adam j hartz's avatar
adam j hartz committed
87
            expand_path = builtins.__tako_expand_path__
Anthony Scopatz's avatar
Anthony Scopatz committed
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106
            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)

    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]
adam j hartz's avatar
adam j hartz committed
107
        if word in builtins.__tako_env__['TAKO_SETTINGS'].aliases and isinstance(self.get(word), Sequence):
Anthony Scopatz's avatar
Anthony Scopatz committed
108 109 110 111 112 113 114 115 116 117 118 119 120
            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):
121
        if isinstance(val, str):
Anthony Scopatz's avatar
Anthony Scopatz committed
122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155
            self._raw[key] = shlex.split(val)
        else:
            self._raw[key] = val

    def __delitem__(self, key):
        del self._raw[key]

    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))

156

Morten Enemark Lund's avatar
Morten Enemark Lund committed
157

Anthony Scopatz's avatar
Anthony Scopatz committed
158
def exit(args, stdin=None):  # pylint:disable=redefined-builtin,W0622
159
    """Sends signal to exit shell."""
160 161 162 163
    if not clean_jobs():
        # Do not exit if jobs not cleaned up
        return None, None

adam j hartz's avatar
adam j hartz committed
164
    builtins.__tako_exit__ = True
165 166
    print()  # gimme a newline
    return None, None
Anthony Scopatz's avatar
Anthony Scopatz committed
167

adam j hartz's avatar
adam j hartz committed
168

169 170
def source_alias(args, stdin=None):
    """Executes the contents of the provided files in the current context.
Morten Enemark Lund's avatar
Morten Enemark Lund committed
171 172
    If sourced file isn't found in cwd, search for file along $PATH to source
    instead"""
173 174
    for fname in args:
        if not os.path.isfile(fname):
adam j hartz's avatar
adam j hartz committed
175
            fname = takoshell.environ.locate_binary(fname)
176
        with open(fname, 'r') as fp:
177 178 179
            src = fp.read()
        if not src.endswith('\n'):
            src += '\n'
adam j hartz's avatar
adam j hartz committed
180
        builtins.execx(src, 'exec', builtins.__tako_ctx__)
adam j hartz's avatar
adam j hartz committed
181

182

183
def xexec(args, stdin=None):
adam j hartz's avatar
adam j hartz committed
184 185 186
    """exec [-h|--help] command [args...]

    exec (also aliased as xexec) uses the os.execvpe() function to
adam j hartz's avatar
adam j hartz committed
187
    replace the tako process with the specified program. This provides
adam j hartz's avatar
adam j hartz committed
188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203
    the functionality of the bash 'exec' builtin::

        >>> exec bash -l -i
        bash $

    The '-h' and '--help' options print this message and exit.

    Notes
    -----
    This command **is not** the same as the Python builtin function
    exec(). That function is for running Python code. This command,
    which shares the same name as the sh-lang statement, is for launching
    a command directly in the same process. In the event of a name conflict,
    please use the xexec command directly or dive into subprocess mode
    explicitly with ![exec command]. For more details, please see
    http://xon.sh/faq.html#exec.
204
    """
adam j hartz's avatar
adam j hartz committed
205
    if len(args) == 0:
adam j hartz's avatar
adam j hartz committed
206
        return (None, 'tako: exec: no args specified\n', 1)
adam j hartz's avatar
adam j hartz committed
207 208 209
    elif args[0] == '-h' or args[0] == '--help':
        return inspect.getdoc(xexec)
    else:
adam j hartz's avatar
adam j hartz committed
210
        env = builtins.__tako_env__
adam j hartz's avatar
adam j hartz committed
211
        denv = env.detype()
212
        try:
213
            os.execvpe(args[0], args, denv)
214
        except FileNotFoundError as e:
adam j hartz's avatar
adam j hartz committed
215
            return (None, 'tako: exec: file not found: {}: {}'
216
                          '\n'.format(e.args[1], args[0]), 1)
217 218


219
def which(args, stdin=None, stdout=None, stderr=None):
220
    """
adam j hartz's avatar
adam j hartz committed
221
    Checks if each arguments is a tako aliases, then if it's an executable,
222
    then finally return an error code equal to the number of misses.
adam j hartz's avatar
adam j hartz committed
223
    If '-a' flag is passed, run both to return both `tako` match and
224
    `which` match.
225
    """
226 227
    desc = "Parses arguments to which wrapper"
    parser = ArgumentParser('which', description=desc)
228 229
    parser.add_argument('args', type=str, nargs='+',
                        help='The executables or aliases to search for')
230
    parser.add_argument('-a','--all', action='store_true', dest='all',
adam j hartz's avatar
adam j hartz committed
231
                        help='Show all matches in $PATH and takoshell.aliases')
232
    parser.add_argument('-s', '--skip-alias', action='store_true',
adam j hartz's avatar
adam j hartz committed
233
                        help='Do not search in takoshell.aliases', dest='skip')
234
    parser.add_argument('-V', '--version', action='version',
235 236
                        version='{}'.format(_which.__version__),
                        help='Display the version of the python which module '
adam j hartz's avatar
adam j hartz committed
237
                        'used by tako')
238
    parser.add_argument('-v', '--verbose', action='store_true', dest='verbose',
239 240
                        help='Print out how matches were located and show '
                        'near misses on stderr')
241
    parser.add_argument('-p', '--plain', action='store_true', dest='plain',
242
                        help='Do not display alias expansions or location of '
243 244 245
                             'where binaries are found. This is the '
                             'default behavior, but the option can be used to '
                             'override the --verbose option')
246 247 248
    if len(args) == 0:
        parser.print_usage(file=stderr)
        return -1
249
    pargs = parser.parse_args(args)
250

251 252
    if pargs.all:
        pargs.verbose = True
253

adam j hartz's avatar
adam j hartz committed
254
    exts = None
255

256
    failures = []
257 258
    settings = builtins.__tako_env__['TAKO_SETTINGS']
    aliases = settings.aliases
259 260 261
    for arg in pargs.args:
        nmatches = 0
        # skip alias check if user asks to skip
adam j hartz's avatar
adam j hartz committed
262
        if (arg in aliases and not pargs.skip):
263
            if pargs.plain or not pargs.verbose:
adam j hartz's avatar
adam j hartz committed
264 265
                if isinstance(aliases[arg], list):
                    print(' '.join(aliases[arg]), file=stdout)
266 267
                else:
                    print(arg, file=stdout)
268
            else:
269
                print("$TAKO_SETTINGS.aliases['{}'] = {}".format(arg, aliases[arg]), file=stdout)
270 271 272
            nmatches += 1
            if not pargs.all:
                continue
273 274 275 276 277 278 279 280 281 282 283 284 285 286
        else:
            for plugin in settings.plugins:
                local_aliases = settings.plugins[plugin].aliases
                if arg in local_aliases and not pargs.skip:
                    if pargs.plain or not pargs.verbose:
                        if isinstance(local_aliases[arg], list):
                            print(' '.join(local_aliases[arg]), file=stdout)
                        else:
                            print(arg, file=stdout)
                    else:
                        print("$TAKO_SETTINGS.plugins.%s.aliases['{}'] = {}".format(repr(plugin.lower()), arg, local_aliases[arg]), file=stdout)
                    nmatches += 1
                    if not pargs.all:
                        continue
287 288 289
        # which.whichgen gives the nicest 'verbose' output if PATH is taken
        # from os.environ so we temporarily override it with
        # __xosnh_env__['PATH']
290
        original_os_path = os.environ['PATH']
adam j hartz's avatar
adam j hartz committed
291
        os.environ['PATH'] = builtins.__tako_env__.detype()['PATH']
292
        matches = _which.whichgen(arg, exts=exts, verbose=pargs.verbose)
293
        for abs_name, from_where in matches:
294
            if pargs.plain or not pargs.verbose:
295
                print(abs_name, file=stdout)
296
            else:
297
                print('{} ({})'.format(abs_name, from_where), file=stdout)
298 299 300
            nmatches += 1
            if not pargs.all:
                break
Hugo Wang's avatar
Hugo Wang committed
301
        os.environ['PATH'] = original_os_path
302 303 304
        if not nmatches:
            failures.append(arg)
    if len(failures) == 0:
305
        return 0
306
    else:
307 308
        print('{} not in $PATH'.format(', '.join(failures)), file=stderr, end='')
        if not pargs.skip:
309
            print(' or aliases', file=stderr, end='')
310
        print('', end='\n')
311
        return len(failures)
312 313


314 315
def suppress_welcome(args, stdin=None):
    fileloc = os.path.join(builtins.__tako_env__['XDG_CONFIG_HOME'], 'tako', 'suppress_message')
316 317 318
    filedir = os.path.dirname(fileloc)
    if not os.path.isdir(filedir):
        os.makedirs(filedir)
319 320 321 322
    pathlib.Path(fileloc).touch()
    return "Suppressing tako welcome message in the future.\n"


adam j hartz's avatar
adam j hartz committed
323
def license(args, stdin=None):
adam j hartz's avatar
adam j hartz committed
324
    return open(os.path.join(os.path.dirname(__file__), 'LICENSE')).read()
adam j hartz's avatar
adam j hartz committed
325 326


Anthony Scopatz's avatar
Anthony Scopatz committed
327 328 329
def showcmd(args, stdin=None):
    """usage: showcmd [-h|--help|cmd args]

adam j hartz's avatar
adam j hartz committed
330 331
    Displays the command and arguments as a list of strings that tako would
    run in subprocess mode. This is useful for determining how tako evaluates
Anthony Scopatz's avatar
Anthony Scopatz committed
332 333 334 335 336 337 338 339 340 341 342 343 344 345 346
    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)


adam j hartz's avatar
adam j hartz committed
347 348 349 350 351 352 353 354
default_aliases = {
    'cd': cd,
    'pushd': pushd,
    'popd': popd,
    'dirs': dirs,
    'jobs': jobs,
    'fg': fg,
    'bg': bg,
adam j hartz's avatar
adam j hartz committed
355
    'disown': disown,
adam j hartz's avatar
adam j hartz committed
356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371
    'EOF': exit,
    'exit': exit,
    'quit': exit,
    'tako_license': license,
    'xexec': xexec,
    'exec': xexec,
    'source': source_alias,
    'scp-resume': ['rsync', '--partial', '-h', '--progress', '--rsh=ssh'],
    'showcmd': showcmd,
    'ipynb': ['jupyter', 'notebook', '--no-browser'],
    'which': which,
    'completer': completer_alias,
    'grep': ['grep', '--color=auto'],
    'egrep': ['egrep', '--color=auto'],
    'fgrep': ['fgrep', '--color=auto'],
    'ls': ['ls', '--color=auto', '-v'],
372
    'suppress_tako_welcome_message': suppress_welcome,
adam j hartz's avatar
adam j hartz committed
373
}