File: shell.py

package info (click to toggle)
crazy-complete 0.3.7-4
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 2,528 kB
  • sloc: python: 13,342; sh: 995; makefile: 68
file content (174 lines) | stat: -rw-r--r-- 5,705 bytes parent folder | download
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")