360 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
			
		
		
	
	
			360 lines
		
	
	
		
			12 KiB
		
	
	
	
		
			Python
		
	
	
		
			Executable File
		
	
	
	
	
| #!/usr/bin/env python
 | |
| # -*- coding: utf-8 -*-
 | |
| # 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 variables 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, str):
 | |
|         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, str):
 | |
|         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, str):
 | |
|                 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 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()
 | |
|         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()
 | |
|             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
 |