xref: /aosp_15_r20/tools/repohooks/rh/shell.py (revision d68f33bc6fb0cc2476107c2af0573a2f5a63dfc1)
1*d68f33bcSAndroid Build Coastguard Worker# Copyright 2016 The Android Open Source Project
2*d68f33bcSAndroid Build Coastguard Worker#
3*d68f33bcSAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*d68f33bcSAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*d68f33bcSAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*d68f33bcSAndroid Build Coastguard Worker#
7*d68f33bcSAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*d68f33bcSAndroid Build Coastguard Worker#
9*d68f33bcSAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*d68f33bcSAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*d68f33bcSAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*d68f33bcSAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*d68f33bcSAndroid Build Coastguard Worker# limitations under the License.
14*d68f33bcSAndroid Build Coastguard Worker
15*d68f33bcSAndroid Build Coastguard Worker"""Functions for working with shell code."""
16*d68f33bcSAndroid Build Coastguard Worker
17*d68f33bcSAndroid Build Coastguard Workerimport os
18*d68f33bcSAndroid Build Coastguard Workerimport pathlib
19*d68f33bcSAndroid Build Coastguard Workerimport sys
20*d68f33bcSAndroid Build Coastguard Worker
21*d68f33bcSAndroid Build Coastguard Worker_path = os.path.realpath(__file__ + '/../..')
22*d68f33bcSAndroid Build Coastguard Workerif sys.path[0] != _path:
23*d68f33bcSAndroid Build Coastguard Worker    sys.path.insert(0, _path)
24*d68f33bcSAndroid Build Coastguard Workerdel _path
25*d68f33bcSAndroid Build Coastguard Worker
26*d68f33bcSAndroid Build Coastguard Worker
27*d68f33bcSAndroid Build Coastguard Worker# For use by ShellQuote.  Match all characters that the shell might treat
28*d68f33bcSAndroid Build Coastguard Worker# specially.  This means a number of things:
29*d68f33bcSAndroid Build Coastguard Worker#  - Reserved characters.
30*d68f33bcSAndroid Build Coastguard Worker#  - Characters used in expansions (brace, variable, path, globs, etc...).
31*d68f33bcSAndroid Build Coastguard Worker#  - Characters that an interactive shell might use (like !).
32*d68f33bcSAndroid Build Coastguard Worker#  - Whitespace so that one arg turns into multiple.
33*d68f33bcSAndroid Build Coastguard Worker# See the bash man page as well as the POSIX shell documentation for more info:
34*d68f33bcSAndroid Build Coastguard Worker#   http://www.gnu.org/software/bash/manual/bashref.html
35*d68f33bcSAndroid Build Coastguard Worker#   http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html
36*d68f33bcSAndroid Build Coastguard Worker_SHELL_QUOTABLE_CHARS = frozenset('[|&;()<> \t!{}[]=*?~$"\'\\#^')
37*d68f33bcSAndroid Build Coastguard Worker# The chars that, when used inside of double quotes, need escaping.
38*d68f33bcSAndroid Build Coastguard Worker# Order here matters as we need to escape backslashes first.
39*d68f33bcSAndroid Build Coastguard Worker_SHELL_ESCAPE_CHARS = r'\"`$'
40*d68f33bcSAndroid Build Coastguard Worker
41*d68f33bcSAndroid Build Coastguard Worker
42*d68f33bcSAndroid Build Coastguard Workerdef quote(s):
43*d68f33bcSAndroid Build Coastguard Worker    """Quote |s| in a way that is safe for use in a shell.
44*d68f33bcSAndroid Build Coastguard Worker
45*d68f33bcSAndroid Build Coastguard Worker    We aim to be safe, but also to produce "nice" output.  That means we don't
46*d68f33bcSAndroid Build Coastguard Worker    use quotes when we don't need to, and we prefer to use less quotes (like
47*d68f33bcSAndroid Build Coastguard Worker    putting it all in single quotes) than more (using double quotes and escaping
48*d68f33bcSAndroid Build Coastguard Worker    a bunch of stuff, or mixing the quotes).
49*d68f33bcSAndroid Build Coastguard Worker
50*d68f33bcSAndroid Build Coastguard Worker    While python does provide a number of alternatives like:
51*d68f33bcSAndroid Build Coastguard Worker     - pipes.quote
52*d68f33bcSAndroid Build Coastguard Worker     - shlex.quote
53*d68f33bcSAndroid Build Coastguard Worker    They suffer from various problems like:
54*d68f33bcSAndroid Build Coastguard Worker     - Not widely available in different python versions.
55*d68f33bcSAndroid Build Coastguard Worker     - Do not produce pretty output in many cases.
56*d68f33bcSAndroid Build Coastguard Worker     - Are in modules that rarely otherwise get used.
57*d68f33bcSAndroid Build Coastguard Worker
58*d68f33bcSAndroid Build Coastguard Worker    Note: We don't handle reserved shell words like "for" or "case".  This is
59*d68f33bcSAndroid Build Coastguard Worker    because those only matter when they're the first element in a command, and
60*d68f33bcSAndroid Build Coastguard Worker    there is no use case for that.  When we want to run commands, we tend to
61*d68f33bcSAndroid Build Coastguard Worker    run real programs and not shell ones.
62*d68f33bcSAndroid Build Coastguard Worker
63*d68f33bcSAndroid Build Coastguard Worker    Args:
64*d68f33bcSAndroid Build Coastguard Worker      s: The string to quote.
65*d68f33bcSAndroid Build Coastguard Worker
66*d68f33bcSAndroid Build Coastguard Worker    Returns:
67*d68f33bcSAndroid Build Coastguard Worker      A safely (possibly quoted) string.
68*d68f33bcSAndroid Build Coastguard Worker    """
69*d68f33bcSAndroid Build Coastguard Worker    # If callers pass down bad types, don't blow up.
70*d68f33bcSAndroid Build Coastguard Worker    if isinstance(s, bytes):
71*d68f33bcSAndroid Build Coastguard Worker        s = s.encode('utf-8')
72*d68f33bcSAndroid Build Coastguard Worker    elif isinstance(s, pathlib.PurePath):
73*d68f33bcSAndroid Build Coastguard Worker        return str(s)
74*d68f33bcSAndroid Build Coastguard Worker    elif not isinstance(s, str):
75*d68f33bcSAndroid Build Coastguard Worker        return repr(s)
76*d68f33bcSAndroid Build Coastguard Worker
77*d68f33bcSAndroid Build Coastguard Worker    # See if no quoting is needed so we can return the string as-is.
78*d68f33bcSAndroid Build Coastguard Worker    for c in s:
79*d68f33bcSAndroid Build Coastguard Worker        if c in _SHELL_QUOTABLE_CHARS:
80*d68f33bcSAndroid Build Coastguard Worker            break
81*d68f33bcSAndroid Build Coastguard Worker    else:
82*d68f33bcSAndroid Build Coastguard Worker        return s if s else "''"
83*d68f33bcSAndroid Build Coastguard Worker
84*d68f33bcSAndroid Build Coastguard Worker    # See if we can use single quotes first.  Output is nicer.
85*d68f33bcSAndroid Build Coastguard Worker    if "'" not in s:
86*d68f33bcSAndroid Build Coastguard Worker        return f"'{s}'"
87*d68f33bcSAndroid Build Coastguard Worker
88*d68f33bcSAndroid Build Coastguard Worker    # Have to use double quotes.  Escape the few chars that still expand when
89*d68f33bcSAndroid Build Coastguard Worker    # used inside of double quotes.
90*d68f33bcSAndroid Build Coastguard Worker    for c in _SHELL_ESCAPE_CHARS:
91*d68f33bcSAndroid Build Coastguard Worker        if c in s:
92*d68f33bcSAndroid Build Coastguard Worker            s = s.replace(c, fr'\{c}')
93*d68f33bcSAndroid Build Coastguard Worker    return f'"{s}"'
94*d68f33bcSAndroid Build Coastguard Worker
95*d68f33bcSAndroid Build Coastguard Worker
96*d68f33bcSAndroid Build Coastguard Workerdef unquote(s):
97*d68f33bcSAndroid Build Coastguard Worker    """Do the opposite of ShellQuote.
98*d68f33bcSAndroid Build Coastguard Worker
99*d68f33bcSAndroid Build Coastguard Worker    This function assumes that the input is a valid escaped string.
100*d68f33bcSAndroid Build Coastguard Worker    The behaviour is undefined on malformed strings.
101*d68f33bcSAndroid Build Coastguard Worker
102*d68f33bcSAndroid Build Coastguard Worker    Args:
103*d68f33bcSAndroid Build Coastguard Worker      s: An escaped string.
104*d68f33bcSAndroid Build Coastguard Worker
105*d68f33bcSAndroid Build Coastguard Worker    Returns:
106*d68f33bcSAndroid Build Coastguard Worker      The unescaped version of the string.
107*d68f33bcSAndroid Build Coastguard Worker    """
108*d68f33bcSAndroid Build Coastguard Worker    if not s:
109*d68f33bcSAndroid Build Coastguard Worker        return ''
110*d68f33bcSAndroid Build Coastguard Worker
111*d68f33bcSAndroid Build Coastguard Worker    if s[0] == "'":
112*d68f33bcSAndroid Build Coastguard Worker        return s[1:-1]
113*d68f33bcSAndroid Build Coastguard Worker
114*d68f33bcSAndroid Build Coastguard Worker    if s[0] != '"':
115*d68f33bcSAndroid Build Coastguard Worker        return s
116*d68f33bcSAndroid Build Coastguard Worker
117*d68f33bcSAndroid Build Coastguard Worker    s = s[1:-1]
118*d68f33bcSAndroid Build Coastguard Worker    output = ''
119*d68f33bcSAndroid Build Coastguard Worker    i = 0
120*d68f33bcSAndroid Build Coastguard Worker    while i < len(s) - 1:
121*d68f33bcSAndroid Build Coastguard Worker        # Skip the backslash when it makes sense.
122*d68f33bcSAndroid Build Coastguard Worker        if s[i] == '\\' and s[i + 1] in _SHELL_ESCAPE_CHARS:
123*d68f33bcSAndroid Build Coastguard Worker            i += 1
124*d68f33bcSAndroid Build Coastguard Worker        output += s[i]
125*d68f33bcSAndroid Build Coastguard Worker        i += 1
126*d68f33bcSAndroid Build Coastguard Worker    return output + s[i] if i < len(s) else output
127*d68f33bcSAndroid Build Coastguard Worker
128*d68f33bcSAndroid Build Coastguard Worker
129*d68f33bcSAndroid Build Coastguard Workerdef cmd_to_str(cmd):
130*d68f33bcSAndroid Build Coastguard Worker    """Translate a command list into a space-separated string.
131*d68f33bcSAndroid Build Coastguard Worker
132*d68f33bcSAndroid Build Coastguard Worker    The resulting string should be suitable for logging messages and for
133*d68f33bcSAndroid Build Coastguard Worker    pasting into a terminal to run.  Command arguments are surrounded by
134*d68f33bcSAndroid Build Coastguard Worker    quotes to keep them grouped, even if an argument has spaces in it.
135*d68f33bcSAndroid Build Coastguard Worker
136*d68f33bcSAndroid Build Coastguard Worker    Examples:
137*d68f33bcSAndroid Build Coastguard Worker      ['a', 'b'] ==> "'a' 'b'"
138*d68f33bcSAndroid Build Coastguard Worker      ['a b', 'c'] ==> "'a b' 'c'"
139*d68f33bcSAndroid Build Coastguard Worker      ['a', 'b\'c'] ==> '\'a\' "b\'c"'
140*d68f33bcSAndroid Build Coastguard Worker      [u'a', "/'$b"] ==> '\'a\' "/\'$b"'
141*d68f33bcSAndroid Build Coastguard Worker      [] ==> ''
142*d68f33bcSAndroid Build Coastguard Worker      See unittest for additional (tested) examples.
143*d68f33bcSAndroid Build Coastguard Worker
144*d68f33bcSAndroid Build Coastguard Worker    Args:
145*d68f33bcSAndroid Build Coastguard Worker      cmd: List of command arguments.
146*d68f33bcSAndroid Build Coastguard Worker
147*d68f33bcSAndroid Build Coastguard Worker    Returns:
148*d68f33bcSAndroid Build Coastguard Worker      String representing full command.
149*d68f33bcSAndroid Build Coastguard Worker    """
150*d68f33bcSAndroid Build Coastguard Worker    # Use str before repr to translate unicode strings to regular strings.
151*d68f33bcSAndroid Build Coastguard Worker    return ' '.join(quote(arg) for arg in cmd)
152*d68f33bcSAndroid Build Coastguard Worker
153*d68f33bcSAndroid Build Coastguard Worker
154*d68f33bcSAndroid Build Coastguard Workerdef boolean_shell_value(sval, default):
155*d68f33bcSAndroid Build Coastguard Worker    """See if |sval| is a value users typically consider as boolean."""
156*d68f33bcSAndroid Build Coastguard Worker    if sval is None:
157*d68f33bcSAndroid Build Coastguard Worker        return default
158*d68f33bcSAndroid Build Coastguard Worker
159*d68f33bcSAndroid Build Coastguard Worker    if isinstance(sval, str):
160*d68f33bcSAndroid Build Coastguard Worker        s = sval.lower()
161*d68f33bcSAndroid Build Coastguard Worker        if s in ('yes', 'y', '1', 'true'):
162*d68f33bcSAndroid Build Coastguard Worker            return True
163*d68f33bcSAndroid Build Coastguard Worker        if s in ('no', 'n', '0', 'false'):
164*d68f33bcSAndroid Build Coastguard Worker            return False
165*d68f33bcSAndroid Build Coastguard Worker
166*d68f33bcSAndroid Build Coastguard Worker    raise ValueError(f'Could not decode as a boolean value: {sval!r}')
167