Home | History | Annotate | Download | only in rh
      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 
     18 from __future__ import print_function
     19 
     20 import os
     21 import sys
     22 
     23 _path = os.path.realpath(__file__ + '/../..')
     24 if sys.path[0] != _path:
     25     sys.path.insert(0, _path)
     26 del _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 
     44 def 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 
     95 def 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 
    128 def 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 
    153 def 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