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. 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_string): 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_string) 103 return parser.Parse() 104 105 106 def FromGNArgs(input_string): 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_string) 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 preceded 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 ident = '' 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 ident += 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 ident += next_char 252 self.cur += 1 253 next_char = self.input[self.cur] 254 255 return ident 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 352