1# encoding: utf-8
2"""
3System command aliases.
4
5Authors:
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
23import os
24import re
25import sys
26
27from traitlets.config.configurable import Configurable
28from .error import UsageError
29
30from traitlets import List, Instance
31from logging import error
32
33import typing as t
34
35
36#-----------------------------------------------------------------------------
37# Utilities
38#-----------------------------------------------------------------------------
39
40# This is used as the pattern for calls to split_user_input.
41shell_line_split = re.compile(r'^(\s*)()(\S+)(.*$)')
42
43def 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
116class AliasError(Exception):
117 pass
118
119
120class InvalidAliasError(AliasError):
121 pass
122
123
124class Alias:
125 """Callable object storing the details of one alias.
126
127 Instances are registered as magic functions to allow use of aliases.
128 """
129
130 # Prepare blacklist
131 blacklist = {'cd','popd','pushd','dhist','alias','unalias'}
132
133 def __init__(self, shell, name, cmd):
134 self.shell = shell
135 self.name = name
136 self.cmd = cmd
137 self.__doc__ = "Alias for `!{}`".format(cmd)
138 self.nargs = self.validate()
139
140 def validate(self):
141 """Validate the alias, and return the number of arguments."""
142 if self.name in self.blacklist:
143 raise InvalidAliasError("The name %s can't be aliased "
144 "because it is a keyword or builtin." % self.name)
145 try:
146 caller = self.shell.magics_manager.magics['line'][self.name]
147 except KeyError:
148 pass
149 else:
150 if not isinstance(caller, Alias):
151 raise InvalidAliasError("The name %s can't be aliased "
152 "because it is another magic command." % self.name)
153
154 if not (isinstance(self.cmd, str)):
155 raise InvalidAliasError("An alias command must be a string, "
156 "got: %r" % self.cmd)
157
158 nargs = self.cmd.count('%s') - self.cmd.count('%%s')
159
160 if (nargs > 0) and (self.cmd.find('%l') >= 0):
161 raise InvalidAliasError('The %s and %l specifiers are mutually '
162 'exclusive in alias definitions.')
163
164 return nargs
165
166 def __repr__(self):
167 return "<alias {} for {!r}>".format(self.name, self.cmd)
168
169 def __call__(self, rest=''):
170 cmd = self.cmd
171 nargs = self.nargs
172 # Expand the %l special to be the user's input line
173 if cmd.find('%l') >= 0:
174 cmd = cmd.replace('%l', rest)
175 rest = ''
176
177 if nargs==0:
178 if cmd.find('%%s') >= 1:
179 cmd = cmd.replace('%%s', '%s')
180 # Simple, argument-less aliases
181 cmd = '%s %s' % (cmd, rest)
182 else:
183 # Handle aliases with positional arguments
184 args = rest.split(None, nargs)
185 if len(args) < nargs:
186 raise UsageError('Alias <%s> requires %s arguments, %s given.' %
187 (self.name, nargs, len(args)))
188 cmd = '%s %s' % (cmd % tuple(args[:nargs]),' '.join(args[nargs:]))
189
190 self.shell.system(cmd)
191
192#-----------------------------------------------------------------------------
193# Main AliasManager class
194#-----------------------------------------------------------------------------
195
196class AliasManager(Configurable):
197 default_aliases: List = List(default_aliases()).tag(config=True)
198 user_aliases: List = List(default_value=[]).tag(config=True)
199 shell = Instance(
200 "IPython.core.interactiveshell.InteractiveShellABC", allow_none=True
201 )
202
203 def __init__(self, shell=None, **kwargs):
204 super(AliasManager, self).__init__(shell=shell, **kwargs)
205 # For convenient access
206 if self.shell is not None:
207 self.linemagics = self.shell.magics_manager.magics["line"]
208 self.init_aliases()
209
210 def init_aliases(self):
211 # Load default & user aliases
212 for name, cmd in self.default_aliases + self.user_aliases:
213 if (
214 cmd.startswith("ls ")
215 and self.shell is not None
216 and self.shell.colors == "nocolor"
217 ):
218 cmd = cmd.replace(" --color", "")
219 self.soft_define_alias(name, cmd)
220
221 @property
222 def aliases(self):
223 return [(n, func.cmd) for (n, func) in self.linemagics.items()
224 if isinstance(func, Alias)]
225
226 def soft_define_alias(self, name, cmd):
227 """Define an alias, but don't raise on an AliasError."""
228 try:
229 self.define_alias(name, cmd)
230 except AliasError as e:
231 error("Invalid alias: %s" % e)
232
233 def define_alias(self, name, cmd):
234 """Define a new alias after validating it.
235
236 This will raise an :exc:`AliasError` if there are validation
237 problems.
238 """
239 caller = Alias(shell=self.shell, name=name, cmd=cmd)
240 self.shell.magics_manager.register_function(caller, magic_kind='line',
241 magic_name=name)
242
243 def get_alias(self, name):
244 """Return an alias, or None if no alias by that name exists."""
245 aname = self.linemagics.get(name, None)
246 return aname if isinstance(aname, Alias) else None
247
248 def is_alias(self, name):
249 """Return whether or not a given name has been defined as an alias"""
250 return self.get_alias(name) is not None
251
252 def undefine_alias(self, name):
253 if self.is_alias(name):
254 del self.linemagics[name]
255 else:
256 raise ValueError('%s is not an alias' % name)
257
258 def clear_aliases(self):
259 for name, _ in self.aliases:
260 self.undefine_alias(name)
261
262 def retrieve_alias(self, name):
263 """Retrieve the command to which an alias expands."""
264 caller = self.get_alias(name)
265 if caller:
266 return caller.cmd
267 else:
268 raise ValueError('%s is not an alias' % name)