1 # encoding: utf-8 2 """ 3 System command aliases. 4 5 Authors: 6 7 * Fernando Perez 8 * Brian Granger 9 """ 10 11 #----------------------------------------------------------------------------- 12 # Copyright (C) 2008-2011 The IPython Development Team 13 # 14 # Distributed under the terms of the BSD License. 15 # 16 # The full license is in the file COPYING.txt, distributed with this software. 17 #----------------------------------------------------------------------------- 18 19 #----------------------------------------------------------------------------- 20 # Imports 21 #----------------------------------------------------------------------------- 22 23 import os 24 import re 25 import sys 26 27 from traitlets.config.configurable import Configurable 28 from .error import UsageError 29 30 from traitlets import List, Instance 31 from logging import error 32 33 import typing as t 34 35 36 #----------------------------------------------------------------------------- 37 # Utilities 38 #----------------------------------------------------------------------------- 39 40 # This is used as the pattern for calls to split_user_input. 41 shell_line_split = re.compile(r'^(\s*)()(\S+)(.*$)') 42 43 def default_aliases() -> t.List[t.Tuple[str, str]]: 44 """Return list of shell aliases to auto-define. 45 """ 46 # Note: the aliases defined here should be safe to use on a kernel 47 # regardless of what frontend it is attached to. Frontends that use a 48 # kernel in-process can define additional aliases that will only work in 49 # their case. For example, things like 'less' or 'clear' that manipulate 50 # the terminal should NOT be declared here, as they will only work if the 51 # kernel is running inside a true terminal, and not over the network. 52 53 if os.name == 'posix': 54 default_aliases = [('mkdir', 'mkdir'), ('rmdir', 'rmdir'), 55 ('mv', 'mv'), ('rm', 'rm'), ('cp', 'cp'), 56 ('cat', 'cat'), 57 ] 58 # Useful set of ls aliases. The GNU and BSD options are a little 59 # different, so we make aliases that provide as similar as possible 60 # behavior in ipython, by passing the right flags for each platform 61 if sys.platform.startswith('linux'): 62 ls_aliases = [('ls', 'ls -F --color'), 63 # long ls 64 ('ll', 'ls -F -o --color'), 65 # ls normal files only 66 ('lf', 'ls -F -o --color %l | grep ^-'), 67 # ls symbolic links 68 ('lk', 'ls -F -o --color %l | grep ^l'), 69 # directories or links to directories, 70 ('ldir', 'ls -F -o --color %l | grep /$'), 71 # things which are executable 72 ('lx', 'ls -F -o --color %l | grep ^-..x'), 73 ] 74 elif sys.platform.startswith('openbsd') or sys.platform.startswith('netbsd'): 75 # OpenBSD, NetBSD. The ls implementation on these platforms do not support 76 # the -G switch and lack the ability to use colorized output. 77 ls_aliases = [('ls', 'ls -F'), 78 # long ls 79 ('ll', 'ls -F -l'), 80 # ls normal files only 81 ('lf', 'ls -F -l %l | grep ^-'), 82 # ls symbolic links 83 ('lk', 'ls -F -l %l | grep ^l'), 84 # directories or links to directories, 85 ('ldir', 'ls -F -l %l | grep /$'), 86 # things which are executable 87 ('lx', 'ls -F -l %l | grep ^-..x'), 88 ] 89 else: 90 # BSD, OSX, etc. 91 ls_aliases = [('ls', 'ls -F -G'), 92 # long ls 93 ('ll', 'ls -F -l -G'), 94 # ls normal files only 95 ('lf', 'ls -F -l -G %l | grep ^-'), 96 # ls symbolic links 97 ('lk', 'ls -F -l -G %l | grep ^l'), 98 # directories or links to directories, 99 ('ldir', 'ls -F -G -l %l | grep /$'), 100 # things which are executable 101 ('lx', 'ls -F -l -G %l | grep ^-..x'), 102 ] 103 default_aliases = default_aliases + ls_aliases 104 elif os.name in ['nt', 'dos']: 105 default_aliases = [('ls', 'dir /on'), 106 ('ddir', 'dir /ad /on'), ('ldir', 'dir /ad /on'), 107 ('mkdir', 'mkdir'), ('rmdir', 'rmdir'), 108 ('echo', 'echo'), ('ren', 'ren'), ('copy', 'copy'), 109 ] 110 else: 111 default_aliases = [] 112 113 return default_aliases 114 115 116 class AliasError(Exception): 117 pass 118 119 120 class InvalidAliasError(AliasError): 121 pass 122 123 class Alias(object): 124 """Callable object storing the details of one alias. 125 126 Instances are registered as magic functions to allow use of aliases. 127 """ 128 129 # Prepare blacklist 130 blacklist = {'cd','popd','pushd','dhist','alias','unalias'} 131 132 def __init__(self, shell, name, cmd): 133 self.shell = shell 134 self.name = name 135 self.cmd = cmd 136 self.__doc__ = "Alias for `!{}`".format(cmd) 137 self.nargs = self.validate() 138 139 def validate(self): 140 """Validate the alias, and return the number of arguments.""" 141 if self.name in self.blacklist: 142 raise InvalidAliasError("The name %s can't be aliased " 143 "because it is a keyword or builtin." % self.name) 144 try: 145 caller = self.shell.magics_manager.magics['line'][self.name] 146 except KeyError: 147 pass 148 else: 149 if not isinstance(caller, Alias): 150 raise InvalidAliasError("The name %s can't be aliased " 151 "because it is another magic command." % self.name) 152 153 if not (isinstance(self.cmd, str)): 154 raise InvalidAliasError("An alias command must be a string, " 155 "got: %r" % self.cmd) 156 157 nargs = self.cmd.count('%s') - self.cmd.count('%%s') 158 159 if (nargs > 0) and (self.cmd.find('%l') >= 0): 160 raise InvalidAliasError('The %s and %l specifiers are mutually ' 161 'exclusive in alias definitions.') 162 163 return nargs 164 165 def __repr__(self): 166 return "<alias {} for {!r}>".format(self.name, self.cmd) 167 168 def __call__(self, rest=''): 169 cmd = self.cmd 170 nargs = self.nargs 171 # Expand the %l special to be the user's input line 172 if cmd.find('%l') >= 0: 173 cmd = cmd.replace('%l', rest) 174 rest = '' 175 176 if nargs==0: 177 if cmd.find('%%s') >= 1: 178 cmd = cmd.replace('%%s', '%s') 179 # Simple, argument-less aliases 180 cmd = '%s %s' % (cmd, rest) 181 else: 182 # Handle aliases with positional arguments 183 args = rest.split(None, nargs) 184 if len(args) < nargs: 185 raise UsageError('Alias <%s> requires %s arguments, %s given.' % 186 (self.name, nargs, len(args))) 187 cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:])) 188 189 self.shell.system(cmd) 190 191 #----------------------------------------------------------------------------- 192 # Main AliasManager class 193 #----------------------------------------------------------------------------- 194 195 class AliasManager(Configurable): 196 default_aliases: List = List(default_aliases()).tag(config=True) 197 user_aliases: List = List(default_value=[]).tag(config=True) 198 shell = Instance( 199 "IPython.core.interactiveshell.InteractiveShellABC", allow_none=True 200 ) 201 202 def __init__(self, shell=None, **kwargs): 203 super(AliasManager, self).__init__(shell=shell, **kwargs) 204 # For convenient access 205 if self.shell is not None: 206 self.linemagics = self.shell.magics_manager.magics["line"] 207 self.init_aliases() 208 209 def init_aliases(self): 210 # Load default & user aliases 211 for name, cmd in self.default_aliases + self.user_aliases: 212 if ( 213 cmd.startswith("ls ") 214 and self.shell is not None 215 and self.shell.colors == "NoColor" 216 ): 217 cmd = cmd.replace(" --color", "") 218 self.soft_define_alias(name, cmd) 219 220 @property 221 def aliases(self): 222 return [(n, func.cmd) for (n, func) in self.linemagics.items() 223 if isinstance(func, Alias)] 224 225 def soft_define_alias(self, name, cmd): 226 """Define an alias, but don't raise on an AliasError.""" 227 try: 228 self.define_alias(name, cmd) 229 except AliasError as e: 230 error("Invalid alias: %s" % e) 231 232 def define_alias(self, name, cmd): 233 """Define a new alias after validating it. 234 235 This will raise an :exc:`AliasError` if there are validation 236 problems. 237 """ 238 caller = Alias(shell=self.shell, name=name, cmd=cmd) 239 self.shell.magics_manager.register_function(caller, magic_kind='line', 240 magic_name=name) 241 242 def get_alias(self, name): 243 """Return an alias, or None if no alias by that name exists.""" 244 aname = self.linemagics.get(name, None) 245 return aname if isinstance(aname, Alias) else None 246 247 def is_alias(self, name): 248 """Return whether or not a given name has been defined as an alias""" 249 return self.get_alias(name) is not None 250 251 def undefine_alias(self, name): 252 if self.is_alias(name): 253 del self.linemagics[name] 254 else: 255 raise ValueError('%s is not an alias' % name) 256 257 def clear_aliases(self): 258 for name, _ in self.aliases: 259 self.undefine_alias(name) 260 261 def retrieve_alias(self, name): 262 """Retrieve the command to which an alias expands.""" 263 caller = self.get_alias(name) 264 if caller: 265 return caller.cmd 266 else: 267 raise ValueError('%s is not an alias' % name) 268