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
5"""Helper functions useful when writing scripts that integrate with GN.
6
7The main functions are ToGNString and FromGNString which convert between
8serialized GN veriables and Python variables.
9
10To use in a random python file in the build:
11
12  import os
13  import sys
14
15  sys.path.append(os.path.join(os.path.dirname(__file__),
16                               os.pardir, os.pardir, "build"))
17  import gn_helpers
18
19Where the sequence of parameters to join is the relative path from your source
20file to the build directory."""
21
22class GNException(Exception):
23  pass
24
25
26def ToGNString(value, allow_dicts = True):
27  """Returns a stringified GN equivalent of the Python value.
28
29  allow_dicts indicates if this function will allow converting dictionaries
30  to GN scopes. This is only possible at the top level, you can't nest a
31  GN scope in a list, so this should be set to False for recursive calls."""
32  if isinstance(value, basestring):
33    if value.find('\n') >= 0:
34      raise GNException("Trying to print a string with a newline in it.")
35    return '"' + \
36        value.replace('\\', '\\\\').replace('"', '\\"').replace('$', '\\$') + \
37        '"'
38
39  if isinstance(value, unicode):
40    return ToGNString(value.encode('utf-8'))
41
42  if isinstance(value, bool):
43    if value:
44      return "true"
45    return "false"
46
47  if isinstance(value, list):
48    return '[ %s ]' % ', '.join(ToGNString(v) for v in value)
49
50  if isinstance(value, dict):
51    if not allow_dicts:
52      raise GNException("Attempting to recursively print a dictionary.")
53    result = ""
54    for key in sorted(value):
55      if not isinstance(key, basestring):
56        raise GNException("Dictionary key is not a string.")
57      result += "%s = %s\n" % (key, ToGNString(value[key], False))
58    return result
59
60  if isinstance(value, int):
61    return str(value)
62
63  raise GNException("Unsupported type when printing to GN.")
64
65
66def FromGNString(input_string):
67  """Converts the input string from a GN serialized value to Python values.
68
69  For details on supported types see GNValueParser.Parse() below.
70
71  If your GN script did:
72    something = [ "file1", "file2" ]
73    args = [ "--values=$something" ]
74  The command line would look something like:
75    --values="[ \"file1\", \"file2\" ]"
76  Which when interpreted as a command line gives the value:
77    [ "file1", "file2" ]
78
79  You can parse this into a Python list using GN rules with:
80    input_values = FromGNValues(options.values)
81  Although the Python 'ast' module will parse many forms of such input, it
82  will not handle GN escaping properly, nor GN booleans. You should use this
83  function instead.
84
85
86  A NOTE ON STRING HANDLING:
87
88  If you just pass a string on the command line to your Python script, or use
89  string interpolation on a string variable, the strings will not be quoted:
90    str = "asdf"
91    args = [ str, "--value=$str" ]
92  Will yield the command line:
93    asdf --value=asdf
94  The unquoted asdf string will not be valid input to this function, which
95  accepts only quoted strings like GN scripts. In such cases, you can just use
96  the Python string literal directly.
97
98  The main use cases for this is for other types, in particular lists. When
99  using string interpolation on a list (as in the top example) the embedded
100  strings will be quoted and escaped according to GN rules so the list can be
101  re-parsed to get the same result."""
102  parser = GNValueParser(input_string)
103  return parser.Parse()
104
105
106def FromGNArgs(input_string):
107  """Converts a string with a bunch of gn arg assignments into a Python dict.
108
109  Given a whitespace-separated list of
110
111    <ident> = (integer | string | boolean | <list of the former>)
112
113  gn assignments, this returns a Python dict, i.e.:
114
115    FromGNArgs("foo=true\nbar=1\n") -> { 'foo': True, 'bar': 1 }.
116
117  Only simple types and lists supported; variables, structs, calls
118  and other, more complicated things are not.
119
120  This routine is meant to handle only the simple sorts of values that
121  arise in parsing --args.
122  """
123  parser = GNValueParser(input_string)
124  return parser.ParseArgs()
125
126
127def UnescapeGNString(value):
128  """Given a string with GN escaping, returns the unescaped string.
129
130  Be careful not to feed with input from a Python parsing function like
131  'ast' because it will do Python unescaping, which will be incorrect when
132  fed into the GN unescaper."""
133  result = ''
134  i = 0
135  while i < len(value):
136    if value[i] == '\\':
137      if i < len(value) - 1:
138        next_char = value[i + 1]
139        if next_char in ('$', '"', '\\'):
140          # These are the escaped characters GN supports.
141          result += next_char
142          i += 1
143        else:
144          # Any other backslash is a literal.
145          result += '\\'
146    else:
147      result += value[i]
148    i += 1
149  return result
150
151
152def _IsDigitOrMinus(char):
153  return char in "-0123456789"
154
155
156class GNValueParser(object):
157  """Duplicates GN parsing of values and converts to Python types.
158
159  Normally you would use the wrapper function FromGNValue() below.
160
161  If you expect input as a specific type, you can also call one of the Parse*
162  functions directly. All functions throw GNException on invalid input. """
163  def __init__(self, string):
164    self.input = string
165    self.cur = 0
166
167  def IsDone(self):
168    return self.cur == len(self.input)
169
170  def ConsumeWhitespace(self):
171    while not self.IsDone() and self.input[self.cur] in ' \t\n':
172      self.cur += 1
173
174  def Parse(self):
175    """Converts a string representing a printed GN value to the Python type.
176
177    See additional usage notes on FromGNString above.
178
179    - GN booleans ('true', 'false') will be converted to Python booleans.
180
181    - GN numbers ('123') will be converted to Python numbers.
182
183    - GN strings (double-quoted as in '"asdf"') will be converted to Python
184      strings with GN escaping rules. GN string interpolation (embedded
185      variables preceded by $) are not supported and will be returned as
186      literals.
187
188    - GN lists ('[1, "asdf", 3]') will be converted to Python lists.
189
190    - GN scopes ('{ ... }') are not supported."""
191    result = self._ParseAllowTrailing()
192    self.ConsumeWhitespace()
193    if not self.IsDone():
194      raise GNException("Trailing input after parsing:\n  " +
195                        self.input[self.cur:])
196    return result
197
198  def ParseArgs(self):
199    """Converts a whitespace-separated list of ident=literals to a dict.
200
201    See additional usage notes on FromGNArgs, above.
202    """
203    d = {}
204
205    self.ConsumeWhitespace()
206    while not self.IsDone():
207      ident = self._ParseIdent()
208      self.ConsumeWhitespace()
209      if self.input[self.cur] != '=':
210        raise GNException("Unexpected token: " + self.input[self.cur:])
211      self.cur += 1
212      self.ConsumeWhitespace()
213      val = self._ParseAllowTrailing()
214      self.ConsumeWhitespace()
215      d[ident] = val
216
217    return d
218
219  def _ParseAllowTrailing(self):
220    """Internal version of Parse that doesn't check for trailing stuff."""
221    self.ConsumeWhitespace()
222    if self.IsDone():
223      raise GNException("Expected input to parse.")
224
225    next_char = self.input[self.cur]
226    if next_char == '[':
227      return self.ParseList()
228    elif _IsDigitOrMinus(next_char):
229      return self.ParseNumber()
230    elif next_char == '"':
231      return self.ParseString()
232    elif self._ConstantFollows('true'):
233      return True
234    elif self._ConstantFollows('false'):
235      return False
236    else:
237      raise GNException("Unexpected token: " + self.input[self.cur:])
238
239  def _ParseIdent(self):
240    ident = ''
241
242    next_char = self.input[self.cur]
243    if not next_char.isalpha() and not next_char=='_':
244      raise GNException("Expected an identifier: " + self.input[self.cur:])
245
246    ident += next_char
247    self.cur += 1
248
249    next_char = self.input[self.cur]
250    while next_char.isalpha() or next_char.isdigit() or next_char=='_':
251      ident += next_char
252      self.cur += 1
253      next_char = self.input[self.cur]
254
255    return ident
256
257  def ParseNumber(self):
258    self.ConsumeWhitespace()
259    if self.IsDone():
260      raise GNException('Expected number but got nothing.')
261
262    begin = self.cur
263
264    # The first character can include a negative sign.
265    if not self.IsDone() and _IsDigitOrMinus(self.input[self.cur]):
266      self.cur += 1
267    while not self.IsDone() and self.input[self.cur].isdigit():
268      self.cur += 1
269
270    number_string = self.input[begin:self.cur]
271    if not len(number_string) or number_string == '-':
272      raise GNException("Not a valid number.")
273    return int(number_string)
274
275  def ParseString(self):
276    self.ConsumeWhitespace()
277    if self.IsDone():
278      raise GNException('Expected string but got nothing.')
279
280    if self.input[self.cur] != '"':
281      raise GNException('Expected string beginning in a " but got:\n  ' +
282                        self.input[self.cur:])
283    self.cur += 1  # Skip over quote.
284
285    begin = self.cur
286    while not self.IsDone() and self.input[self.cur] != '"':
287      if self.input[self.cur] == '\\':
288        self.cur += 1  # Skip over the backslash.
289        if self.IsDone():
290          raise GNException("String ends in a backslash in:\n  " +
291                            self.input)
292      self.cur += 1
293
294    if self.IsDone():
295      raise GNException('Unterminated string:\n  ' + self.input[begin:])
296
297    end = self.cur
298    self.cur += 1  # Consume trailing ".
299
300    return UnescapeGNString(self.input[begin:end])
301
302  def ParseList(self):
303    self.ConsumeWhitespace()
304    if self.IsDone():
305      raise GNException('Expected list but got nothing.')
306
307    # Skip over opening '['.
308    if self.input[self.cur] != '[':
309      raise GNException("Expected [ for list but got:\n  " +
310                        self.input[self.cur:])
311    self.cur += 1
312    self.ConsumeWhitespace()
313    if self.IsDone():
314      raise GNException("Unterminated list:\n  " + self.input)
315
316    list_result = []
317    previous_had_trailing_comma = True
318    while not self.IsDone():
319      if self.input[self.cur] == ']':
320        self.cur += 1  # Skip over ']'.
321        return list_result
322
323      if not previous_had_trailing_comma:
324        raise GNException("List items not separated by comma.")
325
326      list_result += [ self._ParseAllowTrailing() ]
327      self.ConsumeWhitespace()
328      if self.IsDone():
329        break
330
331      # Consume comma if there is one.
332      previous_had_trailing_comma = self.input[self.cur] == ','
333      if previous_had_trailing_comma:
334        # Consume comma.
335        self.cur += 1
336        self.ConsumeWhitespace()
337
338    raise GNException("Unterminated list:\n  " + self.input)
339
340  def _ConstantFollows(self, constant):
341    """Returns true if the given constant follows immediately at the current
342    location in the input. If it does, the text is consumed and the function
343    returns true. Otherwise, returns false and the current position is
344    unchanged."""
345    end = self.cur + len(constant)
346    if end > len(self.input):
347      return False  # Not enough room.
348    if self.input[self.cur:end] == constant:
349      self.cur = end
350      return True
351    return False
352