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