blob: 07a7c3e913c1ebeaa9936df9f5fd7f53af62ffe9 [file] [log] [blame]
brettw@chromium.orgc976b182014-04-06 13:35:10 +09001# 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
brettw4c15f782016-01-23 03:40:03 +09005"""Helper functions useful when writing scripts that integrate with GN.
6
7The main functions are ToGNString and FromGNString which convert between
8serialized GN veriables and Python variables."""
brettw@chromium.orgc976b182014-04-06 13:35:10 +09009
10class GNException(Exception):
11 pass
12
13
14def ToGNString(value, allow_dicts = True):
15 """Prints the given value to stdout.
16
17 allow_dicts indicates if this function will allow converting dictionaries
18 to GN scopes. This is only possible at the top level, you can't nest a
19 GN scope in a list, so this should be set to False for recursive calls."""
20 if isinstance(value, str):
21 if value.find('\n') >= 0:
22 raise GNException("Trying to print a string with a newline in it.")
brettw4c15f782016-01-23 03:40:03 +090023 return '"' + \
24 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
25 '"'
26
27 if isinstance(value, bool):
28 if value:
29 return "true"
30 return "false"
brettw@chromium.orgc976b182014-04-06 13:35:10 +090031
32 if isinstance(value, list):
33 return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
34
35 if isinstance(value, dict):
36 if not allow_dicts:
37 raise GNException("Attempting to recursively print a dictionary.")
38 result = ""
39 for key in value:
40 if not isinstance(key, str):
41 raise GNException("Dictionary key is not a string.")
42 result += "%s = %s\n" % (key, ToGNString(value[key], False))
43 return result
44
45 if isinstance(value, int):
46 return str(value)
47
48 raise GNException("Unsupported type when printing to GN.")
brettw4c15f782016-01-23 03:40:03 +090049
50
51def 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
91def UnescapeGNString(value):
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 in ('$', '"', '\\'):
104 # These are the escaped characters GN supports.
105 result += next_char
106 i += 1
107 else:
108 # Any other backslash is a literal.
109 result += '\\'
110 else:
111 result += value[i]
112 i += 1
113 return result
114
115
116def _IsDigitOrMinus(char):
117 return char in "-0123456789"
118
119
120class GNValueParser(object):
121 """Duplicates GN parsing of values and converts to Python types.
122
123 Normally you would use the wrapper function FromGNValue() below.
124
125 If you expect input as a specific type, you can also call one of the Parse*
126 functions directly. All functions throw GNException on invalid input. """
127 def __init__(self, string):
128 self.input = string
129 self.cur = 0
130
131 def IsDone(self):
132 return self.cur == len(self.input)
133
134 def ConsumeWhitespace(self):
135 while not self.IsDone() and self.input[self.cur] in ' \t\n':
136 self.cur += 1
137
138 def Parse(self):
139 """Converts a string representing a printed GN value to the Python type.
140
141 See additional usage notes on FromGNString above.
142
143 - GN booleans ('true', 'false') will be converted to Python booleans.
144
145 - GN numbers ('123') will be converted to Python numbers.
146
147 - GN strings (double-quoted as in '"asdf"') will be converted to Python
148 strings with GN escaping rules. GN string interpolation (embedded
149 variables preceeded by $) are not supported and will be returned as
150 literals.
151
152 - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
153
154 - GN scopes ('{ ... }') are not supported."""
155 result = self._ParseAllowTrailing()
156 self.ConsumeWhitespace()
157 if not self.IsDone():
158 raise GNException("Trailing input after parsing:\n " +
159 self.input[self.cur:])
160 return result
161
162 def _ParseAllowTrailing(self):
163 """Internal version of Parse that doesn't check for trailing stuff."""
164 self.ConsumeWhitespace()
165 if self.IsDone():
166 raise GNException("Expected input to parse.")
167
168 next_char = self.input[self.cur]
169 if next_char == '[':
170 return self.ParseList()
171 elif _IsDigitOrMinus(next_char):
172 return self.ParseNumber()
173 elif next_char == '"':
174 return self.ParseString()
175 elif self._ConstantFollows('true'):
176 return True
177 elif self._ConstantFollows('false'):
178 return False
179 else:
180 raise GNException("Unexpected token: " + self.input[self.cur:])
181
182 def ParseNumber(self):
183 self.ConsumeWhitespace()
184 if self.IsDone():
185 raise GNException('Expected number but got nothing.')
186
187 begin = self.cur
188
189 # The first character can include a negative sign.
190 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
191 self.cur += 1
192 while not self.IsDone() and self.input[self.cur].isdigit():
193 self.cur += 1
194
195 number_string = self.input[begin:self.cur]
196 if not len(number_string) or number_string == '-':
197 raise GNException("Not a valid number.")
198 return int(number_string)
199
200 def ParseString(self):
201 self.ConsumeWhitespace()
202 if self.IsDone():
203 raise GNException('Expected string but got nothing.')
204
205 if self.input[self.cur] != '"':
206 raise GNException('Expected string beginning in a " but got:\n ' +
207 self.input[self.cur:])
208 self.cur += 1 # Skip over quote.
209
210 begin = self.cur
211 while not self.IsDone() and self.input[self.cur] != '"':
212 if self.input[self.cur] == '\\':
213 self.cur += 1 # Skip over the backslash.
214 if self.IsDone():
215 raise GNException("String ends in a backslash in:\n " +
216 self.input)
217 self.cur += 1
218
219 if self.IsDone():
220 raise GNException('Unterminated string:\n ' + self.input[begin:])
221
222 end = self.cur
223 self.cur += 1 # Consume trailing ".
224
225 return UnescapeGNString(self.input[begin:end])
226
227 def ParseList(self):
228 self.ConsumeWhitespace()
229 if self.IsDone():
230 raise GNException('Expected list but got nothing.')
231
232 # Skip over opening '['.
233 if self.input[self.cur] != '[':
234 raise GNException("Expected [ for list but got:\n " +
235 self.input[self.cur:])
236 self.cur += 1
237 self.ConsumeWhitespace()
238 if self.IsDone():
239 raise GNException("Unterminated list:\n " + self.input)
240
241 list_result = []
242 previous_had_trailing_comma = True
243 while not self.IsDone():
244 if self.input[self.cur] == ']':
245 self.cur += 1 # Skip over ']'.
246 return list_result
247
248 if not previous_had_trailing_comma:
249 raise GNException("List items not separated by comma.")
250
251 list_result += [ self._ParseAllowTrailing() ]
252 self.ConsumeWhitespace()
253 if self.IsDone():
254 break
255
256 # Consume comma if there is one.
257 previous_had_trailing_comma = self.input[self.cur] == ','
258 if previous_had_trailing_comma:
259 # Consume comma.
260 self.cur += 1
261 self.ConsumeWhitespace()
262
263 raise GNException("Unterminated list:\n " + self.input)
264
265 def _ConstantFollows(self, constant):
266 """Returns true if the given constant follows immediately at the current
267 location in the input. If it does, the text is consumed and the function
268 returns true. Otherwise, returns false and the current position is
269 unchanged."""
270 end = self.cur + len(constant)
271 if end >= len(self.input):
272 return False # Not enough room.
273 if self.input[self.cur:end] == constant:
274 self.cur = end
275 return True
276 return False