blob: fb94d5496c5e555a4d6118198ac4421423c87ddc [file] [log] [blame]
mtklein7fbfbbe2016-07-21 12:25:45 -07001# 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.
4
5"""Helper functions useful when writing scripts that integrate with GN.
6
7The main functions are ToGNString and FromGNString which convert between
8serialized GN veriables and Python variables.
9
10To use in a random python file in the build:
11
12 import os
13 import sys
14
15 sys.path.append(os.path.join(os.path.dirname(__file__),
16 os.pardir, os.pardir, "build"))
17 import gn_helpers
18
19Where the sequence of parameters to join is the relative path from your source
20file to the build directory."""
21
22class GNException(Exception):
23 pass
24
25
26def ToGNString(value, allow_dicts = True):
27 """Returns a stringified GN equivalent of the Python value.
28
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.")
35 return '"' + \
36 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
37 '"'
38
39 if isinstance(value, unicode):
40 return ToGNString(value.encode('utf-8'))
41
42 if isinstance(value, bool):
43 if value:
44 return "true"
45 return "false"
46
47 if isinstance(value, list):
48 return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
49
50 if isinstance(value, dict):
51 if not allow_dicts:
52 raise GNException("Attempting to recursively print a dictionary.")
53 result = ""
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))
58 return result
59
60 if isinstance(value, int):
61 return str(value)
62
63 raise GNException("Unsupported type when printing to GN.")
64
65
66def FromGNString(input_):
67 """Converts the input string from a GN serialized value to Python values.
68
69 For details on supported types see GNValueParser.Parse() below.
70
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:
77 [ "file1", "file2" ]
78
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
83 function instead.
84
85
86 A NOTE ON STRING HANDLING:
87
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:
90 str = "asdf"
91 args = [ str, "--value=$str" ]
92 Will yield the command line:
93 asdf --value=asdf
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.
97
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_)
103 return parser.Parse()
104
105
106def FromGNArgs(input_):
107 """Converts a string with a bunch of gn arg assignments into a Python dict.
108
109 Given a whitespace-separated list of
110
111 <ident> = (integer | string | boolean | <list of the former>)
112
113 gn assignments, this returns a Python dict, i.e.:
114
115 FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
116
117 Only simple types and lists supported; variables, structs, calls
118 and other, more complicated things are not.
119
120 This routine is meant to handle only the simple sorts of values that
121 arise in parsing --args.
122 """
123 parser = GNValueParser(input_)
124 return parser.ParseArgs()
125
126
127def UnescapeGNString(value):
128 """Given a string with GN escaping, returns the unescaped string.
129
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."""
133 result = ''
134 i = 0
135 while i < len(value):
136 if value[i] == '\\':
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.
141 result += next_char
142 i += 1
143 else:
144 # Any other backslash is a literal.
145 result += '\\'
146 else:
147 result += value[i]
148 i += 1
149 return result
150
151
152def _IsDigitOrMinus(char):
153 return char in "-0123456789"
154
155
156class GNValueParser(object):
157 """Duplicates GN parsing of values and converts to Python types.
158
159 Normally you would use the wrapper function FromGNValue() below.
160
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):
164 self.input = string
165 self.cur = 0
166
167 def IsDone(self):
168 return self.cur == len(self.input)
169
170 def ConsumeWhitespace(self):
171 while not self.IsDone() and self.input[self.cur] in ' \t\n':
172 self.cur += 1
173
174 def Parse(self):
175 """Converts a string representing a printed GN value to the Python type.
176
177 See additional usage notes on FromGNString above.
178
179 - GN booleans ('true', 'false') will be converted to Python booleans.
180
181 - GN numbers ('123') will be converted to Python numbers.
182
183 - GN strings (double-quoted as in '"asdf"') will be converted to Python
184 strings with GN escaping rules. GN string interpolation (embedded
185 variables preceeded by $) are not supported and will be returned as
186 literals.
187
188 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
189
190 - GN scopes ('{ ... }') are not supported."""
191 result = self._ParseAllowTrailing()
192 self.ConsumeWhitespace()
193 if not self.IsDone():
194 raise GNException("Trailing input after parsing:\n " +
195 self.input[self.cur:])
196 return result
197
198 def ParseArgs(self):
199 """Converts a whitespace-separated list of ident=literals to a dict.
200
201 See additional usage notes on FromGNArgs, above.
202 """
203 d = {}
204
205 self.ConsumeWhitespace()
206 while not self.IsDone():
207 ident = self._ParseIdent()
208 self.ConsumeWhitespace()
209 if self.input[self.cur] != '=':
210 raise GNException("Unexpected token: " + self.input[self.cur:])
211 self.cur += 1
212 self.ConsumeWhitespace()
213 val = self._ParseAllowTrailing()
214 self.ConsumeWhitespace()
215 d[ident] = val
216
217 return d
218
219 def _ParseAllowTrailing(self):
220 """Internal version of Parse that doesn't check for trailing stuff."""
221 self.ConsumeWhitespace()
222 if self.IsDone():
223 raise GNException("Expected input to parse.")
224
225 next_char = self.input[self.cur]
226 if next_char == '[':
227 return self.ParseList()
228 elif _IsDigitOrMinus(next_char):
229 return self.ParseNumber()
230 elif next_char == '"':
231 return self.ParseString()
232 elif self._ConstantFollows('true'):
233 return True
234 elif self._ConstantFollows('false'):
235 return False
236 else:
237 raise GNException("Unexpected token: " + self.input[self.cur:])
238
239 def _ParseIdent(self):
240 id_ = ''
241
242 next_char = self.input[self.cur]
243 if not next_char.isalpha() and not next_char=='_':
244 raise GNException("Expected an identifier: " + self.input[self.cur:])
245
246 id_ += next_char
247 self.cur += 1
248
249 next_char = self.input[self.cur]
250 while next_char.isalpha() or next_char.isdigit() or next_char=='_':
251 id_ += next_char
252 self.cur += 1
253 next_char = self.input[self.cur]
254
255 return id_
256
257 def ParseNumber(self):
258 self.ConsumeWhitespace()
259 if self.IsDone():
260 raise GNException('Expected number but got nothing.')
261
262 begin = self.cur
263
264 # The first character can include a negative sign.
265 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
266 self.cur += 1
267 while not self.IsDone() and self.input[self.cur].isdigit():
268 self.cur += 1
269
270 number_string = self.input[begin:self.cur]
271 if not len(number_string) or number_string == '-':
272 raise GNException("Not a valid number.")
273 return int(number_string)
274
275 def ParseString(self):
276 self.ConsumeWhitespace()
277 if self.IsDone():
278 raise GNException('Expected string but got nothing.')
279
280 if self.input[self.cur] != '"':
281 raise GNException('Expected string beginning in a " but got:\n ' +
282 self.input[self.cur:])
283 self.cur += 1 # Skip over quote.
284
285 begin = self.cur
286 while not self.IsDone() and self.input[self.cur] != '"':
287 if self.input[self.cur] == '\\':
288 self.cur += 1 # Skip over the backslash.
289 if self.IsDone():
290 raise GNException("String ends in a backslash in:\n " +
291 self.input)
292 self.cur += 1
293
294 if self.IsDone():
295 raise GNException('Unterminated string:\n ' + self.input[begin:])
296
297 end = self.cur
298 self.cur += 1 # Consume trailing ".
299
300 return UnescapeGNString(self.input[begin:end])
301
302 def ParseList(self):
303 self.ConsumeWhitespace()
304 if self.IsDone():
305 raise GNException('Expected list but got nothing.')
306
307 # Skip over opening '['.
308 if self.input[self.cur] != '[':
309 raise GNException("Expected [ for list but got:\n " +
310 self.input[self.cur:])
311 self.cur += 1
312 self.ConsumeWhitespace()
313 if self.IsDone():
314 raise GNException("Unterminated list:\n " + self.input)
315
316 list_result = []
317 previous_had_trailing_comma = True
318 while not self.IsDone():
319 if self.input[self.cur] == ']':
320 self.cur += 1 # Skip over ']'.
321 return list_result
322
323 if not previous_had_trailing_comma:
324 raise GNException("List items not separated by comma.")
325
326 list_result += [ self._ParseAllowTrailing() ]
327 self.ConsumeWhitespace()
328 if self.IsDone():
329 break
330
331 # Consume comma if there is one.
332 previous_had_trailing_comma = self.input[self.cur] == ','
333 if previous_had_trailing_comma:
334 # Consume comma.
335 self.cur += 1
336 self.ConsumeWhitespace()
337
338 raise GNException("Unterminated list:\n " + self.input)
339
340 def _ConstantFollows(self, constant):
341 """Returns true if the given constant follows immediately at the current
342 location in the input. If it does, the text is consumed and the function
343 returns true. Otherwise, returns false and the current position is
344 unchanged."""
345 end = self.cur + len(constant)
346 if end > len(self.input):
347 return False # Not enough room.
348 if self.input[self.cur:end] == constant:
349 self.cur = end
350 return True
351 return False