blob: 53543e669abd3dfd9d24770e66227cb37bb5a4f0 [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
Raul Tambre4197d3a2019-03-19 15:04:2022import sys
23
24
[email protected]530bf7e82014-04-06 04:35:1025class GNException(Exception):
26 pass
27
28
29def ToGNString(value, allow_dicts = True):
dprankeeca4a782016-04-14 01:42:3830 """Returns a stringified GN equivalent of the Python value.
[email protected]530bf7e82014-04-06 04:35:1031
32 allow_dicts indicates if this function will allow converting dictionaries
33 to GN scopes. This is only possible at the top level, you can't nest a
34 GN scope in a list, so this should be set to False for recursive calls."""
Raul Tambre4197d3a2019-03-19 15:04:2035 if isinstance(value, str):
[email protected]530bf7e82014-04-06 04:35:1036 if value.find('\n') >= 0:
37 raise GNException("Trying to print a string with a newline in it.")
brettw9dffb542016-01-22 18:40:0338 return '"' + \
39 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
40 '"'
41
Raul Tambre4197d3a2019-03-19 15:04:2042 if sys.version_info.major < 3 and isinstance(value, unicode):
hashimoto7bf1d0a2016-04-04 01:01:5143 return ToGNString(value.encode('utf-8'))
44
brettw9dffb542016-01-22 18:40:0345 if isinstance(value, bool):
46 if value:
47 return "true"
48 return "false"
[email protected]530bf7e82014-04-06 04:35:1049
50 if isinstance(value, list):
51 return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
52
53 if isinstance(value, dict):
54 if not allow_dicts:
55 raise GNException("Attempting to recursively print a dictionary.")
56 result = ""
dprankeeca4a782016-04-14 01:42:3857 for key in sorted(value):
Raul Tambre4197d3a2019-03-19 15:04:2058 if not isinstance(key, str) and not isinstance(key, unicode):
[email protected]530bf7e82014-04-06 04:35:1059 raise GNException("Dictionary key is not a string.")
60 result += "%s = %s\n" % (key, ToGNString(value[key], False))
61 return result
62
63 if isinstance(value, int):
64 return str(value)
65
66 raise GNException("Unsupported type when printing to GN.")
brettw9dffb542016-01-22 18:40:0367
68
yyanagisawa18ef9302016-10-07 02:35:1769def FromGNString(input_string):
brettw9dffb542016-01-22 18:40:0370 """Converts the input string from a GN serialized value to Python values.
71
72 For details on supported types see GNValueParser.Parse() below.
73
74 If your GN script did:
75 something = [ "file1", "file2" ]
76 args = [ "--values=$something" ]
77 The command line would look something like:
78 --values="[ \"file1\", \"file2\" ]"
79 Which when interpreted as a command line gives the value:
80 [ "file1", "file2" ]
81
82 You can parse this into a Python list using GN rules with:
83 input_values = FromGNValues(options.values)
84 Although the Python 'ast' module will parse many forms of such input, it
85 will not handle GN escaping properly, nor GN booleans. You should use this
86 function instead.
87
88
89 A NOTE ON STRING HANDLING:
90
91 If you just pass a string on the command line to your Python script, or use
92 string interpolation on a string variable, the strings will not be quoted:
93 str = "asdf"
94 args = [ str, "--value=$str" ]
95 Will yield the command line:
96 asdf --value=asdf
97 The unquoted asdf string will not be valid input to this function, which
98 accepts only quoted strings like GN scripts. In such cases, you can just use
99 the Python string literal directly.
100
101 The main use cases for this is for other types, in particular lists. When
102 using string interpolation on a list (as in the top example) the embedded
103 strings will be quoted and escaped according to GN rules so the list can be
104 re-parsed to get the same result."""
yyanagisawa18ef9302016-10-07 02:35:17105 parser = GNValueParser(input_string)
brettw9dffb542016-01-22 18:40:03106 return parser.Parse()
107
108
yyanagisawa18ef9302016-10-07 02:35:17109def FromGNArgs(input_string):
dpranke65d84dc02016-04-06 00:07:18110 """Converts a string with a bunch of gn arg assignments into a Python dict.
111
112 Given a whitespace-separated list of
113
114 <ident> = (integer | string | boolean | <list of the former>)
115
116 gn assignments, this returns a Python dict, i.e.:
117
118 FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
119
120 Only simple types and lists supported; variables, structs, calls
121 and other, more complicated things are not.
122
123 This routine is meant to handle only the simple sorts of values that
124 arise in parsing --args.
125 """
yyanagisawa18ef9302016-10-07 02:35:17126 parser = GNValueParser(input_string)
dpranke65d84dc02016-04-06 00:07:18127 return parser.ParseArgs()
128
129
brettw9dffb542016-01-22 18:40:03130def UnescapeGNString(value):
131 """Given a string with GN escaping, returns the unescaped string.
132
133 Be careful not to feed with input from a Python parsing function like
134 'ast' because it will do Python unescaping, which will be incorrect when
135 fed into the GN unescaper."""
136 result = ''
137 i = 0
138 while i < len(value):
139 if value[i] == '\\':
140 if i < len(value) - 1:
141 next_char = value[i + 1]
142 if next_char in ('$', '"', '\\'):
143 # These are the escaped characters GN supports.
144 result += next_char
145 i += 1
146 else:
147 # Any other backslash is a literal.
148 result += '\\'
149 else:
150 result += value[i]
151 i += 1
152 return result
153
154
155def _IsDigitOrMinus(char):
156 return char in "-0123456789"
157
158
159class GNValueParser(object):
160 """Duplicates GN parsing of values and converts to Python types.
161
162 Normally you would use the wrapper function FromGNValue() below.
163
164 If you expect input as a specific type, you can also call one of the Parse*
165 functions directly. All functions throw GNException on invalid input. """
166 def __init__(self, string):
167 self.input = string
168 self.cur = 0
169
170 def IsDone(self):
171 return self.cur == len(self.input)
172
173 def ConsumeWhitespace(self):
174 while not self.IsDone() and self.input[self.cur] in ' \t\n':
175 self.cur += 1
176
Ben Pastene9b24d852018-11-06 00:42:09177 def ConsumeComment(self):
178 if self.IsDone() or self.input[self.cur] != '#':
179 return
180
181 # Consume each comment, line by line.
182 while not self.IsDone() and self.input[self.cur] == '#':
183 # Consume the rest of the comment, up until the end of the line.
184 while not self.IsDone() and self.input[self.cur] != '\n':
185 self.cur += 1
186 # Move the cursor to the next line (if there is one).
187 if not self.IsDone():
188 self.cur += 1
189
brettw9dffb542016-01-22 18:40:03190 def Parse(self):
191 """Converts a string representing a printed GN value to the Python type.
192
193 See additional usage notes on FromGNString above.
194
195 - GN booleans ('true', 'false') will be converted to Python booleans.
196
197 - GN numbers ('123') will be converted to Python numbers.
198
199 - GN strings (double-quoted as in '"asdf"') will be converted to Python
200 strings with GN escaping rules. GN string interpolation (embedded
Julien Brianceau96dfe4d82017-08-01 09:03:13201 variables preceded by $) are not supported and will be returned as
brettw9dffb542016-01-22 18:40:03202 literals.
203
204 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
205
206 - GN scopes ('{ ... }') are not supported."""
207 result = self._ParseAllowTrailing()
208 self.ConsumeWhitespace()
209 if not self.IsDone():
210 raise GNException("Trailing input after parsing:\n " +
211 self.input[self.cur:])
212 return result
213
dpranke65d84dc02016-04-06 00:07:18214 def ParseArgs(self):
215 """Converts a whitespace-separated list of ident=literals to a dict.
216
217 See additional usage notes on FromGNArgs, above.
218 """
219 d = {}
220
221 self.ConsumeWhitespace()
Ben Pastene9b24d852018-11-06 00:42:09222 self.ConsumeComment()
dpranke65d84dc02016-04-06 00:07:18223 while not self.IsDone():
224 ident = self._ParseIdent()
225 self.ConsumeWhitespace()
226 if self.input[self.cur] != '=':
227 raise GNException("Unexpected token: " + self.input[self.cur:])
228 self.cur += 1
229 self.ConsumeWhitespace()
230 val = self._ParseAllowTrailing()
231 self.ConsumeWhitespace()
Ben Pastene9b24d852018-11-06 00:42:09232 self.ConsumeComment()
dpranke65d84dc02016-04-06 00:07:18233 d[ident] = val
234
235 return d
236
brettw9dffb542016-01-22 18:40:03237 def _ParseAllowTrailing(self):
238 """Internal version of Parse that doesn't check for trailing stuff."""
239 self.ConsumeWhitespace()
240 if self.IsDone():
241 raise GNException("Expected input to parse.")
242
243 next_char = self.input[self.cur]
244 if next_char == '[':
245 return self.ParseList()
246 elif _IsDigitOrMinus(next_char):
247 return self.ParseNumber()
248 elif next_char == '"':
249 return self.ParseString()
250 elif self._ConstantFollows('true'):
251 return True
252 elif self._ConstantFollows('false'):
253 return False
254 else:
255 raise GNException("Unexpected token: " + self.input[self.cur:])
256
dpranke65d84dc02016-04-06 00:07:18257 def _ParseIdent(self):
yyanagisawa18ef9302016-10-07 02:35:17258 ident = ''
dpranke65d84dc02016-04-06 00:07:18259
260 next_char = self.input[self.cur]
261 if not next_char.isalpha() and not next_char=='_':
262 raise GNException("Expected an identifier: " + self.input[self.cur:])
263
yyanagisawa18ef9302016-10-07 02:35:17264 ident += next_char
dpranke65d84dc02016-04-06 00:07:18265 self.cur += 1
266
267 next_char = self.input[self.cur]
268 while next_char.isalpha() or next_char.isdigit() or next_char=='_':
yyanagisawa18ef9302016-10-07 02:35:17269 ident += next_char
dpranke65d84dc02016-04-06 00:07:18270 self.cur += 1
271 next_char = self.input[self.cur]
272
yyanagisawa18ef9302016-10-07 02:35:17273 return ident
dpranke65d84dc02016-04-06 00:07:18274
brettw9dffb542016-01-22 18:40:03275 def ParseNumber(self):
276 self.ConsumeWhitespace()
277 if self.IsDone():
278 raise GNException('Expected number but got nothing.')
279
280 begin = self.cur
281
282 # The first character can include a negative sign.
283 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
284 self.cur += 1
285 while not self.IsDone() and self.input[self.cur].isdigit():
286 self.cur += 1
287
288 number_string = self.input[begin:self.cur]
289 if not len(number_string) or number_string == '-':
290 raise GNException("Not a valid number.")
291 return int(number_string)
292
293 def ParseString(self):
294 self.ConsumeWhitespace()
295 if self.IsDone():
296 raise GNException('Expected string but got nothing.')
297
298 if self.input[self.cur] != '"':
299 raise GNException('Expected string beginning in a " but got:\n ' +
300 self.input[self.cur:])
301 self.cur += 1 # Skip over quote.
302
303 begin = self.cur
304 while not self.IsDone() and self.input[self.cur] != '"':
305 if self.input[self.cur] == '\\':
306 self.cur += 1 # Skip over the backslash.
307 if self.IsDone():
308 raise GNException("String ends in a backslash in:\n " +
309 self.input)
310 self.cur += 1
311
312 if self.IsDone():
313 raise GNException('Unterminated string:\n ' + self.input[begin:])
314
315 end = self.cur
316 self.cur += 1 # Consume trailing ".
317
318 return UnescapeGNString(self.input[begin:end])
319
320 def ParseList(self):
321 self.ConsumeWhitespace()
322 if self.IsDone():
323 raise GNException('Expected list but got nothing.')
324
325 # Skip over opening '['.
326 if self.input[self.cur] != '[':
327 raise GNException("Expected [ for list but got:\n " +
328 self.input[self.cur:])
329 self.cur += 1
330 self.ConsumeWhitespace()
331 if self.IsDone():
332 raise GNException("Unterminated list:\n " + self.input)
333
334 list_result = []
335 previous_had_trailing_comma = True
336 while not self.IsDone():
337 if self.input[self.cur] == ']':
338 self.cur += 1 # Skip over ']'.
339 return list_result
340
341 if not previous_had_trailing_comma:
342 raise GNException("List items not separated by comma.")
343
344 list_result += [ self._ParseAllowTrailing() ]
345 self.ConsumeWhitespace()
346 if self.IsDone():
347 break
348
349 # Consume comma if there is one.
350 previous_had_trailing_comma = self.input[self.cur] == ','
351 if previous_had_trailing_comma:
352 # Consume comma.
353 self.cur += 1
354 self.ConsumeWhitespace()
355
356 raise GNException("Unterminated list:\n " + self.input)
357
358 def _ConstantFollows(self, constant):
359 """Returns true if the given constant follows immediately at the current
360 location in the input. If it does, the text is consumed and the function
361 returns true. Otherwise, returns false and the current position is
362 unchanged."""
363 end = self.cur + len(constant)
dprankee031ec22016-04-02 00:17:34364 if end > len(self.input):
brettw9dffb542016-01-22 18:40:03365 return False # Not enough room.
366 if self.input[self.cur:end] == constant:
367 self.cur = end
368 return True
369 return False