Home | History | Annotate | Download | only in gn
      1 # Copyright 2014 The Chromium Authors. All rights reserved.
      2 # Use of this source code is governed by a BSD-style license that can be
      3 # found in the LICENSE file.
      4 
      5 """Helper functions useful when writing scripts that integrate with GN.
      6 
      7 The main functions are ToGNString and FromGNString which convert between
      8 serialized GN veriables and Python variables.
      9 
     10 To use in a random python file in the build:
     11 
     12   import os
     13   import sys
     14 
     15   sys.path.append(os.path.join(os.path.dirname(__file__),
     16                                os.pardir, os.pardir, "build"))
     17   import gn_helpers
     18 
     19 Where the sequence of parameters to join is the relative path from your source
     20 file to the build directory."""
     21 
     22 class GNException(Exception):
     23   pass
     24 
     25 
     26 def ToGNString(value, allow_dicts = True):
     27   """Returns a stringified GN equivalent of the Python value.
     28 
     29   allow_dicts indicates if this function will allow converting dictionaries
     30   to GN scopes. This is only possible at the top level, you can't nest a
     31   GN scope in a list, so this should be set to False for recursive calls."""
     32   if isinstance(value, basestring):
     33     if value.find('\n') >= 0:
     34       raise GNException("Trying to print a string with a newline in it.")
     35     return '"' + \
     36         value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
     37         '"'
     38 
     39   if isinstance(value, unicode):
     40     return ToGNString(value.encode('utf-8'))
     41 
     42   if isinstance(value, bool):
     43     if value:
     44       return "true"
     45     return "false"
     46 
     47   if isinstance(value, list):
     48     return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
     49 
     50   if isinstance(value, dict):
     51     if not allow_dicts:
     52       raise GNException("Attempting to recursively print a dictionary.")
     53     result = ""
     54     for key in sorted(value):
     55       if not isinstance(key, basestring):
     56         raise GNException("Dictionary key is not a string.")
     57       result += "%s = %s\n" % (key, ToGNString(value[key], False))
     58     return result
     59 
     60   if isinstance(value, int):
     61     return str(value)
     62 
     63   raise GNException("Unsupported type when printing to GN.")
     64 
     65 
     66 def FromGNString(input_):
     67   """Converts the input string from a GN serialized value to Python values.
     68 
     69   For details on supported types see GNValueParser.Parse() below.
     70 
     71   If your GN script did:
     72     something = [ "file1", "file2" ]
     73     args = [ "--values=$something" ]
     74   The command line would look something like:
     75     --values="[ \"file1\", \"file2\" ]"
     76   Which when interpreted as a command line gives the value:
     77     [ "file1", "file2" ]
     78 
     79   You can parse this into a Python list using GN rules with:
     80     input_values = FromGNValues(options.values)
     81   Although the Python 'ast' module will parse many forms of such input, it
     82   will not handle GN escaping properly, nor GN booleans. You should use this
     83   function instead.
     84 
     85 
     86   A NOTE ON STRING HANDLING:
     87 
     88   If you just pass a string on the command line to your Python script, or use
     89   string interpolation on a string variable, the strings will not be quoted:
     90     str = "asdf"
     91     args = [ str, "--value=$str" ]
     92   Will yield the command line:
     93     asdf --value=asdf
     94   The unquoted asdf string will not be valid input to this function, which
     95   accepts only quoted strings like GN scripts. In such cases, you can just use
     96   the Python string literal directly.
     97 
     98   The main use cases for this is for other types, in particular lists. When
     99   using string interpolation on a list (as in the top example) the embedded
    100   strings will be quoted and escaped according to GN rules so the list can be
    101   re-parsed to get the same result."""
    102   parser = GNValueParser(input_)
    103   return parser.Parse()
    104 
    105 
    106 def FromGNArgs(input_):
    107   """Converts a string with a bunch of gn arg assignments into a Python dict.
    108 
    109   Given a whitespace-separated list of
    110 
    111     <ident> = (integer | string | boolean | <list of the former>)
    112 
    113   gn assignments, this returns a Python dict, i.e.:
    114 
    115     FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
    116 
    117   Only simple types and lists supported; variables, structs, calls
    118   and other, more complicated things are not.
    119 
    120   This routine is meant to handle only the simple sorts of values that
    121   arise in parsing --args.
    122   """
    123   parser = GNValueParser(input_)
    124   return parser.ParseArgs()
    125 
    126 
    127 def UnescapeGNString(value):
    128   """Given a string with GN escaping, returns the unescaped string.
    129 
    130   Be careful not to feed with input from a Python parsing function like
    131   'ast' because it will do Python unescaping, which will be incorrect when
    132   fed into the GN unescaper."""
    133   result = ''
    134   i = 0
    135   while i < len(value):
    136     if value[i] == '\\':
    137       if i < len(value) - 1:
    138         next_char = value[i + 1]
    139         if next_char in ('$', '"', '\\'):
    140           # These are the escaped characters GN supports.
    141           result += next_char
    142           i += 1
    143         else:
    144           # Any other backslash is a literal.
    145           result += '\\'
    146     else:
    147       result += value[i]
    148     i += 1
    149   return result
    150 
    151 
    152 def _IsDigitOrMinus(char):
    153   return char in "-0123456789"
    154 
    155 
    156 class GNValueParser(object):
    157   """Duplicates GN parsing of values and converts to Python types.
    158 
    159   Normally you would use the wrapper function FromGNValue() below.
    160 
    161   If you expect input as a specific type, you can also call one of the Parse*
    162   functions directly. All functions throw GNException on invalid input. """
    163   def __init__(self, string):
    164     self.input = string
    165     self.cur = 0
    166 
    167   def IsDone(self):
    168     return self.cur == len(self.input)
    169 
    170   def ConsumeWhitespace(self):
    171     while not self.IsDone() and self.input[self.cur] in ' \t\n':
    172       self.cur += 1
    173 
    174   def Parse(self):
    175     """Converts a string representing a printed GN value to the Python type.
    176 
    177     See additional usage notes on FromGNString above.
    178 
    179     - GN booleans ('true', 'false') will be converted to Python booleans.
    180 
    181     - GN numbers ('123') will be converted to Python numbers.
    182 
    183     - GN strings (double-quoted as in '"asdf"') will be converted to Python
    184       strings with GN escaping rules. GN string interpolation (embedded
    185       variables preceeded by $) are not supported and will be returned as
    186       literals.
    187 
    188     - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
    189 
    190     - GN scopes ('{ ... }') are not supported."""
    191     result = self._ParseAllowTrailing()
    192     self.ConsumeWhitespace()
    193     if not self.IsDone():
    194       raise GNException("Trailing input after parsing:\n  " +
    195                         self.input[self.cur:])
    196     return result
    197 
    198   def ParseArgs(self):
    199     """Converts a whitespace-separated list of ident=literals to a dict.
    200 
    201     See additional usage notes on FromGNArgs, above.
    202     """
    203     d = {}
    204 
    205     self.ConsumeWhitespace()
    206     while not self.IsDone():
    207       ident = self._ParseIdent()
    208       self.ConsumeWhitespace()
    209       if self.input[self.cur] != '=':
    210         raise GNException("Unexpected token: " + self.input[self.cur:])
    211       self.cur += 1
    212       self.ConsumeWhitespace()
    213       val = self._ParseAllowTrailing()
    214       self.ConsumeWhitespace()
    215       d[ident] = val
    216 
    217     return d
    218 
    219   def _ParseAllowTrailing(self):
    220     """Internal version of Parse that doesn't check for trailing stuff."""
    221     self.ConsumeWhitespace()
    222     if self.IsDone():
    223       raise GNException("Expected input to parse.")
    224 
    225     next_char = self.input[self.cur]
    226     if next_char == '[':
    227       return self.ParseList()
    228     elif _IsDigitOrMinus(next_char):
    229       return self.ParseNumber()
    230     elif next_char == '"':
    231       return self.ParseString()
    232     elif self._ConstantFollows('true'):
    233       return True
    234     elif self._ConstantFollows('false'):
    235       return False
    236     else:
    237       raise GNException("Unexpected token: " + self.input[self.cur:])
    238 
    239   def _ParseIdent(self):
    240     id_ = ''
    241 
    242     next_char = self.input[self.cur]
    243     if not next_char.isalpha() and not next_char=='_':
    244       raise GNException("Expected an identifier: " + self.input[self.cur:])
    245 
    246     id_ += next_char
    247     self.cur += 1
    248 
    249     next_char = self.input[self.cur]
    250     while next_char.isalpha() or next_char.isdigit() or next_char=='_':
    251       id_ += next_char
    252       self.cur += 1
    253       next_char = self.input[self.cur]
    254 
    255     return id_
    256 
    257   def ParseNumber(self):
    258     self.ConsumeWhitespace()
    259     if self.IsDone():
    260       raise GNException('Expected number but got nothing.')
    261 
    262     begin = self.cur
    263 
    264     # The first character can include a negative sign.
    265     if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
    266       self.cur += 1
    267     while not self.IsDone() and self.input[self.cur].isdigit():
    268       self.cur += 1
    269 
    270     number_string = self.input[begin:self.cur]
    271     if not len(number_string) or number_string == '-':
    272       raise GNException("Not a valid number.")
    273     return int(number_string)
    274 
    275   def ParseString(self):
    276     self.ConsumeWhitespace()
    277     if self.IsDone():
    278       raise GNException('Expected string but got nothing.')
    279 
    280     if self.input[self.cur] != '"':
    281       raise GNException('Expected string beginning in a " but got:\n  ' +
    282                         self.input[self.cur:])
    283     self.cur += 1  # Skip over quote.
    284 
    285     begin = self.cur
    286     while not self.IsDone() and self.input[self.cur] != '"':
    287       if self.input[self.cur] == '\\':
    288         self.cur += 1  # Skip over the backslash.
    289         if self.IsDone():
    290           raise GNException("String ends in a backslash in:\n  " +
    291                             self.input)
    292       self.cur += 1
    293 
    294     if self.IsDone():
    295       raise GNException('Unterminated string:\n  ' + self.input[begin:])
    296 
    297     end = self.cur
    298     self.cur += 1  # Consume trailing ".
    299 
    300     return UnescapeGNString(self.input[begin:end])
    301 
    302   def ParseList(self):
    303     self.ConsumeWhitespace()
    304     if self.IsDone():
    305       raise GNException('Expected list but got nothing.')
    306 
    307     # Skip over opening '['.
    308     if self.input[self.cur] != '[':
    309       raise GNException("Expected [ for list but got:\n  " +
    310                         self.input[self.cur:])
    311     self.cur += 1
    312     self.ConsumeWhitespace()
    313     if self.IsDone():
    314       raise GNException("Unterminated list:\n  " + self.input)
    315 
    316     list_result = []
    317     previous_had_trailing_comma = True
    318     while not self.IsDone():
    319       if self.input[self.cur] == ']':
    320         self.cur += 1  # Skip over ']'.
    321         return list_result
    322 
    323       if not previous_had_trailing_comma:
    324         raise GNException("List items not separated by comma.")
    325 
    326       list_result += [ self._ParseAllowTrailing() ]
    327       self.ConsumeWhitespace()
    328       if self.IsDone():
    329         break
    330 
    331       # Consume comma if there is one.
    332       previous_had_trailing_comma = self.input[self.cur] == ','
    333       if previous_had_trailing_comma:
    334         # Consume comma.
    335         self.cur += 1
    336         self.ConsumeWhitespace()
    337 
    338     raise GNException("Unterminated list:\n  " + self.input)
    339 
    340   def _ConstantFollows(self, constant):
    341     """Returns true if the given constant follows immediately at the current
    342     location in the input. If it does, the text is consumed and the function
    343     returns true. Otherwise, returns false and the current position is
    344     unchanged."""
    345     end = self.cur + len(constant)
    346     if end > len(self.input):
    347       return False  # Not enough room.
    348     if self.input[self.cur:end] == constant:
    349       self.cur = end
    350       return True
    351     return False
    352