370 строки
11 KiB
Python
370 строки
11 KiB
Python
# 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."""
|
|
|
|
import sys
|
|
|
|
|
|
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 sys.version_info.major < 3 and 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, str) and not isinstance(key, unicode):
|
|
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
|