| # Copyright 2014 The Chromium Authors. All rights reserved. | 
 | # Use of this source code is governed by a BSD-style license that can be | 
 | # found in the LICENSE file. | 
 |  | 
 | """Helper functions useful when writing scripts that integrate with GN. | 
 |  | 
 | The main functions are ToGNString and FromGNString which convert between | 
 | serialized GN veriables and Python variables. | 
 |  | 
 | To use in a random python file in the build: | 
 |  | 
 |   import os | 
 |   import sys | 
 |  | 
 |   sys.path.append(os.path.join(os.path.dirname(__file__), | 
 |                                os.pardir, os.pardir, "build")) | 
 |   import gn_helpers | 
 |  | 
 | Where the sequence of parameters to join is the relative path from your source | 
 | file to the build directory.""" | 
 |  | 
 | class GNException(Exception): | 
 |   pass | 
 |  | 
 |  | 
 | def ToGNString(value, allow_dicts = True): | 
 |   """Returns a stringified GN equivalent of the Python value. | 
 |  | 
 |   allow_dicts indicates if this function will allow converting dictionaries | 
 |   to GN scopes. This is only possible at the top level, you can't nest a | 
 |   GN scope in a list, so this should be set to False for recursive calls.""" | 
 |   if isinstance(value, basestring): | 
 |     if value.find('\n') >= 0: | 
 |       raise GNException("Trying to print a string with a newline in it.") | 
 |     return '"' + \ | 
 |         value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \ | 
 |         '"' | 
 |  | 
 |   if isinstance(value, unicode): | 
 |     return ToGNString(value.encode('utf-8')) | 
 |  | 
 |   if isinstance(value, bool): | 
 |     if value: | 
 |       return "true" | 
 |     return "false" | 
 |  | 
 |   if isinstance(value, list): | 
 |     return '[ %s ]' % ', '.join(ToGNString(v) for v in value) | 
 |  | 
 |   if isinstance(value, dict): | 
 |     if not allow_dicts: | 
 |       raise GNException("Attempting to recursively print a dictionary.") | 
 |     result = "" | 
 |     for key in sorted(value): | 
 |       if not isinstance(key, basestring): | 
 |         raise GNException("Dictionary key is not a string.") | 
 |       result += "%s = %s\n" % (key, ToGNString(value[key], False)) | 
 |     return result | 
 |  | 
 |   if isinstance(value, int): | 
 |     return str(value) | 
 |  | 
 |   raise GNException("Unsupported type when printing to GN.") | 
 |  | 
 |  | 
 | def FromGNString(input_string): | 
 |   """Converts the input string from a GN serialized value to Python values. | 
 |  | 
 |   For details on supported types see GNValueParser.Parse() below. | 
 |  | 
 |   If your GN script did: | 
 |     something = [ "file1", "file2" ] | 
 |     args = [ "--values=$something" ] | 
 |   The command line would look something like: | 
 |     --values="[ \"file1\", \"file2\" ]" | 
 |   Which when interpreted as a command line gives the value: | 
 |     [ "file1", "file2" ] | 
 |  | 
 |   You can parse this into a Python list using GN rules with: | 
 |     input_values = FromGNValues(options.values) | 
 |   Although the Python 'ast' module will parse many forms of such input, it | 
 |   will not handle GN escaping properly, nor GN booleans. You should use this | 
 |   function instead. | 
 |  | 
 |  | 
 |   A NOTE ON STRING HANDLING: | 
 |  | 
 |   If you just pass a string on the command line to your Python script, or use | 
 |   string interpolation on a string variable, the strings will not be quoted: | 
 |     str = "asdf" | 
 |     args = [ str, "--value=$str" ] | 
 |   Will yield the command line: | 
 |     asdf --value=asdf | 
 |   The unquoted asdf string will not be valid input to this function, which | 
 |   accepts only quoted strings like GN scripts. In such cases, you can just use | 
 |   the Python string literal directly. | 
 |  | 
 |   The main use cases for this is for other types, in particular lists. When | 
 |   using string interpolation on a list (as in the top example) the embedded | 
 |   strings will be quoted and escaped according to GN rules so the list can be | 
 |   re-parsed to get the same result.""" | 
 |   parser = GNValueParser(input_string) | 
 |   return parser.Parse() | 
 |  | 
 |  | 
 | def FromGNArgs(input_string): | 
 |   """Converts a string with a bunch of gn arg assignments into a Python dict. | 
 |  | 
 |   Given a whitespace-separated list of | 
 |  | 
 |     <ident> = (integer | string | boolean | <list of the former>) | 
 |  | 
 |   gn assignments, this returns a Python dict, i.e.: | 
 |  | 
 |     FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }. | 
 |  | 
 |   Only simple types and lists supported; variables, structs, calls | 
 |   and other, more complicated things are not. | 
 |  | 
 |   This routine is meant to handle only the simple sorts of values that | 
 |   arise in parsing --args. | 
 |   """ | 
 |   parser = GNValueParser(input_string) | 
 |   return parser.ParseArgs() | 
 |  | 
 |  | 
 | def UnescapeGNString(value): | 
 |   """Given a string with GN escaping, returns the unescaped string. | 
 |  | 
 |   Be careful not to feed with input from a Python parsing function like | 
 |   'ast' because it will do Python unescaping, which will be incorrect when | 
 |   fed into the GN unescaper.""" | 
 |   result = '' | 
 |   i = 0 | 
 |   while i < len(value): | 
 |     if value[i] == '\\': | 
 |       if i < len(value) - 1: | 
 |         next_char = value[i + 1] | 
 |         if next_char in ('$', '"', '\\'): | 
 |           # These are the escaped characters GN supports. | 
 |           result += next_char | 
 |           i += 1 | 
 |         else: | 
 |           # Any other backslash is a literal. | 
 |           result += '\\' | 
 |     else: | 
 |       result += value[i] | 
 |     i += 1 | 
 |   return result | 
 |  | 
 |  | 
 | def _IsDigitOrMinus(char): | 
 |   return char in "-0123456789" | 
 |  | 
 |  | 
 | class GNValueParser(object): | 
 |   """Duplicates GN parsing of values and converts to Python types. | 
 |  | 
 |   Normally you would use the wrapper function FromGNValue() below. | 
 |  | 
 |   If you expect input as a specific type, you can also call one of the Parse* | 
 |   functions directly. All functions throw GNException on invalid input. """ | 
 |   def __init__(self, string): | 
 |     self.input = string | 
 |     self.cur = 0 | 
 |  | 
 |   def IsDone(self): | 
 |     return self.cur == len(self.input) | 
 |  | 
 |   def ConsumeWhitespace(self): | 
 |     while not self.IsDone() and self.input[self.cur] in ' \t\n': | 
 |       self.cur += 1 | 
 |  | 
 |   def ConsumeComment(self): | 
 |     if self.IsDone() or self.input[self.cur] != '#': | 
 |       return | 
 |  | 
 |     # Consume each comment, line by line. | 
 |     while not self.IsDone() and self.input[self.cur] == '#': | 
 |       # Consume the rest of the comment, up until the end of the line. | 
 |       while not self.IsDone() and self.input[self.cur] != '\n': | 
 |         self.cur += 1 | 
 |       # Move the cursor to the next line (if there is one). | 
 |       if not self.IsDone(): | 
 |         self.cur += 1 | 
 |  | 
 |   def Parse(self): | 
 |     """Converts a string representing a printed GN value to the Python type. | 
 |  | 
 |     See additional usage notes on FromGNString above. | 
 |  | 
 |     - GN booleans ('true', 'false') will be converted to Python booleans. | 
 |  | 
 |     - GN numbers ('123') will be converted to Python numbers. | 
 |  | 
 |     - GN strings (double-quoted as in '"asdf"') will be converted to Python | 
 |       strings with GN escaping rules. GN string interpolation (embedded | 
 |       variables preceded by $) are not supported and will be returned as | 
 |       literals. | 
 |  | 
 |     - GN lists ('[1, "asdf", 3]') will be converted to Python lists. | 
 |  | 
 |     - GN scopes ('{ ... }') are not supported.""" | 
 |     result = self._ParseAllowTrailing() | 
 |     self.ConsumeWhitespace() | 
 |     if not self.IsDone(): | 
 |       raise GNException("Trailing input after parsing:\n  " + | 
 |                         self.input[self.cur:]) | 
 |     return result | 
 |  | 
 |   def ParseArgs(self): | 
 |     """Converts a whitespace-separated list of ident=literals to a dict. | 
 |  | 
 |     See additional usage notes on FromGNArgs, above. | 
 |     """ | 
 |     d = {} | 
 |  | 
 |     self.ConsumeWhitespace() | 
 |     self.ConsumeComment() | 
 |     while not self.IsDone(): | 
 |       ident = self._ParseIdent() | 
 |       self.ConsumeWhitespace() | 
 |       if self.input[self.cur] != '=': | 
 |         raise GNException("Unexpected token: " + self.input[self.cur:]) | 
 |       self.cur += 1 | 
 |       self.ConsumeWhitespace() | 
 |       val = self._ParseAllowTrailing() | 
 |       self.ConsumeWhitespace() | 
 |       self.ConsumeComment() | 
 |       d[ident] = val | 
 |  | 
 |     return d | 
 |  | 
 |   def _ParseAllowTrailing(self): | 
 |     """Internal version of Parse that doesn't check for trailing stuff.""" | 
 |     self.ConsumeWhitespace() | 
 |     if self.IsDone(): | 
 |       raise GNException("Expected input to parse.") | 
 |  | 
 |     next_char = self.input[self.cur] | 
 |     if next_char == '[': | 
 |       return self.ParseList() | 
 |     elif _IsDigitOrMinus(next_char): | 
 |       return self.ParseNumber() | 
 |     elif next_char == '"': | 
 |       return self.ParseString() | 
 |     elif self._ConstantFollows('true'): | 
 |       return True | 
 |     elif self._ConstantFollows('false'): | 
 |       return False | 
 |     else: | 
 |       raise GNException("Unexpected token: " + self.input[self.cur:]) | 
 |  | 
 |   def _ParseIdent(self): | 
 |     ident = '' | 
 |  | 
 |     next_char = self.input[self.cur] | 
 |     if not next_char.isalpha() and not next_char=='_': | 
 |       raise GNException("Expected an identifier: " + self.input[self.cur:]) | 
 |  | 
 |     ident += next_char | 
 |     self.cur += 1 | 
 |  | 
 |     next_char = self.input[self.cur] | 
 |     while next_char.isalpha() or next_char.isdigit() or next_char=='_': | 
 |       ident += next_char | 
 |       self.cur += 1 | 
 |       next_char = self.input[self.cur] | 
 |  | 
 |     return ident | 
 |  | 
 |   def ParseNumber(self): | 
 |     self.ConsumeWhitespace() | 
 |     if self.IsDone(): | 
 |       raise GNException('Expected number but got nothing.') | 
 |  | 
 |     begin = self.cur | 
 |  | 
 |     # The first character can include a negative sign. | 
 |     if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]): | 
 |       self.cur += 1 | 
 |     while not self.IsDone() and self.input[self.cur].isdigit(): | 
 |       self.cur += 1 | 
 |  | 
 |     number_string = self.input[begin:self.cur] | 
 |     if not len(number_string) or number_string == '-': | 
 |       raise GNException("Not a valid number.") | 
 |     return int(number_string) | 
 |  | 
 |   def ParseString(self): | 
 |     self.ConsumeWhitespace() | 
 |     if self.IsDone(): | 
 |       raise GNException('Expected string but got nothing.') | 
 |  | 
 |     if self.input[self.cur] != '"': | 
 |       raise GNException('Expected string beginning in a " but got:\n  ' + | 
 |                         self.input[self.cur:]) | 
 |     self.cur += 1  # Skip over quote. | 
 |  | 
 |     begin = self.cur | 
 |     while not self.IsDone() and self.input[self.cur] != '"': | 
 |       if self.input[self.cur] == '\\': | 
 |         self.cur += 1  # Skip over the backslash. | 
 |         if self.IsDone(): | 
 |           raise GNException("String ends in a backslash in:\n  " + | 
 |                             self.input) | 
 |       self.cur += 1 | 
 |  | 
 |     if self.IsDone(): | 
 |       raise GNException('Unterminated string:\n  ' + self.input[begin:]) | 
 |  | 
 |     end = self.cur | 
 |     self.cur += 1  # Consume trailing ". | 
 |  | 
 |     return UnescapeGNString(self.input[begin:end]) | 
 |  | 
 |   def ParseList(self): | 
 |     self.ConsumeWhitespace() | 
 |     if self.IsDone(): | 
 |       raise GNException('Expected list but got nothing.') | 
 |  | 
 |     # Skip over opening '['. | 
 |     if self.input[self.cur] != '[': | 
 |       raise GNException("Expected [ for list but got:\n  " + | 
 |                         self.input[self.cur:]) | 
 |     self.cur += 1 | 
 |     self.ConsumeWhitespace() | 
 |     if self.IsDone(): | 
 |       raise GNException("Unterminated list:\n  " + self.input) | 
 |  | 
 |     list_result = [] | 
 |     previous_had_trailing_comma = True | 
 |     while not self.IsDone(): | 
 |       if self.input[self.cur] == ']': | 
 |         self.cur += 1  # Skip over ']'. | 
 |         return list_result | 
 |  | 
 |       if not previous_had_trailing_comma: | 
 |         raise GNException("List items not separated by comma.") | 
 |  | 
 |       list_result += [ self._ParseAllowTrailing() ] | 
 |       self.ConsumeWhitespace() | 
 |       if self.IsDone(): | 
 |         break | 
 |  | 
 |       # Consume comma if there is one. | 
 |       previous_had_trailing_comma = self.input[self.cur] == ',' | 
 |       if previous_had_trailing_comma: | 
 |         # Consume comma. | 
 |         self.cur += 1 | 
 |         self.ConsumeWhitespace() | 
 |  | 
 |     raise GNException("Unterminated list:\n  " + self.input) | 
 |  | 
 |   def _ConstantFollows(self, constant): | 
 |     """Returns true if the given constant follows immediately at the current | 
 |     location in the input. If it does, the text is consumed and the function | 
 |     returns true. Otherwise, returns false and the current position is | 
 |     unchanged.""" | 
 |     end = self.cur + len(constant) | 
 |     if end > len(self.input): | 
 |       return False  # Not enough room. | 
 |     if self.input[self.cur:end] == constant: | 
 |       self.cur = end | 
 |       return True | 
 |     return False |