brettw@chromium.org | c976b18 | 2014-04-06 13:35:10 +0900 | [diff] [blame] | 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 | |
brettw | 4c15f78 | 2016-01-23 03:40:03 +0900 | [diff] [blame^] | 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.""" |
brettw@chromium.org | c976b18 | 2014-04-06 13:35:10 +0900 | [diff] [blame] | 9 | |
| 10 | class GNException(Exception): |
| 11 | pass |
| 12 | |
| 13 | |
| 14 | def ToGNString(value, allow_dicts = True): |
| 15 | """Prints the given value to stdout. |
| 16 | |
| 17 | allow_dicts indicates if this function will allow converting dictionaries |
| 18 | to GN scopes. This is only possible at the top level, you can't nest a |
| 19 | GN scope in a list, so this should be set to False for recursive calls.""" |
| 20 | if isinstance(value, str): |
| 21 | if value.find('\n') >= 0: |
| 22 | raise GNException("Trying to print a string with a newline in it.") |
brettw | 4c15f78 | 2016-01-23 03:40:03 +0900 | [diff] [blame^] | 23 | return '"' + \ |
| 24 | value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \ |
| 25 | '"' |
| 26 | |
| 27 | if isinstance(value, bool): |
| 28 | if value: |
| 29 | return "true" |
| 30 | return "false" |
brettw@chromium.org | c976b18 | 2014-04-06 13:35:10 +0900 | [diff] [blame] | 31 | |
| 32 | if isinstance(value, list): |
| 33 | return '[ %s ]' % ', '.join(ToGNString(v) for v in value) |
| 34 | |
| 35 | if isinstance(value, dict): |
| 36 | if not allow_dicts: |
| 37 | raise GNException("Attempting to recursively print a dictionary.") |
| 38 | result = "" |
| 39 | for key in value: |
| 40 | if not isinstance(key, str): |
| 41 | raise GNException("Dictionary key is not a string.") |
| 42 | result += "%s = %s\n" % (key, ToGNString(value[key], False)) |
| 43 | return result |
| 44 | |
| 45 | if isinstance(value, int): |
| 46 | return str(value) |
| 47 | |
| 48 | raise GNException("Unsupported type when printing to GN.") |
brettw | 4c15f78 | 2016-01-23 03:40:03 +0900 | [diff] [blame^] | 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 |