Chromium Code Reviews| 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 |