aboutsummaryrefslogtreecommitdiffhomepage
path: root/gn/gn_helpers.py
blob: fb94d5496c5e555a4d6118198ac4421423c87ddc (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
# Copyright 2014 The Chromium Authors. All rights reserved.
# Use of this source code is governed by a BSD-style license that can be
# found in the LICENSE file.

"""Helper functions useful when writing scripts that integrate with GN.

The main functions are ToGNString and FromGNString which convert between
serialized GN veriables and Python variables.

To use in a random python file in the build:

  import os
  import sys

  sys.path.append(os.path.join(os.path.dirname(__file__),
                               os.pardir, os.pardir, "build"))
  import gn_helpers

Where the sequence of parameters to join is the relative path from your source
file to the build directory."""

class GNException(Exception):
  pass


def ToGNString(value, allow_dicts = True):
  """Returns a stringified GN equivalent of the Python value.

  allow_dicts indicates if this function will allow converting dictionaries
  to GN scopes. This is only possible at the top level, you can't nest a
  GN scope in a list, so this should be set to False for recursive calls."""
  if isinstance(value, basestring):
    if value.find('\n') >= 0:
      raise GNException("Trying to print a string with a newline in it.")
    return '"' + \
        value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
        '"'

  if isinstance(value, unicode):
    return ToGNString(value.encode('utf-8'))

  if isinstance(value, bool):
    if value:
      return "true"
    return "false"

  if isinstance(value, list):
    return '[ %s ]' % ', '.join(ToGNString(v) for v in value)

  if isinstance(value, dict):
    if not allow_dicts:
      raise GNException("Attempting to recursively print a dictionary.")
    result = ""
    for key in sorted(value):
      if not isinstance(key, basestring):
        raise GNException("Dictionary key is not a string.")
      result += "%s = %s\n" % (key, ToGNString(value[key], False))
    return result

  if isinstance(value, int):
    return str(value)

  raise GNException("Unsupported type when printing to GN.")


def FromGNString(input_):
  """Converts the input string from a GN serialized value to Python values.

  For details on supported types see GNValueParser.Parse() below.

  If your GN script did:
    something = [ "file1", "file2" ]
    args = [ "--values=$something" ]
  The command line would look something like:
    --values="[ \"file1\", \"file2\" ]"
  Which when interpreted as a command line gives the value:
    [ "file1", "file2" ]

  You can parse this into a Python list using GN rules with:
    input_values = FromGNValues(options.values)
  Although the Python 'ast' module will parse many forms of such input, it
  will not handle GN escaping properly, nor GN booleans. You should use this
  function instead.


  A NOTE ON STRING HANDLING:

  If you just pass a string on the command line to your Python script, or use
  string interpolation on a string variable, the strings will not be quoted:
    str = "asdf"
    args = [ str, "--value=$str" ]
  Will yield the command line:
    asdf --value=asdf
  The unquoted asdf string will not be valid input to this function, which
  accepts only quoted strings like GN scripts. In such cases, you can just use
  the Python string literal directly.

  The main use cases for this is for other types, in particular lists. When
  using string interpolation on a list (as in the top example) the embedded
  strings will be quoted and escaped according to GN rules so the list can be
  re-parsed to get the same result."""
  parser = GNValueParser(input_)
  return parser.Parse()


def FromGNArgs(input_):
  """Converts a string with a bunch of gn arg assignments into a Python dict.

  Given a whitespace-separated list of

    <ident> = (integer | string | boolean | <list of the former>)

  gn assignments, this returns a Python dict, i.e.:

    FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.

  Only simple types and lists supported; variables, structs, calls
  and other, more complicated things are not.

  This routine is meant to handle only the simple sorts of values that
  arise in parsing --args.
  """
  parser = GNValueParser(input_)
  return parser.ParseArgs()


def UnescapeGNString(value):
  """Given a string with GN escaping, returns the unescaped string.

  Be careful not to feed with input from a Python parsing function like
  'ast' because it will do Python unescaping, which will be incorrect when
  fed into the GN unescaper."""
  result = ''
  i = 0
  while i < len(value):
    if value[i] == '\\':
      if i < len(value) - 1:
        next_char = value[i + 1]
        if next_char in ('$', '"', '\\'):
          # These are the escaped characters GN supports.
          result += next_char
          i += 1
        else:
          # Any other backslash is a literal.
          result += '\\'
    else:
      result += value[i]
    i += 1
  return result


def _IsDigitOrMinus(char):
  return char in "-0123456789"


class GNValueParser(object):
  """Duplicates GN parsing of values and converts to Python types.

  Normally you would use the wrapper function FromGNValue() below.

  If you expect input as a specific type, you can also call one of the Parse*
  functions directly. All functions throw GNException on invalid input. """
  def __init__(self, string):
    self.input = string
    self.cur = 0

  def IsDone(self):
    return self.cur == len(self.input)

  def ConsumeWhitespace(self):
    while not self.IsDone() and self.input[self.cur] in ' \t\n':
      self.cur += 1

  def Parse(self):
    """Converts a string representing a printed GN value to the Python type.

    See additional usage notes on FromGNString above.

    - GN booleans ('true', 'false') will be converted to Python booleans.

    - GN numbers ('123') will be converted to Python numbers.

    - GN strings (double-quoted as in '"asdf"') will be converted to Python
      strings with GN escaping rules. GN string interpolation (embedded
      variables preceeded by $) are not supported and will be returned as
      literals.

    - GN lists ('[1, "asdf", 3]') will be converted to Python lists.

    - GN scopes ('{ ... }') are not supported."""
    result = self._ParseAllowTrailing()
    self.ConsumeWhitespace()
    if not self.IsDone():
      raise GNException("Trailing input after parsing:\n  " +
                        self.input[self.cur:])
    return result

  def ParseArgs(self):
    """Converts a whitespace-separated list of ident=literals to a dict.

    See additional usage notes on FromGNArgs, above.
    """
    d = {}

    self.ConsumeWhitespace()
    while not self.IsDone():
      ident = self._ParseIdent()
      self.ConsumeWhitespace()
      if self.input[self.cur] != '=':
        raise GNException("Unexpected token: " + self.input[self.cur:])
      self.cur += 1
      self.ConsumeWhitespace()
      val = self._ParseAllowTrailing()
      self.ConsumeWhitespace()
      d[ident] = val

    return d

  def _ParseAllowTrailing(self):
    """Internal version of Parse that doesn't check for trailing stuff."""
    self.ConsumeWhitespace()
    if self.IsDone():
      raise GNException("Expected input to parse.")

    next_char = self.input[self.cur]
    if next_char == '[':
      return self.ParseList()
    elif _IsDigitOrMinus(next_char):
      return self.ParseNumber()
    elif next_char == '"':
      return self.ParseString()
    elif self._ConstantFollows('true'):
      return True
    elif self._ConstantFollows('false'):
      return False
    else:
      raise GNException("Unexpected token: " + self.input[self.cur:])

  def _ParseIdent(self):
    id_ = ''

    next_char = self.input[self.cur]
    if not next_char.isalpha() and not next_char=='_':
      raise GNException("Expected an identifier: " + self.input[self.cur:])

    id_ += next_char
    self.cur += 1

    next_char = self.input[self.cur]
    while next_char.isalpha() or next_char.isdigit() or next_char=='_':
      id_ += next_char
      self.cur += 1
      next_char = self.input[self.cur]

    return id_

  def ParseNumber(self):
    self.ConsumeWhitespace()
    if self.IsDone():
      raise GNException('Expected number but got nothing.')

    begin = self.cur

    # The first character can include a negative sign.
    if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
      self.cur += 1
    while not self.IsDone() and self.input[self.cur].isdigit():
      self.cur += 1

    number_string = self.input[begin:self.cur]
    if not len(number_string) or number_string == '-':
      raise GNException("Not a valid number.")
    return int(number_string)

  def ParseString(self):
    self.ConsumeWhitespace()
    if self.IsDone():
      raise GNException('Expected string but got nothing.')

    if self.input[self.cur] != '"':
      raise GNException('Expected string beginning in a " but got:\n  ' +
                        self.input[self.cur:])
    self.cur += 1  # Skip over quote.

    begin = self.cur
    while not self.IsDone() and self.input[self.cur] != '"':
      if self.input[self.cur] == '\\':
        self.cur += 1  # Skip over the backslash.
        if self.IsDone():
          raise GNException("String ends in a backslash in:\n  " +
                            self.input)
      self.cur += 1

    if self.IsDone():
      raise GNException('Unterminated string:\n  ' + self.input[begin:])

    end = self.cur
    self.cur += 1  # Consume trailing ".

    return UnescapeGNString(self.input[begin:end])

  def ParseList(self):
    self.ConsumeWhitespace()
    if self.IsDone():
      raise GNException('Expected list but got nothing.')

    # Skip over opening '['.
    if self.input[self.cur] != '[':
      raise GNException("Expected [ for list but got:\n  " +
                        self.input[self.cur:])
    self.cur += 1
    self.ConsumeWhitespace()
    if self.IsDone():
      raise GNException("Unterminated list:\n  " + self.input)

    list_result = []
    previous_had_trailing_comma = True
    while not self.IsDone():
      if self.input[self.cur] == ']':
        self.cur += 1  # Skip over ']'.
        return list_result

      if not previous_had_trailing_comma:
        raise GNException("List items not separated by comma.")

      list_result += [ self._ParseAllowTrailing() ]
      self.ConsumeWhitespace()
      if self.IsDone():
        break

      # Consume comma if there is one.
      previous_had_trailing_comma = self.input[self.cur] == ','
      if previous_had_trailing_comma:
        # Consume comma.
        self.cur += 1
        self.ConsumeWhitespace()

    raise GNException("Unterminated list:\n  " + self.input)

  def _ConstantFollows(self, constant):
    """Returns true if the given constant follows immediately at the current
    location in the input. If it does, the text is consumed and the function
    returns true. Otherwise, returns false and the current position is
    unchanged."""
    end = self.cur + len(constant)
    if end > len(self.input):
      return False  # Not enough room.
    if self.input[self.cur:end] == constant:
      self.cur = end
      return True
    return False