1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 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 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 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 156 157 158 159 160 161 162 163 164 165 166 167
|
'''Shell utility functions.'''
import re
import collections
from . import cli
from . import utils
def make_identifier(string):
'''Make `string` a valid shell identifier.
This function replaces any dashes '-' with underscores '_',
removes any characters that are not letters, digits, or underscores,
and ensures that consecutive underscores are replaced with a single underscore.
Args:
string (str): The input string to be converted into a valid shell identifier.
Returns:
str: The modified string that is a valid shell identifier.
'''
string = string.replace('-', '_')
string = re.sub('[^a-zA-Z0-9_]', '', string)
string = re.sub('_+', '_', string)
if string and string[0] in '0123456789':
return '_' + string
return string
def needs_escape(string):
'''Return if string needs escaping.'''
return not re.fullmatch('[a-zA-Z0-9_@%+=:,./-]+', string)
def escape(string, escape_empty_string=True):
'''Escapes special characters in a string for safe usage in shell commands or scripts.
Args:
string (str): The input string to be escaped.
escape_empty_string (bool, optional): Determines whether to escape an empty string or not.
Defaults to True.
Returns:
str: The escaped string.
'''
if not string and not escape_empty_string:
return ''
if not needs_escape(string):
return string
if "'" not in string:
return "'%s'" % string
if '"' not in string:
return '"%s"' % string.replace('\\', '\\\\').replace('$', '\\$').replace('`', '\\`')
return "'%s'" % string.replace("'", '\'"\'"\'')
def make_completion_funcname(cmdline, prefix='_', suffix=''):
'''Generates a function name for auto-completing a program or subcommand.
Args:
cmdline (CommandLine): The CommandLine instance representing the program or subcommand.
prefix (str): The prefix that shall be prepended to the result.
suffix (str): The suffix that shall be appended to the result.
Returns:
str: The generated function name for auto-completion.
This function is used to generate a unique function name for auto-completing
a program or subcommand in the specified shell. It concatenates the names of
all parent commandlines, including the current commandline, and converts them
into a valid function name format.
Example:
For a program with the name 'my_program' and a subcommand with the name 'subcommand',
the generated function name is '_my_program_subcommand'.
'''
assert isinstance(cmdline, cli.CommandLine), \
"make_completion_funcname: cmdline: expected CommandLine, got %r" % cmdline
commandlines = cmdline.get_parents(include_self=True)
identifier = make_identifier('_'.join(p.prog for p in commandlines))
return f'{prefix}{identifier}{suffix}'
class ShellCompleter:
'''Base class for argument completion.'''
# pylint: disable=missing-function-docstring
def complete(self, ctxt, completion, *a):
if not hasattr(self, completion):
utils.warn(f"ShellCompleter: Falling back from `{completion}` to `none`")
completion = 'none'
return getattr(self, completion)(ctxt, *a)
def fallback(self, ctxt, from_, to, *a):
utils.warn(f"ShellCompleter: Falling back from `{from_}` to `{to}`")
return self.complete(ctxt, to, *a)
def signal(self, ctxt, prefix=''):
sig = prefix
signals = collections.OrderedDict([
(sig+'ABRT', 'Process abort signal'),
(sig+'ALRM', 'Alarm clock'),
(sig+'BUS', 'Access to an undefined portion of a memory object'),
(sig+'CHLD', 'Child process terminated, stopped, or continued'),
(sig+'CONT', 'Continue executing, if stopped'),
(sig+'FPE', 'Erroneous arithmetic operation'),
(sig+'HUP', 'Hangup'),
(sig+'ILL', 'Illegal instruction'),
(sig+'INT', 'Terminal interrupt signal'),
(sig+'KILL', 'Kill (cannot be caught or ignored)'),
(sig+'PIPE', 'Write on a pipe with no one to read it'),
(sig+'QUIT', 'Terminal quit signal'),
(sig+'SEGV', 'Invalid memory reference'),
(sig+'STOP', 'Stop executing (cannot be caught or ignored)'),
(sig+'TERM', 'Termination signal'),
(sig+'TSTP', 'Terminal stop signal'),
(sig+'TTIN', 'Background process attempting read'),
(sig+'TTOU', 'Background process attempting write'),
(sig+'USR1', 'User-defined signal 1'),
(sig+'USR2', 'User-defined signal 2'),
(sig+'POLL', 'Pollable event'),
(sig+'PROF', 'Profiling timer expired'),
(sig+'SYS', 'Bad system call'),
(sig+'TRAP', 'Trace/breakpoint trap'),
(sig+'XFSZ', 'File size limit exceeded'),
(sig+'VTALRM', 'Virtual timer expired'),
(sig+'XCPU', 'CPU time limit exceeded'),
])
return self.complete(ctxt, 'choices', signals)
def range(self, ctxt, start, stop, step=1):
return self.fallback(ctxt, 'range', 'choices', list(range(start, stop, step)))
def directory(self, ctxt, opts):
return self.fallback(ctxt, 'directory', 'file', opts)
def command(self, ctxt):
return self.fallback(ctxt, 'command', 'file')
# =========================================================================
# Bonus
# =========================================================================
def login_shell(self, ctxt):
return self.exec(ctxt, "command grep -E '^[^#]' /etc/shells")
def locale(self, ctxt):
return self.exec(ctxt, "command locale -a")
def charset(self, ctxt):
return self.exec(ctxt, "command locale -m")
def mountpoint(self, ctxt):
return self.exec(ctxt, "command mount | command cut -d' ' -f3")
|