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): |
| 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 |
| 116 def _IsDigitOrMinus(char): |
| 117 return char in "-0123456789" |
| 118 |
| 119 |
| 120 class 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 |
OLD | NEW |