OLD | NEW |
---|---|
1 # Copyright 2014 The Chromium Authors. All rights reserved. | 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 | 2 # Use of this source code is governed by a BSD-style license that can be |
3 # found in the LICENSE file. | 3 # found in the LICENSE file. |
4 | 4 |
5 """Helper functions useful when writing scripts that are run from GN's | 5 """Helper functions useful when writing scripts that integrate with GN. |
6 exec_script function.""" | 6 |
7 The main functions are ToGNString and FromGNString which convert between | |
8 serialized GN veriables and Python variables.""" | |
7 | 9 |
8 class GNException(Exception): | 10 class GNException(Exception): |
9 pass | 11 pass |
10 | 12 |
11 | 13 |
12 def ToGNString(value, allow_dicts = True): | 14 def ToGNString(value, allow_dicts = True): |
13 """Prints the given value to stdout. | 15 """Prints the given value to stdout. |
14 | 16 |
15 allow_dicts indicates if this function will allow converting dictionaries | 17 allow_dicts indicates if this function will allow converting dictionaries |
16 to GN scopes. This is only possible at the top level, you can't nest a | 18 to GN scopes. This is only possible at the top level, you can't nest a |
17 GN scope in a list, so this should be set to False for recursive calls.""" | 19 GN scope in a list, so this should be set to False for recursive calls.""" |
18 if isinstance(value, str): | 20 if isinstance(value, str): |
19 if value.find('\n') >= 0: | 21 if value.find('\n') >= 0: |
20 raise GNException("Trying to print a string with a newline in it.") | 22 raise GNException("Trying to print a string with a newline in it.") |
21 return '"' + value.replace('"', '\\"') + '"' | 23 return '"' + \ |
24 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \ | |
25 '"' | |
26 | |
27 if isinstance(value, bool): | |
28 if value: | |
29 return "true" | |
30 return "false" | |
22 | 31 |
23 if isinstance(value, list): | 32 if isinstance(value, list): |
24 return '[ %s ]' % ', '.join(ToGNString(v) for v in value) | 33 return '[ %s ]' % ', '.join(ToGNString(v) for v in value) |
25 | 34 |
26 if isinstance(value, dict): | 35 if isinstance(value, dict): |
27 if not allow_dicts: | 36 if not allow_dicts: |
28 raise GNException("Attempting to recursively print a dictionary.") | 37 raise GNException("Attempting to recursively print a dictionary.") |
29 result = "" | 38 result = "" |
30 for key in value: | 39 for key in value: |
31 if not isinstance(key, str): | 40 if not isinstance(key, str): |
32 raise GNException("Dictionary key is not a string.") | 41 raise GNException("Dictionary key is not a string.") |
33 result += "%s = %s\n" % (key, ToGNString(value[key], False)) | 42 result += "%s = %s\n" % (key, ToGNString(value[key], False)) |
34 return result | 43 return result |
35 | 44 |
36 if isinstance(value, int): | 45 if isinstance(value, int): |
37 return str(value) | 46 return str(value) |
38 | 47 |
39 raise GNException("Unsupported type when printing to GN.") | 48 raise GNException("Unsupported type when printing to GN.") |
49 | |
50 | |
51 def FromGNString(input): | |
52 """Converts the input string from a GN serialized value to Python values. | |
53 | |
54 For details on supported types see GNValueParser.Parse() below. | |
55 | |
56 If your GN script did: | |
57 something = [ "file1", "file2" ] | |
58 args = [ "--values=$something" ] | |
59 The command line would look something like: | |
60 --values="[ \"file1\", \"file2\" ]" | |
61 Which when interpreted as a command line gives the value: | |
62 [ "file1", "file2" ] | |
63 | |
64 You can parse this into a Python list using GN rules with: | |
65 input_values = FromGNValues(options.values) | |
66 Although the Python 'ast' module will parse many forms of such input, it | |
67 will not handle GN escaping properly, nor GN booleans. You should use this | |
68 function instead. | |
69 | |
70 | |
71 A NOTE ON STRING HANDLING: | |
72 | |
73 If you just pass a string on the command line to your Python script, or use | |
74 string interpolation on a string variable, the strings will not be quoted: | |
75 str = "asdf" | |
76 args = [ str, "--value=$str" ] | |
77 Will yield the command line: | |
78 asdf --value=asdf | |
79 The unquoted asdf string will not be valid input to this function, which | |
80 accepts only quoted strings like GN scripts. In such cases, you can just use | |
81 the Python string literal directly. | |
82 | |
83 The main use cases for this is for other types, in particular lists. When | |
84 using string interpolation on a list (as in the top example) the embedded | |
85 strings will be quoted and escaped according to GN rules so the list can be | |
86 re-parsed to get the same result.""" | |
87 parser = GNValueParser(input) | |
88 return parser.Parse() | |
89 | |
90 | |
91 def UnescapeGNString(value): | |
Dirk Pranke
2016/01/22 00:34:02
Is this supposed to be a public function, or shoul
brettw
2016/01/22 18:12:14
I was thinking public since one could conceive of
| |
92 """Given a string with GN escaping, returns the unescaped string. | |
93 | |
94 Be careful not to feed with input from a Python parsing function like | |
95 'ast' because it will do Python unescaping, which will be incorrect when | |
96 fed into the GN unescaper.""" | |
97 result = '' | |
98 i = 0 | |
99 while i < len(value): | |
100 if value[i] == '\\': | |
101 if i < len(value) - 1: | |
102 next_char = value[i + 1] | |
103 if next_char == '$' or next_char == '"' or next_char == '\\': | |
Dirk Pranke
2016/01/22 00:34:02
nit: maybe
if next_char in ('$', '"', '\\'):
brettw
2016/01/22 18:12:15
Done.
| |
104 # These are the escaped characters GN supports. | |
105 result += next_char | |
106 i += 1 | |
107 else: | |
108 # Any other backslash is a litera. | |
Dirk Pranke
2016/01/22 00:34:02
typo: s/litera/literal/ .
| |
109 result += '\\' | |
110 else: | |
111 result += value[i] | |
112 i += 1 | |
113 return result | |
114 | |
115 | |
116 class GNValueParser: | |
Dirk Pranke
2016/01/22 00:34:02
nit: inherit from object, like:
class GNValuePar
brettw
2016/01/22 18:12:15
Yes, if you want to parse a specific type or somet
| |
117 """Duplicates GN parsing of values and converts to Python types. | |
118 | |
119 Normally you would use the wrapper function FromGNValue() below. | |
120 | |
121 If you expect input as a specific type, you can also call one of the Parse* | |
122 functions directly. All functions throw GNException on invalid input. """ | |
123 def __init__(self, string): | |
124 self.input = string | |
125 self.cur = 0 | |
126 | |
127 def IsDone(self): | |
128 return self.cur == len(self.input) | |
129 | |
130 def ConsumeWhitespace(self): | |
131 while not self.IsDone() and self.input[self.cur] in ' \t\n': | |
132 self.cur += 1 | |
133 | |
134 def Parse(self): | |
135 """Converts a string representing a printed GN value to the Python type. | |
136 | |
137 See additional usage notes on FromGNString above. | |
138 | |
139 - GN booleans ('true', 'false') will be converted to Python booleans. | |
140 | |
141 - GN numbers ('123') will be converted to Python numbers. | |
142 | |
143 - GN strings (double-quoted as in '"asdf"') will be converted to Python | |
144 strings with GN escaping rules. GN string interpolation (embedded | |
145 variables preceeded by $) are not supported and will be returned as | |
146 literals. | |
147 | |
148 - GN lists ('[1, "asdf", 3]') will be converted to Python lists. | |
149 | |
150 - GN scopes ('{ ... }') are not supported.""" | |
151 result = self.ParseAllowTrailing() | |
152 self.ConsumeWhitespace() | |
153 if not self.IsDone(): | |
154 raise GNException("Trailing input after parsing:\n " + | |
155 self.input[self.cur:]) | |
156 return result | |
157 | |
158 def ParseAllowTrailing(self): | |
Dirk Pranke
2016/01/22 00:34:02
if this and the following functions are not public
| |
159 """Internal version of Parse that doesn't check for trailing stuff.""" | |
160 self.ConsumeWhitespace() | |
161 if self.IsDone(): | |
162 raise GNException("Expected input to parse.") | |
163 | |
164 next_char = self.input[self.cur] | |
165 if next_char == '[': | |
166 return self.ParseList() | |
167 elif next_char in "-0123456789": | |
Dirk Pranke
2016/01/22 00:34:02
nit: consider making this a helper function: IsDig
| |
168 return self.ParseNumber() | |
169 elif next_char == '"': | |
170 return self.ParseString() | |
171 elif self.ConstantFollows('true'): | |
172 return True | |
173 elif self.ConstantFollows('false'): | |
174 return False | |
175 else: | |
176 raise GNException("Unexpected token: " + self.input[self.cur:]) | |
177 | |
178 def ParseNumber(self): | |
179 self.ConsumeWhitespace() | |
180 if self.IsDone(): | |
181 raise GNException('Expected number but got nothing.') | |
182 | |
183 begin = self.cur | |
184 | |
185 # The first character can include a negative sign. | |
186 if not self.IsDone() and self.input[self.cur] in '-0123456789': | |
187 self.cur += 1 | |
188 while not self.IsDone() and self.input[self.cur] in '0123456789': | |
Dirk Pranke
2016/01/22 00:34:02
maybe self.input[self.cur].isdigit() ? maybe not .
brettw
2016/01/22 18:12:14
I don't think in regex's and the code is longer. I
| |
189 self.cur += 1 | |
190 | |
191 number_string = self.input[begin:self.cur] | |
192 if not len(number_string) or number_string == '-': | |
193 raise GNException("Not a valid number.") | |
194 return int(number_string) | |
195 | |
196 def ParseString(self): | |
197 self.ConsumeWhitespace() | |
198 if self.IsDone(): | |
199 raise GNException('Expected string but got nothing.') | |
200 | |
201 if self.input[self.cur] != '"': | |
202 raise GNException('Expected string beginning in a " but got:\n ' + | |
203 self.input[self.cur:]) | |
204 self.cur += 1 # Skip over quote. | |
205 | |
206 begin = self.cur | |
207 while not self.IsDone() and self.input[self.cur] != '"': | |
208 if self.input[self.cur] == '\\': | |
209 self.cur += 1 # Skip over the backslash. | |
210 if self.IsDone(): | |
211 raise GNException("String ends in a backslash in:\n " + | |
212 self.input) | |
213 self.cur += 1 | |
214 | |
215 if self.IsDone(): | |
216 raise GNException('Unterminated string:\n ' + self.input[begin:]) | |
217 | |
218 end = self.cur | |
219 self.cur += 1 # Consume trailing ". | |
220 | |
221 return UnescapeGNString(self.input[begin:end]) | |
222 | |
223 def ParseList(self): | |
224 self.ConsumeWhitespace() | |
225 if self.IsDone(): | |
226 raise GNException('Expected string but got nothing.') | |
Dirk Pranke
2016/01/22 00:34:02
'Expected list' instead?
brettw
2016/01/22 18:12:15
Done.
| |
227 | |
228 # Skip over opening '['. | |
229 if self.input[self.cur] != '[': | |
230 raise GNException("Expected [ for list but got:\n " + | |
231 self.input[self.cur:]) | |
232 self.cur += 1 | |
233 self.ConsumeWhitespace() | |
234 if self.IsDone(): | |
235 raise GNException("Unterminated list:\n " + self.input) | |
236 | |
237 list_result = [] | |
238 previous_had_trailing_comma = True | |
239 while not self.IsDone(): | |
240 if self.input[self.cur] == ']': | |
241 self.cur += 1 # Skip over ']'. | |
242 return list_result | |
243 | |
244 if not previous_had_trailing_comma: | |
245 raise GNException("List items not separated by comma.") | |
246 | |
247 list_result += [ self.ParseAllowTrailing() ] | |
248 self.ConsumeWhitespace() | |
249 if self.IsDone(): | |
250 break | |
251 | |
252 # Consume comma if there is one. | |
253 previous_had_trailing_comma = self.input[self.cur] == ',' | |
254 if previous_had_trailing_comma: | |
255 # Consume comma. | |
256 self.cur += 1 | |
257 self.ConsumeWhitespace() | |
258 | |
259 raise GNException("Unterminated list:\n " + self.input) | |
260 | |
261 def ConstantFollows(self, constant): | |
Dirk Pranke
2016/01/22 00:34:02
nit: _ConstantFollows() if this isn't a public fun
brettw
2016/01/22 18:12:14
Done.
| |
262 """Returns true if the given constant follows immediately at the current | |
263 location in the input. If it does, the text is consumed and the function | |
264 returns true. Otherwise, returns false and the current position is | |
265 unchanged.""" | |
266 end = self.cur + len(constant) | |
267 if end >= len(self.input): | |
268 return False # Not enough room. | |
269 if self.input[self.cur:end] == constant: | |
270 self.cur = end | |
271 return True | |
272 return False | |
OLD | NEW |