• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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