blob: b90c2fbbd71b2e04f3cb071f3094344bdad67382 [file] [log] [blame]
[email protected]530bf7e82014-04-06 04:35:101# 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
brettw9dffb542016-01-22 18:40:035"""Helper functions useful when writing scripts that integrate with GN.
6
7The main functions are ToGNString and FromGNString which convert between
brettw81687d82016-01-29 23:23:108serialized 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."""
[email protected]530bf7e82014-04-06 04:35:1021
Ben Pastene00156a22020-03-23 18:13:1022import os
23import re
Raul Tambre4197d3a2019-03-19 15:04:2024import sys
25
26
Ben Pastene00156a22020-03-23 18:13:1027IMPORT_RE = re.compile(r'^import\("//(\S+)"\)')
28
29
[email protected]530bf7e82014-04-06 04:35:1030class GNException(Exception):
31 pass
32
33
34def ToGNString(value, allow_dicts = True):
dprankeeca4a782016-04-14 01:42:3835 """Returns a stringified GN equivalent of the Python value.
[email protected]530bf7e82014-04-06 04:35:1036
37 allow_dicts indicates if this function will allow converting dictionaries
38 to GN scopes. This is only possible at the top level, you can't nest a
39 GN scope in a list, so this should be set to False for recursive calls."""
Raul Tambre4197d3a2019-03-19 15:04:2040 if isinstance(value, str):
[email protected]530bf7e82014-04-06 04:35:1041 if value.find('\n') >= 0:
42 raise GNException("Trying to print a string with a newline in it.")
brettw9dffb542016-01-22 18:40:0343 return '"' + \
44 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
45 '"'
46
Raul Tambre4197d3a2019-03-19 15:04:2047 if sys.version_info.major < 3 and isinstance(value, unicode):
hashimoto7bf1d0a2016-04-04 01:01:5148 return ToGNString(value.encode('utf-8'))
49
brettw9dffb542016-01-22 18:40:0350 if isinstance(value, bool):
51 if value:
52 return "true"
53 return "false"
[email protected]530bf7e82014-04-06 04:35:1054
55 if isinstance(value, list):
56 return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
57
58 if isinstance(value, dict):
59 if not allow_dicts:
60 raise GNException("Attempting to recursively print a dictionary.")
61 result = ""
dprankeeca4a782016-04-14 01:42:3862 for key in sorted(value):
Raul Tambre4197d3a2019-03-19 15:04:2063 if not isinstance(key, str) and not isinstance(key, unicode):
[email protected]530bf7e82014-04-06 04:35:1064 raise GNException("Dictionary key is not a string.")
65 result += "%s = %s\n" % (key, ToGNString(value[key], False))
66 return result
67
68 if isinstance(value, int):
69 return str(value)
70
71 raise GNException("Unsupported type when printing to GN.")
brettw9dffb542016-01-22 18:40:0372
73
yyanagisawa18ef9302016-10-07 02:35:1774def FromGNString(input_string):
brettw9dffb542016-01-22 18:40:0375 """Converts the input string from a GN serialized value to Python values.
76
77 For details on supported types see GNValueParser.Parse() below.
78
79 If your GN script did:
80 something = [ "file1", "file2" ]
81 args = [ "--values=$something" ]
82 The command line would look something like:
83 --values="[ \"file1\", \"file2\" ]"
84 Which when interpreted as a command line gives the value:
85 [ "file1", "file2" ]
86
87 You can parse this into a Python list using GN rules with:
88 input_values = FromGNValues(options.values)
89 Although the Python 'ast' module will parse many forms of such input, it
90 will not handle GN escaping properly, nor GN booleans. You should use this
91 function instead.
92
93
94 A NOTE ON STRING HANDLING:
95
96 If you just pass a string on the command line to your Python script, or use
97 string interpolation on a string variable, the strings will not be quoted:
98 str = "asdf"
99 args = [ str, "--value=$str" ]
100 Will yield the command line:
101 asdf --value=asdf
102 The unquoted asdf string will not be valid input to this function, which
103 accepts only quoted strings like GN scripts. In such cases, you can just use
104 the Python string literal directly.
105
106 The main use cases for this is for other types, in particular lists. When
107 using string interpolation on a list (as in the top example) the embedded
108 strings will be quoted and escaped according to GN rules so the list can be
109 re-parsed to get the same result."""
yyanagisawa18ef9302016-10-07 02:35:17110 parser = GNValueParser(input_string)
brettw9dffb542016-01-22 18:40:03111 return parser.Parse()
112
113
yyanagisawa18ef9302016-10-07 02:35:17114def FromGNArgs(input_string):
dpranke65d84dc02016-04-06 00:07:18115 """Converts a string with a bunch of gn arg assignments into a Python dict.
116
117 Given a whitespace-separated list of
118
119 <ident> = (integer | string | boolean | <list of the former>)
120
121 gn assignments, this returns a Python dict, i.e.:
122
123 FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
124
125 Only simple types and lists supported; variables, structs, calls
126 and other, more complicated things are not.
127
128 This routine is meant to handle only the simple sorts of values that
129 arise in parsing --args.
130 """
yyanagisawa18ef9302016-10-07 02:35:17131 parser = GNValueParser(input_string)
dpranke65d84dc02016-04-06 00:07:18132 return parser.ParseArgs()
133
134
brettw9dffb542016-01-22 18:40:03135def UnescapeGNString(value):
136 """Given a string with GN escaping, returns the unescaped string.
137
138 Be careful not to feed with input from a Python parsing function like
139 'ast' because it will do Python unescaping, which will be incorrect when
140 fed into the GN unescaper."""
141 result = ''
142 i = 0
143 while i < len(value):
144 if value[i] == '\\':
145 if i < len(value) - 1:
146 next_char = value[i + 1]
147 if next_char in ('$', '"', '\\'):
148 # These are the escaped characters GN supports.
149 result += next_char
150 i += 1
151 else:
152 # Any other backslash is a literal.
153 result += '\\'
154 else:
155 result += value[i]
156 i += 1
157 return result
158
159
160def _IsDigitOrMinus(char):
161 return char in "-0123456789"
162
163
164class GNValueParser(object):
165 """Duplicates GN parsing of values and converts to Python types.
166
167 Normally you would use the wrapper function FromGNValue() below.
168
169 If you expect input as a specific type, you can also call one of the Parse*
170 functions directly. All functions throw GNException on invalid input. """
171 def __init__(self, string):
172 self.input = string
173 self.cur = 0
174
175 def IsDone(self):
176 return self.cur == len(self.input)
177
Ben Pastene00156a22020-03-23 18:13:10178 def ReplaceImports(self):
179 """Replaces import(...) lines with the contents of the imports.
180
181 Recurses on itself until there are no imports remaining, in the case of
182 nested imports.
183 """
184 lines = self.input.splitlines()
185 if not any(line.startswith('import(') for line in lines):
186 return
187 for line in lines:
188 if not line.startswith('import('):
189 continue
190 regex_match = IMPORT_RE.match(line)
191 if not regex_match:
192 raise GNException('Not a valid import string: %s' % line)
193 import_path = os.path.join(
194 os.path.dirname(__file__), os.pardir, regex_match.group(1))
195 with open(import_path) as f:
196 imported_args = f.read()
197 self.input = self.input.replace(line, imported_args)
198 # Call ourselves again if we've just replaced an import() with additional
199 # imports.
200 self.ReplaceImports()
201
202
brettw9dffb542016-01-22 18:40:03203 def ConsumeWhitespace(self):
204 while not self.IsDone() and self.input[self.cur] in ' \t\n':
205 self.cur += 1
206
Ben Pastene9b24d852018-11-06 00:42:09207 def ConsumeComment(self):
208 if self.IsDone() or self.input[self.cur] != '#':
209 return
210
211 # Consume each comment, line by line.
212 while not self.IsDone() and self.input[self.cur] == '#':
213 # Consume the rest of the comment, up until the end of the line.
214 while not self.IsDone() and self.input[self.cur] != '\n':
215 self.cur += 1
216 # Move the cursor to the next line (if there is one).
217 if not self.IsDone():
218 self.cur += 1
219
brettw9dffb542016-01-22 18:40:03220 def Parse(self):
221 """Converts a string representing a printed GN value to the Python type.
222
223 See additional usage notes on FromGNString above.
224
225 - GN booleans ('true', 'false') will be converted to Python booleans.
226
227 - GN numbers ('123') will be converted to Python numbers.
228
229 - GN strings (double-quoted as in '"asdf"') will be converted to Python
230 strings with GN escaping rules. GN string interpolation (embedded
Julien Brianceau96dfe4d82017-08-01 09:03:13231 variables preceded by $) are not supported and will be returned as
brettw9dffb542016-01-22 18:40:03232 literals.
233
234 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
235
236 - GN scopes ('{ ... }') are not supported."""
237 result = self._ParseAllowTrailing()
238 self.ConsumeWhitespace()
239 if not self.IsDone():
240 raise GNException("Trailing input after parsing:\n " +
241 self.input[self.cur:])
242 return result
243
dpranke65d84dc02016-04-06 00:07:18244 def ParseArgs(self):
245 """Converts a whitespace-separated list of ident=literals to a dict.
246
247 See additional usage notes on FromGNArgs, above.
248 """
249 d = {}
250
Ben Pastene00156a22020-03-23 18:13:10251 self.ReplaceImports()
dpranke65d84dc02016-04-06 00:07:18252 self.ConsumeWhitespace()
Ben Pastene9b24d852018-11-06 00:42:09253 self.ConsumeComment()
dpranke65d84dc02016-04-06 00:07:18254 while not self.IsDone():
255 ident = self._ParseIdent()
256 self.ConsumeWhitespace()
257 if self.input[self.cur] != '=':
258 raise GNException("Unexpected token: " + self.input[self.cur:])
259 self.cur += 1
260 self.ConsumeWhitespace()
261 val = self._ParseAllowTrailing()
262 self.ConsumeWhitespace()
Ben Pastene9b24d852018-11-06 00:42:09263 self.ConsumeComment()
dpranke65d84dc02016-04-06 00:07:18264 d[ident] = val
265
266 return d
267
brettw9dffb542016-01-22 18:40:03268 def _ParseAllowTrailing(self):
269 """Internal version of Parse that doesn't check for trailing stuff."""
270 self.ConsumeWhitespace()
271 if self.IsDone():
272 raise GNException("Expected input to parse.")
273
274 next_char = self.input[self.cur]
275 if next_char == '[':
276 return self.ParseList()
277 elif _IsDigitOrMinus(next_char):
278 return self.ParseNumber()
279 elif next_char == '"':
280 return self.ParseString()
281 elif self._ConstantFollows('true'):
282 return True
283 elif self._ConstantFollows('false'):
284 return False
285 else:
286 raise GNException("Unexpected token: " + self.input[self.cur:])
287
dpranke65d84dc02016-04-06 00:07:18288 def _ParseIdent(self):
yyanagisawa18ef9302016-10-07 02:35:17289 ident = ''
dpranke65d84dc02016-04-06 00:07:18290
291 next_char = self.input[self.cur]
292 if not next_char.isalpha() and not next_char=='_':
293 raise GNException("Expected an identifier: " + self.input[self.cur:])
294
yyanagisawa18ef9302016-10-07 02:35:17295 ident += next_char
dpranke65d84dc02016-04-06 00:07:18296 self.cur += 1
297
298 next_char = self.input[self.cur]
299 while next_char.isalpha() or next_char.isdigit() or next_char=='_':
yyanagisawa18ef9302016-10-07 02:35:17300 ident += next_char
dpranke65d84dc02016-04-06 00:07:18301 self.cur += 1
302 next_char = self.input[self.cur]
303
yyanagisawa18ef9302016-10-07 02:35:17304 return ident
dpranke65d84dc02016-04-06 00:07:18305
brettw9dffb542016-01-22 18:40:03306 def ParseNumber(self):
307 self.ConsumeWhitespace()
308 if self.IsDone():
309 raise GNException('Expected number but got nothing.')
310
311 begin = self.cur
312
313 # The first character can include a negative sign.
314 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
315 self.cur += 1
316 while not self.IsDone() and self.input[self.cur].isdigit():
317 self.cur += 1
318
319 number_string = self.input[begin:self.cur]
320 if not len(number_string) or number_string == '-':
321 raise GNException("Not a valid number.")
322 return int(number_string)
323
324 def ParseString(self):
325 self.ConsumeWhitespace()
326 if self.IsDone():
327 raise GNException('Expected string but got nothing.')
328
329 if self.input[self.cur] != '"':
330 raise GNException('Expected string beginning in a " but got:\n ' +
331 self.input[self.cur:])
332 self.cur += 1 # Skip over quote.
333
334 begin = self.cur
335 while not self.IsDone() and self.input[self.cur] != '"':
336 if self.input[self.cur] == '\\':
337 self.cur += 1 # Skip over the backslash.
338 if self.IsDone():
339 raise GNException("String ends in a backslash in:\n " +
340 self.input)
341 self.cur += 1
342
343 if self.IsDone():
344 raise GNException('Unterminated string:\n ' + self.input[begin:])
345
346 end = self.cur
347 self.cur += 1 # Consume trailing ".
348
349 return UnescapeGNString(self.input[begin:end])
350
351 def ParseList(self):
352 self.ConsumeWhitespace()
353 if self.IsDone():
354 raise GNException('Expected list but got nothing.')
355
356 # Skip over opening '['.
357 if self.input[self.cur] != '[':
358 raise GNException("Expected [ for list but got:\n " +
359 self.input[self.cur:])
360 self.cur += 1
361 self.ConsumeWhitespace()
362 if self.IsDone():
363 raise GNException("Unterminated list:\n " + self.input)
364
365 list_result = []
366 previous_had_trailing_comma = True
367 while not self.IsDone():
368 if self.input[self.cur] == ']':
369 self.cur += 1 # Skip over ']'.
370 return list_result
371
372 if not previous_had_trailing_comma:
373 raise GNException("List items not separated by comma.")
374
375 list_result += [ self._ParseAllowTrailing() ]
376 self.ConsumeWhitespace()
377 if self.IsDone():
378 break
379
380 # Consume comma if there is one.
381 previous_had_trailing_comma = self.input[self.cur] == ','
382 if previous_had_trailing_comma:
383 # Consume comma.
384 self.cur += 1
385 self.ConsumeWhitespace()
386
387 raise GNException("Unterminated list:\n " + self.input)
388
389 def _ConstantFollows(self, constant):
390 """Returns true if the given constant follows immediately at the current
391 location in the input. If it does, the text is consumed and the function
392 returns true. Otherwise, returns false and the current position is
393 unchanged."""
394 end = self.cur + len(constant)
dprankee031ec22016-04-02 00:17:34395 if end > len(self.input):
brettw9dffb542016-01-22 18:40:03396 return False # Not enough room.
397 if self.input[self.cur:end] == constant:
398 self.cur = end
399 return True
400 return False