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.
5 """Helper functions useful when writing scripts that integrate with GN.
7 The main functions are ToGNString and FromGNString which convert between
8 serialized GN veriables and Python variables.
10 To use in a random python file in the build:
15 sys.path.append(os.path.join(os.path.dirname(__file__),
16 os.pardir, os.pardir, "build"))
19 Where the sequence of parameters to join is the relative path from your source
20 file to the build directory."""
22 class GNException(Exception):
26 def ToGNString(value, allow_dicts = True):
27 """Returns a stringified GN equivalent of the Python value.
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.")
36 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
39 if isinstance(value, unicode):
40 return ToGNString(value.encode('utf-8'))
42 if isinstance(value, bool):
47 if isinstance(value, list):
48 return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
50 if isinstance(value, dict):
52 raise GNException("Attempting to recursively print a dictionary.")
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))
60 if isinstance(value, int):
63 raise GNException("Unsupported type when printing to GN.")
66 def FromGNString(input_string):
67 """Converts the input string from a GN serialized value to Python values.
69 For details on supported types see GNValueParser.Parse() below.
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:
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
86 A NOTE ON STRING HANDLING:
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:
91 args = [ str, "--value=$str" ]
92 Will yield the command line:
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.
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_string)
103 return parser.Parse()
106 def FromGNArgs(input_string):
107 """Converts a string with a bunch of gn arg assignments into a Python dict.
109 Given a whitespace-separated list of
111 <ident> = (integer | string | boolean | <list of the former>)
113 gn assignments, this returns a Python dict, i.e.:
115 FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
117 Only simple types and lists supported; variables, structs, calls
118 and other, more complicated things are not.
120 This routine is meant to handle only the simple sorts of values that
121 arise in parsing --args.
123 parser = GNValueParser(input_string)
124 return parser.ParseArgs()
127 def UnescapeGNString(value):
128 """Given a string with GN escaping, returns the unescaped string.
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."""
135 while i < len(value):
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.
144 # Any other backslash is a literal.
152 def _IsDigitOrMinus(char):
153 return char in "-0123456789"
156 class GNValueParser(object):
157 """Duplicates GN parsing of values and converts to Python types.
159 Normally you would use the wrapper function FromGNValue() below.
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):
168 return self.cur == len(self.input)
170 def ConsumeWhitespace(self):
171 while not self.IsDone() and self.input[self.cur] in ' \t\n':
174 def ConsumeComment(self):
175 if self.IsDone() or self.input[self.cur] != '#':
178 # Consume each comment, line by line.
179 while not self.IsDone() and self.input[self.cur] == '#':
180 # Consume the rest of the comment, up until the end of the line.
181 while not self.IsDone() and self.input[self.cur] != '\n':
183 # Move the cursor to the next line (if there is one).
184 if not self.IsDone():
188 """Converts a string representing a printed GN value to the Python type.
190 See additional usage notes on FromGNString above.
192 - GN booleans ('true', 'false') will be converted to Python booleans.
194 - GN numbers ('123') will be converted to Python numbers.
196 - GN strings (double-quoted as in '"asdf"') will be converted to Python
197 strings with GN escaping rules. GN string interpolation (embedded
198 variables preceded by $) are not supported and will be returned as
201 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
203 - GN scopes ('{ ... }') are not supported."""
204 result = self._ParseAllowTrailing()
205 self.ConsumeWhitespace()
206 if not self.IsDone():
207 raise GNException("Trailing input after parsing:\n " +
208 self.input[self.cur:])
212 """Converts a whitespace-separated list of ident=literals to a dict.
214 See additional usage notes on FromGNArgs, above.
218 self.ConsumeWhitespace()
219 self.ConsumeComment()
220 while not self.IsDone():
221 ident = self._ParseIdent()
222 self.ConsumeWhitespace()
223 if self.input[self.cur] != '=':
224 raise GNException("Unexpected token: " + self.input[self.cur:])
226 self.ConsumeWhitespace()
227 val = self._ParseAllowTrailing()
228 self.ConsumeWhitespace()
229 self.ConsumeComment()
234 def _ParseAllowTrailing(self):
235 """Internal version of Parse that doesn't check for trailing stuff."""
236 self.ConsumeWhitespace()
238 raise GNException("Expected input to parse.")
240 next_char = self.input[self.cur]
242 return self.ParseList()
243 elif _IsDigitOrMinus(next_char):
244 return self.ParseNumber()
245 elif next_char == '"':
246 return self.ParseString()
247 elif self._ConstantFollows('true'):
249 elif self._ConstantFollows('false'):
252 raise GNException("Unexpected token: " + self.input[self.cur:])
254 def _ParseIdent(self):
257 next_char = self.input[self.cur]
258 if not next_char.isalpha() and not next_char=='_':
259 raise GNException("Expected an identifier: " + self.input[self.cur:])
264 next_char = self.input[self.cur]
265 while next_char.isalpha() or next_char.isdigit() or next_char=='_':
268 next_char = self.input[self.cur]
272 def ParseNumber(self):
273 self.ConsumeWhitespace()
275 raise GNException('Expected number but got nothing.')
279 # The first character can include a negative sign.
280 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
282 while not self.IsDone() and self.input[self.cur].isdigit():
285 number_string = self.input[begin:self.cur]
286 if not len(number_string) or number_string == '-':
287 raise GNException("Not a valid number.")
288 return int(number_string)
290 def ParseString(self):
291 self.ConsumeWhitespace()
293 raise GNException('Expected string but got nothing.')
295 if self.input[self.cur] != '"':
296 raise GNException('Expected string beginning in a " but got:\n ' +
297 self.input[self.cur:])
298 self.cur += 1 # Skip over quote.
301 while not self.IsDone() and self.input[self.cur] != '"':
302 if self.input[self.cur] == '\\':
303 self.cur += 1 # Skip over the backslash.
305 raise GNException("String ends in a backslash in:\n " +
310 raise GNException('Unterminated string:\n ' + self.input[begin:])
313 self.cur += 1 # Consume trailing ".
315 return UnescapeGNString(self.input[begin:end])
318 self.ConsumeWhitespace()
320 raise GNException('Expected list but got nothing.')
322 # Skip over opening '['.
323 if self.input[self.cur] != '[':
324 raise GNException("Expected [ for list but got:\n " +
325 self.input[self.cur:])
327 self.ConsumeWhitespace()
329 raise GNException("Unterminated list:\n " + self.input)
332 previous_had_trailing_comma = True
333 while not self.IsDone():
334 if self.input[self.cur] == ']':
335 self.cur += 1 # Skip over ']'.
338 if not previous_had_trailing_comma:
339 raise GNException("List items not separated by comma.")
341 list_result += [ self._ParseAllowTrailing() ]
342 self.ConsumeWhitespace()
346 # Consume comma if there is one.
347 previous_had_trailing_comma = self.input[self.cur] == ','
348 if previous_had_trailing_comma:
351 self.ConsumeWhitespace()
353 raise GNException("Unterminated list:\n " + self.input)
355 def _ConstantFollows(self, constant):
356 """Returns true if the given constant follows immediately at the current
357 location in the input. If it does, the text is consumed and the function
358 returns true. Otherwise, returns false and the current position is
360 end = self.cur + len(constant)
361 if end > len(self.input):
362 return False # Not enough room.
363 if self.input[self.cur:end] == constant: