OLD | NEW |
---|---|
(Empty) | |
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. | |
4 | |
5 """Helper functions useful when writing scripts that integrate with GN. | |
6 | |
7 The main functions are ToGNString and FromGNString which convert between | |
8 serialized GN veriables and Python variables. | |
jcgregorio
2016/07/21 18:51:56
variables
mtklein
2016/07/21 19:24:35
Yeah, I didn't even read this file except where ne
| |
9 | |
10 To 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 | |
19 Where the sequence of parameters to join is the relative path from your source | |
20 file to the build directory.""" | |
21 | |
22 class GNException(Exception): | |
23 pass | |
24 | |
25 | |
26 def 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 | |
66 def 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 | |
106 def 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 | |
127 def 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 | |
152 def _IsDigitOrMinus(char): | |
153 return char in "-0123456789" | |
154 | |
155 | |
156 class 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 | |
OLD | NEW |