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 168 169 170 171 172 173 174
|
# SPDX-License-Identifier: GPL-3.0-or-later
# Copyright (C) 2025-2026 Benjamin Abendroth <braph93@gmx.de>
'''Shell utility functions.'''
import re
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_quote(string):
'''Return if string needs quoting.'''
return not re.fullmatch('[a-zA-Z0-9_@%+=:,./-]+', string)
def quote(string, quote_empty_string=True):
'''Quotes a string for safe usage in shell commands or scripts.
Args:
string (str):
The input string to be quoted.
quote_empty_string (bool, optional):
Determines whether to quote an empty string or not.
Defaults to True.
Returns:
str: The quoted string.
'''
if not string and not quote_empty_string:
return ''
if not needs_quote(string):
return string
if "'" not in string:
return f"'{string}'"
if '"' not in string:
s = string
s = s.replace('\\', '\\\\')
s = s.replace('$', '\\$')
s = s.replace('`', '\\`')
return f'"{s}"'
s = string.replace("'", '\'"\'"\'')
return f"'{s}'"
def join_quoted(arguments, string=' '):
'''Joins quoted `arguments` on `string`.'''
return string.join(quote(arg) for arg in arguments)
class ShellCompleter:
'''Base class for argument completion.'''
# pylint: disable=missing-function-docstring
# pylint: disable=too-many-arguments
# pylint: disable=too-many-positional-arguments
def complete(self, ctxt, trace, command, *a):
if not hasattr(self, command):
utils.warn(f"complete: Falling back from `{command}` to `none`")
command = 'none'
return getattr(self, command)(ctxt, trace, *a)
def complete_from_def(self, ctxt, trace, definition):
command, *args = definition
return self.complete(ctxt, trace, command, *args)
def fallback(self, ctxt, trace, from_, to, *a):
utils.warn(f"ShellCompleter: Falling back from `{from_}` to `{to}`")
return self.complete(ctxt, trace, to, *a)
def signal(self, ctxt, trace):
signals = {
'ABRT': 'Process abort signal',
'ALRM': 'Alarm clock',
'BUS': 'Access to an undefined portion of a memory object',
'CHLD': 'Child process terminated: stopped: or continued',
'CONT': 'Continue executing: if stopped',
'FPE': 'Erroneous arithmetic operation',
'HUP': 'Hangup',
'ILL': 'Illegal instruction',
'INT': 'Terminal interrupt signal',
'KILL': 'Kill (cannot be caught or ignored)',
'PIPE': 'Write on a pipe with no one to read it',
'QUIT': 'Terminal quit signal',
'SEGV': 'Invalid memory reference',
'STOP': 'Stop executing (cannot be caught or ignored)',
'TERM': 'Termination signal',
'TSTP': 'Terminal stop signal',
'TTIN': 'Background process attempting read',
'TTOU': 'Background process attempting write',
'USR1': 'User-defined signal 1',
'USR2': 'User-defined signal 2',
'POLL': 'Pollable event',
'PROF': 'Profiling timer expired',
'SYS': 'Bad system call',
'TRAP': 'Trace/breakpoint trap',
'XFSZ': 'File size limit exceeded',
'VTALRM': 'Virtual timer expired',
'XCPU': 'CPU time limit exceeded',
}
return self.complete(ctxt, trace, 'choices', signals)
def exec(self, _ctxt, _trace, _command):
raise NotImplementedError
def list(self, _ctxt, _trace, _command, _opts=None):
raise NotImplementedError
# =========================================================================
# Aliases
# =========================================================================
def file_list(self, ctxt, trace, opts=None):
list_opts = {
'separator': opts.pop('separator', ',') if opts else ',',
'duplicates': opts.pop('duplicates', False) if opts else False
}
return self.list(ctxt, trace, ['file', opts], list_opts)
def directory_list(self, ctxt, trace, opts=None):
list_opts = {
'separator': opts.pop('separator', ',') if opts else ',',
'duplicates': opts.pop('duplicates', False) if opts else False
}
return self.list(ctxt, trace, ['directory', opts], list_opts)
# =========================================================================
# Bonus
# =========================================================================
def login_shell(self, ctxt, trace):
return self.exec(ctxt, trace, "command grep -E '^[^#]' /etc/shells")
def locale(self, ctxt, trace):
return self.exec(ctxt, trace, "command locale -a")
def charset(self, ctxt, trace):
return self.exec(ctxt, trace, "command locale -m")
|