blob: 6a61a0877f04e79ad200637515cf8efd245b711d [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
22class GNException(Exception):
23 pass
24
25
26def ToGNString(value, allow_dicts = True):
27 """Prints the given value to stdout.
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, str):
33 if value.find('\n') >= 0:
34 raise GNException("Trying to print a string with a newline in it.")
brettw9dffb542016-01-22 18:40:0335 return '"' + \
36 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
37 '"'
38
hashimoto7bf1d0a2016-04-04 01:01:5139 if isinstance(value, unicode):
40 return ToGNString(value.encode('utf-8'))
41
brettw9dffb542016-01-22 18:40:0342 if isinstance(value, bool):
43 if value:
44 return "true"
45 return "false"
[email protected]530bf7e82014-04-06 04:35:1046
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 value:
55 if not isinstance(key, str):
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.")
brettw9dffb542016-01-22 18:40:0364
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 UnescapeGNString(value):
107 """Given a string with GN escaping, returns the unescaped string.
108
109 Be careful not to feed with input from a Python parsing function like
110 'ast' because it will do Python unescaping, which will be incorrect when
111 fed into the GN unescaper."""
112 result = ''
113 i = 0
114 while i < len(value):
115 if value[i] == '\\':
116 if i < len(value) - 1:
117 next_char = value[i + 1]
118 if next_char in ('$', '"', '\\'):
119 # These are the escaped characters GN supports.
120 result += next_char
121 i += 1
122 else:
123 # Any other backslash is a literal.
124 result += '\\'
125 else:
126 result += value[i]
127 i += 1
128 return result
129
130
131def _IsDigitOrMinus(char):
132 return char in "-0123456789"
133
134
135class GNValueParser(object):
136 """Duplicates GN parsing of values and converts to Python types.
137
138 Normally you would use the wrapper function FromGNValue() below.
139
140 If you expect input as a specific type, you can also call one of the Parse*
141 functions directly. All functions throw GNException on invalid input. """
142 def __init__(self, string):
143 self.input = string
144 self.cur = 0
145
146 def IsDone(self):
147 return self.cur == len(self.input)
148
149 def ConsumeWhitespace(self):
150 while not self.IsDone() and self.input[self.cur] in ' \t\n':
151 self.cur += 1
152
153 def Parse(self):
154 """Converts a string representing a printed GN value to the Python type.
155
156 See additional usage notes on FromGNString above.
157
158 - GN booleans ('true', 'false') will be converted to Python booleans.
159
160 - GN numbers ('123') will be converted to Python numbers.
161
162 - GN strings (double-quoted as in '"asdf"') will be converted to Python
163 strings with GN escaping rules. GN string interpolation (embedded
164 variables preceeded by $) are not supported and will be returned as
165 literals.
166
167 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
168
169 - GN scopes ('{ ... }') are not supported."""
170 result = self._ParseAllowTrailing()
171 self.ConsumeWhitespace()
172 if not self.IsDone():
173 raise GNException("Trailing input after parsing:\n " +
174 self.input[self.cur:])
175 return result
176
177 def _ParseAllowTrailing(self):
178 """Internal version of Parse that doesn't check for trailing stuff."""
179 self.ConsumeWhitespace()
180 if self.IsDone():
181 raise GNException("Expected input to parse.")
182
183 next_char = self.input[self.cur]
184 if next_char == '[':
185 return self.ParseList()
186 elif _IsDigitOrMinus(next_char):
187 return self.ParseNumber()
188 elif next_char == '"':
189 return self.ParseString()
190 elif self._ConstantFollows('true'):
191 return True
192 elif self._ConstantFollows('false'):
193 return False
194 else:
195 raise GNException("Unexpected token: " + self.input[self.cur:])
196
197 def ParseNumber(self):
198 self.ConsumeWhitespace()
199 if self.IsDone():
200 raise GNException('Expected number but got nothing.')
201
202 begin = self.cur
203
204 # The first character can include a negative sign.
205 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
206 self.cur += 1
207 while not self.IsDone() and self.input[self.cur].isdigit():
208 self.cur += 1
209
210 number_string = self.input[begin:self.cur]
211 if not len(number_string) or number_string == '-':
212 raise GNException("Not a valid number.")
213 return int(number_string)
214
215 def ParseString(self):
216 self.ConsumeWhitespace()
217 if self.IsDone():
218 raise GNException('Expected string but got nothing.')
219
220 if self.input[self.cur] != '"':
221 raise GNException('Expected string beginning in a " but got:\n ' +
222 self.input[self.cur:])
223 self.cur += 1 # Skip over quote.
224
225 begin = self.cur
226 while not self.IsDone() and self.input[self.cur] != '"':
227 if self.input[self.cur] == '\\':
228 self.cur += 1 # Skip over the backslash.
229 if self.IsDone():
230 raise GNException("String ends in a backslash in:\n " +
231 self.input)
232 self.cur += 1
233
234 if self.IsDone():
235 raise GNException('Unterminated string:\n ' + self.input[begin:])
236
237 end = self.cur
238 self.cur += 1 # Consume trailing ".
239
240 return UnescapeGNString(self.input[begin:end])
241
242 def ParseList(self):
243 self.ConsumeWhitespace()
244 if self.IsDone():
245 raise GNException('Expected list but got nothing.')
246
247 # Skip over opening '['.
248 if self.input[self.cur] != '[':
249 raise GNException("Expected [ for list but got:\n " +
250 self.input[self.cur:])
251 self.cur += 1
252 self.ConsumeWhitespace()
253 if self.IsDone():
254 raise GNException("Unterminated list:\n " + self.input)
255
256 list_result = []
257 previous_had_trailing_comma = True
258 while not self.IsDone():
259 if self.input[self.cur] == ']':
260 self.cur += 1 # Skip over ']'.
261 return list_result
262
263 if not previous_had_trailing_comma:
264 raise GNException("List items not separated by comma.")
265
266 list_result += [ self._ParseAllowTrailing() ]
267 self.ConsumeWhitespace()
268 if self.IsDone():
269 break
270
271 # Consume comma if there is one.
272 previous_had_trailing_comma = self.input[self.cur] == ','
273 if previous_had_trailing_comma:
274 # Consume comma.
275 self.cur += 1
276 self.ConsumeWhitespace()
277
278 raise GNException("Unterminated list:\n " + self.input)
279
280 def _ConstantFollows(self, constant):
281 """Returns true if the given constant follows immediately at the current
282 location in the input. If it does, the text is consumed and the function
283 returns true. Otherwise, returns false and the current position is
284 unchanged."""
285 end = self.cur + len(constant)
dprankee031ec22016-04-02 00:17:34286 if end > len(self.input):
brettw9dffb542016-01-22 18:40:03287 return False # Not enough room.
288 if self.input[self.cur:end] == constant:
289 self.cur = end
290 return True
291 return False