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