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