1#!/usr/bin/env python 2# -*- coding: utf-8 -*- 3# Copyright 2014 The Chromium Authors. All rights reserved. 4# Use of this source code is governed by a BSD-style license that can be 5# found in the LICENSE file. 6 7"""Helper functions useful when writing scripts that integrate with GN. 8 9The main functions are ToGNString and FromGNString which convert between 10serialized GN variables and Python variables. 11 12To use in a random python file in the build: 13 14 import os 15 import sys 16 17 sys.path.append(os.path.join(os.path.dirname(__file__), 18 os.pardir, os.pardir, "build")) 19 import gn_helpers 20 21Where the sequence of parameters to join is the relative path from your source 22file to the build directory. 23""" 24 25 26class GNException(Exception): 27 pass 28 29 30def ToGNString(value: str, allow_dicts: bool=True) -> str: 31 """Returns a stringified GN equivalent of the Python value. 32 33 allow_dicts indicates if this function will allow converting dictionaries 34 to GN scopes. This is only possible at the top level, you can't nest a 35 GN scope in a list, so this should be set to False for recursive calls.""" 36 if isinstance(value, str): 37 if value.find('\n') >= 0: 38 raise GNException("Trying to print a string with a newline in it.") 39 return '"' + \ 40 value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \ 41 '"' 42 43 if isinstance(value, str): 44 return ToGNString(value.encode('utf-8')) 45 46 if isinstance(value, bool): 47 if value: 48 return "true" 49 return "false" 50 51 if isinstance(value, list): 52 return '[ %s ]' % ', '.join(ToGNString(v) for v in value) 53 54 if isinstance(value, dict): 55 if not allow_dicts: 56 raise GNException("Attempting to recursively print a dictionary.") 57 result = "" 58 for key in sorted(value): 59 if not isinstance(key, str): 60 raise GNException("Dictionary key is not a string.") 61 result += "%s = %s\n" % (key, ToGNString(value[key], False)) 62 return result 63 64 if isinstance(value, int): 65 return str(value) 66 67 raise GNException("Unsupported type when printing to GN.") 68 69 70def FromGNString(input_string: str) -> dict: 71 """Converts the input string from a GN serialized value to Python values. 72 73 For details on supported types see GNValueParser.Parse() below. 74 75 If your GN script did: 76 something = [ "file1", "file2" ] 77 args = [ "--values=$something" ] 78 The command line would look something like: 79 --values="[ \"file1\", \"file2\" ]" 80 Which when interpreted as a command line gives the value: 81 [ "file1", "file2" ] 82 83 You can parse this into a Python list using GN rules with: 84 input_values = FromGNValues(options.values) 85 Although the Python 'ast' module will parse many forms of such input, it 86 will not handle GN escaping properly, nor GN booleans. You should use this 87 function instead. 88 89 90 A NOTE ON STRING HANDLING: 91 92 If you just pass a string on the command line to your Python script, or use 93 string interpolation on a string variable, the strings will not be quoted: 94 str = "asdf" 95 args = [ str, "--value=$str" ] 96 Will yield the command line: 97 asdf --value=asdf 98 The unquoted asdf string will not be valid input to this function, which 99 accepts only quoted strings like GN scripts. In such cases, you can just 100 use the Python string literal directly. 101 102 The main use cases for this is for other types, in particular lists. When 103 using string interpolation on a list (as in the top example) the embedded 104 strings will be quoted and escaped according to GN rules so the list can be 105 re-parsed to get the same result. 106 """ 107 parser = GNValueParser(input_string) 108 return parser.Parse() 109 110 111def FromGNArgs(input_string: str) -> dict: 112 """Converts a string with a bunch of gn arg assignments into a Python dict. 113 114 Given a whitespace-separated list of 115 116 <ident> = (integer | string | boolean | <list of the former>) 117 118 gn assignments, this returns a Python dict, i.e.: 119 120 FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }. 121 122 Only simple types and lists supported; variables, structs, calls 123 and other, more complicated things are not. 124 125 This routine is meant to handle only the simple sorts of values that 126 arise in parsing --args. 127 """ 128 parser = GNValueParser(input_string) 129 return parser.ParseArgs() 130 131 132def UnescapeGNString(value: list) -> str: 133 """Given a string with GN escaping, returns the unescaped string. 134 135 Be careful not to feed with input from a Python parsing function like 136 'ast' because it will do Python unescaping, which will be incorrect when 137 fed into the GN unescaper.""" 138 result = '' 139 i = 0 140 while i < len(value): 141 if value[i] == '\\': 142 if i < len(value) - 1: 143 next_char = value[i + 1] 144 if next_char in ('$', '"', '\\'): 145 # These are the escaped characters GN supports. 146 result += next_char 147 i += 1 148 else: 149 # Any other backslash is a literal. 150 result += '\\' 151 else: 152 result += value[i] 153 i += 1 154 return result 155 156 157def _IsDigitOrMinus(char: str): 158 return char in "-0123456789" 159 160 161class GNValueParser(object): 162 """Duplicates GN parsing of values and converts to Python types. 163 164 Normally you would use the wrapper function FromGNValue() below. 165 166 If you expect input as a specific type, you can also call one of the Parse* 167 functions directly. All functions throw GNException on invalid input. 168 """ 169 170 def __init__(self, string: str): 171 self.input = string 172 self.cur = 0 173 174 def IsDone(self) -> bool: 175 return self.cur == len(self.input) 176 177 def ConsumeWhitespace(self): 178 while not self.IsDone() and self.input[self.cur] in ' \t\n': 179 self.cur += 1 180 181 def Parse(self): 182 """Converts a string representing a printed GN value to the Python type. 183 184 See additional usage notes on FromGNString above. 185 186 - GN booleans ('true', 'false') will be converted to Python booleans. 187 188 - GN numbers ('123') will be converted to Python numbers. 189 190 - GN strings (double-quoted as in '"asdf"') will be converted to Python 191 strings with GN escaping rules. GN string interpolation (embedded 192 variables preceded by $) are not supported and will be returned as 193 literals. 194 195 - GN lists ('[1, "asdf", 3]') will be converted to Python lists. 196 197 - GN scopes ('{ ... }') are not supported. 198 """ 199 result = self._ParseAllowTrailing() 200 self.ConsumeWhitespace() 201 if not self.IsDone(): 202 raise GNException("Trailing input after parsing:\n " + 203 self.input[self.cur:]) 204 return result 205 206 def ParseArgs(self) -> dict: 207 """Converts a whitespace-separated list of ident=literals to a dict. 208 209 See additional usage notes on FromGNArgs, above. 210 """ 211 d = {} 212 213 self.ConsumeWhitespace() 214 while not self.IsDone(): 215 ident = self._ParseIdent() 216 self.ConsumeWhitespace() 217 if self.input[self.cur] != '=': 218 raise GNException("Unexpected token: " + self.input[self.cur:]) 219 self.cur += 1 220 self.ConsumeWhitespace() 221 val = self._ParseAllowTrailing() 222 self.ConsumeWhitespace() 223 d[ident] = val 224 225 return d 226 227 def _ParseAllowTrailing(self): 228 """Internal version of Parse that doesn't check for trailing stuff.""" 229 self.ConsumeWhitespace() 230 if self.IsDone(): 231 raise GNException("Expected input to parse.") 232 233 next_char = self.input[self.cur] 234 if next_char == '[': 235 return self.ParseList() 236 elif _IsDigitOrMinus(next_char): 237 return self.ParseNumber() 238 elif next_char == '"': 239 return self.ParseString() 240 elif self._ConstantFollows('true'): 241 return True 242 elif self._ConstantFollows('false'): 243 return False 244 else: 245 raise GNException("Unexpected token: " + self.input[self.cur:]) 246 247 def _ParseIdent(self) -> str: 248 ident = '' 249 250 next_char = self.input[self.cur] 251 if not next_char.isalpha() and not next_char == '_': 252 raise GNException("Expected an identifier: " + self.input[self.cur:]) 253 254 ident += next_char 255 self.cur += 1 256 257 next_char = self.input[self.cur] 258 while next_char.isalpha() or next_char.isdigit() or next_char == '_': 259 ident += next_char 260 self.cur += 1 261 next_char = self.input[self.cur] 262 263 return ident 264 265 def ParseNumber(self) -> int: 266 self.ConsumeWhitespace() 267 if self.IsDone(): 268 raise GNException('Expected number but got nothing.') 269 270 begin = self.cur 271 272 # The first character can include a negative sign. 273 if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]): 274 self.cur += 1 275 while not self.IsDone() and self.input[self.cur].isdigit(): 276 self.cur += 1 277 278 number_string = self.input[begin:self.cur] 279 if not len(number_string) or number_string == '-': 280 raise GNException("Not a valid number.") 281 return int(number_string) 282 283 def ParseString(self) -> str: 284 self.ConsumeWhitespace() 285 if self.IsDone(): 286 raise GNException('Expected string but got nothing.') 287 288 if self.input[self.cur] != '"': 289 raise GNException('Expected string beginning in a " but got:\n ' + 290 self.input[self.cur:]) 291 self.cur += 1 # Skip over quote. 292 293 begin = self.cur 294 while not self.IsDone() and self.input[self.cur] != '"': 295 if self.input[self.cur] == '\\': 296 self.cur += 1 # Skip over the backslash. 297 if self.IsDone(): 298 raise GNException("String ends in a backslash in:\n " + 299 self.input) 300 self.cur += 1 301 302 if self.IsDone(): 303 raise GNException('Unterminated string:\n ' + self.input[begin:]) 304 305 end = self.cur 306 self.cur += 1 # Consume trailing ". 307 308 return UnescapeGNString(self.input[begin:end]) 309 310 def ParseList(self): 311 self.ConsumeWhitespace() 312 if self.IsDone(): 313 raise GNException('Expected list but got nothing.') 314 315 # Skip over opening '['. 316 if self.input[self.cur] != '[': 317 raise GNException("Expected [ for list but got:\n " + 318 self.input[self.cur:]) 319 self.cur += 1 320 self.ConsumeWhitespace() 321 if self.IsDone(): 322 raise GNException("Unterminated list:\n " + self.input) 323 324 list_result = [] 325 previous_had_trailing_comma = True 326 while not self.IsDone(): 327 if self.input[self.cur] == ']': 328 self.cur += 1 # Skip over ']'. 329 return list_result 330 331 if not previous_had_trailing_comma: 332 raise GNException("List items not separated by comma.") 333 334 list_result += [self._ParseAllowTrailing()] 335 self.ConsumeWhitespace() 336 if self.IsDone(): 337 break 338 339 # Consume comma if there is one. 340 previous_had_trailing_comma = self.input[self.cur] == ',' 341 if previous_had_trailing_comma: 342 # Consume comma. 343 self.cur += 1 344 self.ConsumeWhitespace() 345 346 raise GNException("Unterminated list:\n " + self.input) 347 348 def _ConstantFollows(self, constant) -> bool: 349 """Returns true if the given constant follows immediately at the 350 current location in the input. If it does, the text is consumed and 351 the function returns true. Otherwise, returns false and the current 352 position is unchanged.""" 353 end = self.cur + len(constant) 354 if end > len(self.input): 355 return False # Not enough room. 356 if self.input[self.cur:end] == constant: 357 self.cur = end 358 return True 359 return False 360