| Index: build/gn_helpers.py
|
| diff --git a/build/gn_helpers.py b/build/gn_helpers.py
|
| index 3b0647d9a5b7ea9ab60fa2c02fab63c3f39c85d2..07a7c3e913c1ebeaa9936df9f5fd7f53af62ffe9 100644
|
| --- a/build/gn_helpers.py
|
| +++ b/build/gn_helpers.py
|
| @@ -2,8 +2,10 @@
|
| # 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 are run from GN's
|
| -exec_script function."""
|
| +"""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."""
|
|
|
| class GNException(Exception):
|
| pass
|
| @@ -18,7 +20,14 @@ def ToGNString(value, allow_dicts = True):
|
| if isinstance(value, str):
|
| if value.find('\n') >= 0:
|
| raise GNException("Trying to print a string with a newline in it.")
|
| - return '"' + value.replace('"', '\\"') + '"'
|
| + return '"' + \
|
| + value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
|
| + '"'
|
| +
|
| + if isinstance(value, bool):
|
| + if value:
|
| + return "true"
|
| + return "false"
|
|
|
| if isinstance(value, list):
|
| return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
|
| @@ -37,3 +46,231 @@ def ToGNString(value, allow_dicts = True):
|
| return str(value)
|
|
|
| raise GNException("Unsupported type when printing to GN.")
|
| +
|
| +
|
| +def FromGNString(input):
|
| + """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)
|
| + return parser.Parse()
|
| +
|
| +
|
| +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 preceeded 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 _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 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
|
|
|