• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2#
3# Copyright (c) 2014 The Chromium OS 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"""
8A script to modify dsp.ini config files.
9A dsp.ini config file is represented by an Ini object.
10An Ini object contains one or more Sections.
11Each Section has a name, a list of Ports, and a list of NonPorts.
12"""
13
14import argparse
15import logging
16import os
17import re
18import StringIO
19import sys
20from collections import namedtuple
21
22Parameter = namedtuple('Parameter', ['value', 'comment'])
23
24
25class Port(object):
26  """Class for port definition in ini file.
27
28  Properties:
29    io: "input" or "output".
30    index: an integer for port index.
31    definition: a string for the content after "=" in port definition line.
32    parameter: a Parameter namedtuple which is parsed from definition.
33  """
34  @staticmethod
35  def ParsePortLine(line):
36    """Parses a port definition line in ini file and init a Port object.
37
38    Args:
39      line: A string possibly containing port definition line like
40        "input_0=1;  something".
41
42    Returns:
43      A Port object if input is a valid port definition line. Returns
44      None if input is not a valid port definition line.
45    """
46    result = re.match(r'(input|output)_(\d+)=(.*)', line)
47    if result:
48      parse_values = result.groups()
49      io = parse_values[0]
50      index = int(parse_values[1])
51      definition = parse_values[2]
52      return Port(io, index, definition)
53    else:
54      return None
55
56  def __init__(self, io, index, definition):
57    """Initializes a port.
58
59    Initializes a port with io, index and definition. The definition will be
60    further parsed to Parameter(value, comment) if the format matches
61    "<some value> ; <some comment>".
62
63    Args:
64      io: "input" or "output".
65      index: an integer for port index.
66      definition: a string for the content after "=" in port definition line.
67    """
68    self.io = io
69    self.index = index
70    self.definition = definition
71    result = re.match(r'(\S+)\s+; (.+)', definition)
72    if result:
73      self.parameter = Parameter._make(result.groups())
74    else:
75      self.parameter = None
76
77  def FormatLine(self):
78    """Returns a port definition line which is used in ini file."""
79    line = '%s_%d=' % (self.io, self.index)
80    if self.parameter:
81      line +="{:<8}; {:}".format(self.parameter.value, self.parameter.comment)
82    else:
83      line += self.definition
84    return line
85
86  def _UpdateIndex(self, index):
87    """Updates index of this port.
88
89    Args:
90      index: The new index.
91    """
92    self.index = index
93
94
95class NonPort(object):
96  """Class for non-port definition in ini file.
97
98  Properties:
99    name: A string representing the non-port name.
100    definition: A string representing the non-port definition.
101  """
102  @staticmethod
103  def ParseNonPortLine(line):
104    """Parses a non-port definition line in ini file and init a NonPort object.
105
106    Args:
107      line: A string possibly containing non-port definition line like
108        "library=builtin".
109
110    Returns:
111      A NonPort object if input is a valid non-port definition line. Returns
112      None if input is not a valid non-port definition line.
113    """
114    result = re.match(r'(\w+)=(.*)', line)
115    if result:
116      parse_values = result.groups()
117      name = parse_values[0]
118      definition = parse_values[1]
119      return NonPort(name, definition)
120    else:
121      return None
122
123  def __init__(self, name, definition):
124    """Initializes a NonPort <name>=<definition>.
125
126    Args:
127      name: A string representing the non-port name.
128      definition: A string representing the non-port definition.
129    """
130    self.name = name
131    self.definition = definition
132
133  def FormatLine(self):
134    """Formats a string representation of a NonPort.
135
136    Returns:
137      A string "<name>=<definition>".
138    """
139    line = '%s=%s' % (self.name, self.definition)
140    return line
141
142
143class SectionException(Exception):
144  pass
145
146
147class Section(object):
148  """Class for section definition in ini file.
149
150  Properties:
151   name: Section name.
152   non_ports: A list containing NonPorts of this section.
153   ports: A list containing Ports of this section.
154  """
155  @staticmethod
156  def ParseSectionName(line):
157    """Parses a section name.
158
159    Args:
160      line: A string possibly containing a section name like [drc].
161
162    Returns:
163      Returns parsed section name without '[' and ']' if input matches
164      the syntax [<section name>]. Returns None if not.
165    """
166    result = re.match(r'\[(\w+)\]', line)
167    return result.groups()[0] if result else None
168
169  @staticmethod
170  def ParseLine(line):
171    """Parses a line that belongs to a section.
172
173    Returns:
174      A Port or NonPort object if input line matches the format. Returns None
175      if input line does not match the format of Port nor NonPort.
176    """
177    if not line:
178      return
179    parse_port = Port.ParsePortLine(line)
180    if parse_port:
181      return parse_port
182    parse_non_port = NonPort.ParseNonPortLine(line)
183    if parse_non_port:
184      return parse_non_port
185
186  def __init__(self, name):
187    """Initializes a Section with given name."""
188    self.name = name
189    self.non_ports= []
190    self.ports = []
191
192  def AddLine(self, line):
193    """Adds a line to this Section.
194
195    Args:
196      line: A line to be added to this section. If it matches port or non-port
197      format, a Port or NonPort will be added to this section. Otherwise,
198      this line is ignored.
199    """
200    to_add = Section.ParseLine(line)
201    if not to_add:
202      return
203    if isinstance(to_add, Port):
204      self.AppendPort(to_add)
205      return
206    if isinstance(to_add, NonPort):
207      self.AppendNonPort(to_add)
208      return
209
210  def AppendNonPort(self, non_port):
211    """Appends a NonPort to non_ports.
212
213    Args:
214      non_port: A NonPort object to be appended.
215    """
216    self.non_ports.append(non_port)
217
218  def AppendPort(self, port):
219    """Appends a Port to ports.
220
221    Args:
222      port: A Port object to be appended. The port should be appended
223      in the order of index, so the index of port should equal to the current
224      size of ports list.
225
226    Raises:
227      SectionException if the index of port is not the current size of ports
228      list.
229    """
230    if not port.index == len(self.ports):
231      raise SectionException(
232          'The port with index %r can not be appended to the end of ports'
233          ' of size' % (port.index, len(self.ports)))
234    else:
235      self.ports.append(port)
236
237  def InsertLine(self, line):
238    """Inserts a line to this section.
239
240    Inserts a line containing port or non-port definition to this section.
241    If input line matches Port or NonPort format, the corresponding insert
242    method InsertNonPort or InsertPort will be called. If input line does not
243    match the format, SectionException will be raised.
244
245    Args:
246      line: A line to be inserted. The line should
247
248    Raises:
249      SectionException if input line does not match the format of Port or
250      NonPort.
251    """
252    to_insert = Section.ParseLine(line)
253    if not to_insert:
254      raise SectionException(
255          'The line %s does not match Port or NonPort syntax' % line)
256    if isinstance(to_insert, Port):
257      self.InsertPort(to_insert)
258      return
259    if isinstance(to_insert, NonPort):
260      self.InsertNonPort(to_insert)
261      return
262
263  def InsertNonPort(self, non_port):
264    """Inserts a NonPort to non_ports list.
265
266    Currently there is no ordering for non-port definition. This method just
267    appends non_port to non_ports list.
268
269    Args:
270      non_port: A NonPort object.
271    """
272    self.non_ports.append(non_port)
273
274  def InsertPort(self, port):
275    """Inserts a Port to ports list.
276
277    The index of port should not be greater than the current size of ports.
278    After insertion, the index of each port in ports should be updated to the
279    new index of that port in the ports list.
280    E.g. Before insertion:
281    self.ports=[Port("input", 0, "foo0"),
282                Port("input", 1, "foo1"),
283                Port("output", 2, "foo2")]
284    Now we insert a Port with index 1 by invoking
285    InsertPort(Port("output, 1, "bar")),
286    Then,
287    self.ports=[Port("input", 0, "foo0"),
288                Port("output, 1, "bar"),
289                Port("input", 2, "foo1"),
290                Port("output", 3, "foo2")].
291    Note that the indices of foo1 and foo2 had been shifted by one because a
292    new port was inserted at index 1.
293
294    Args:
295      port: A Port object.
296
297    Raises:
298      SectionException: If the port to be inserted does not have a valid index.
299    """
300    if port.index > len(self.ports):
301      raise SectionException('Inserting port index %d but'
302          ' currently there are only %d ports' % (port.index,
303              len(self.ports)))
304
305    self.ports.insert(port.index, port)
306    self._UpdatePorts()
307
308  def _UpdatePorts(self):
309    """Updates the index property of each Port in ports.
310
311    Updates the index property of each Port in ports so the new index property
312    is the index of that Port in ports list.
313    """
314    for index, port in enumerate(self.ports):
315      port._UpdateIndex(index)
316
317  def Print(self, output):
318    """Prints the section definition to output.
319
320    The format is:
321    [section_name]
322    non_port_name_0=non_port_definition_0
323    non_port_name_1=non_port_definition_1
324    ...
325    port_name_0=port_definition_0
326    port_name_1=port_definition_1
327    ...
328
329    Args:
330      output: A StringIO.StringIO object.
331    """
332    output.write('[%s]\n' % self.name)
333    for non_port in self.non_ports:
334      output.write('%s\n' % non_port.FormatLine())
335    for port in self.ports:
336      output.write('%s\n' % port.FormatLine())
337
338
339class Ini(object):
340  """Class for an ini config file.
341
342  Properties:
343    sections: A dict containing mapping from section name to Section.
344    section_names: A list of section names.
345    file_path: The path of this ini config file.
346  """
347  def __init__(self, input_file):
348    """Initializes an Ini object from input config file.
349
350    Args:
351      input_file: The path to an ini config file.
352    """
353    self.sections = {}
354    self.section_names = []
355    self.file_path = input_file
356    self._ParseFromFile(input_file)
357
358  def _ParseFromFile(self, input_file):
359    """Parses sections in the input config file.
360
361    Reads in the content of the input config file and parses each sections.
362    The parsed sections are stored in sections dict.
363    The names of each section is stored in section_names list.
364
365    Args:
366      input_file: The path to an ini config file.
367    """
368    content = open(input_file, 'r').read()
369    content_lines = content.splitlines()
370    self.sections = {}
371    self.section_names = []
372    current_name = None
373    for line in content_lines:
374      name = Section.ParseSectionName(line)
375      if name:
376        self.section_names.append(name)
377        self.sections[name] = Section(name)
378        current_name = name
379      else:
380        self.sections[current_name].AddLine(line)
381
382  def Print(self, output_file=None):
383    """Prints all sections of this Ini object.
384
385    Args:
386      output_file: The path to write output. If this is not None, writes the
387        output to this path. Otherwise, just print the output to console.
388
389    Returns:
390      A StringIO.StringIO object containing output.
391    """
392    output = StringIO.StringIO()
393    for index, name in enumerate(self.section_names):
394      self.sections[name].Print(output)
395      if index < len(self.section_names) - 1:
396        output.write('\n')
397    if output_file:
398      with open(output_file, 'w') as f:
399        f.write(output.getvalue())
400        output.close()
401    else:
402      print output.getvalue()
403    return output
404
405  def HasSection(self, name):
406    """Checks if this Ini object has a section with certain name.
407
408    Args:
409      name: The name of the section.
410    """
411    return name in self.sections
412
413  def PrintSection(self, name):
414    """Prints a section to console.
415
416    Args:
417      name: The name of the section.
418
419    Returns:
420      A StringIO.StringIO object containing output.
421    """
422    output = StringIO.StringIO()
423    self.sections[name].Print(output)
424    output.write('\n')
425    print output.getvalue()
426    return output
427
428  def InsertLineToSection(self, name, line):
429    """Inserts a line to a section.
430
431    Args:
432      name: The name of the section.
433      line: A line to be inserted.
434    """
435    self.sections[name].InsertLine(line)
436
437
438def prompt(question, binary_answer=True):
439  """Displays the question to the user and wait for input.
440
441  Args:
442    question: The question to be displayed to user.
443    binary_answer: True to expect an yes/no answer from user.
444  Returns:
445    True/False if binary_answer is True. Otherwise, returns a string
446    containing user input to the question.
447  """
448
449  sys.stdout.write(question)
450  answer = raw_input()
451  if binary_answer:
452    answer = answer.lower()
453    if answer in ['y', 'yes']:
454      return True
455    elif answer in ['n', 'no']:
456      return False
457    else:
458      return prompt(question)
459  else:
460    return answer
461
462
463class IniEditorException(Exception):
464  pass
465
466
467class IniEditor(object):
468  """The class for ini file editing command line interface.
469
470  Properties:
471    input_files: The files to be edited. Note that the same editing command
472      can be applied on many config files.
473    args: The result of ArgumentParser.parse_args method. It is an object
474      containing args as attributes.
475  """
476  def __init__(self):
477    self.input_files = []
478    self.args = None
479
480  def Main(self):
481    """The main method of IniEditor.
482
483    Parses the arguments and processes files according to the arguments.
484    """
485    self.ParseArgs()
486    self.ProcessFiles()
487
488  def ParseArgs(self):
489    """Parses the arguments from command line.
490
491    Parses the arguments from command line to determine input_files.
492    Also, checks the arguments are valid.
493
494    Raises:
495      IniEditorException if arguments are not valid.
496    """
497    parser = argparse.ArgumentParser(
498        description=('Edit or show the config files'))
499    parser.add_argument('--input_file', '-i', default=None,
500                        help='Use the specified file as input file. If this '
501                             'is not given, the editor will try to find config '
502                             'files using config_dirs and board.')
503    parser.add_argument('--config_dirs', '-c',
504                        default='~/trunk/src/third_party/adhd/cras-config',
505                        help='Config directory. By default it is '
506                             '~/trunk/src/third_party/adhd/cras-config.')
507    parser.add_argument('--board', '-b', default=None, nargs='*',
508                        help='The boards to apply the changes. Use "all" '
509                             'to apply on all boards. '
510                             'Use --board <board_1> <board_2> to specify more '
511                             'than one boards')
512    parser.add_argument('--section', '-s', default=None,
513                        help='The section to be shown/edited in the ini file.')
514    parser.add_argument('--insert', '-n', default=None,
515                        help='The line to be inserted into the ini file. '
516                             'Must be used with --section.')
517    parser.add_argument('--output-suffix', '-o', default='.new',
518                        help='The output file suffix. Set it to "None" if you '
519                             'want to apply the changes in-place.')
520    self.args = parser.parse_args()
521
522    # If input file is given, just edit this file.
523    if self.args.input_file:
524      self.input_files.append(self.args.input_file)
525    # Otherwise, try to find config files in board directories of config
526    # directory.
527    else:
528      if self.args.config_dirs.startswith('~'):
529        self.args.config_dirs = os.path.join(
530            os.path.expanduser('~'),
531            self.args.config_dirs.split('~/')[1])
532      all_boards = os.walk(self.args.config_dirs).next()[1]
533      # "board" argument must be a valid board name or "all".
534      if (not self.args.board or
535          (self.args.board != ['all'] and
536           not set(self.args.board).issubset(set(all_boards)))):
537        logging.error('Please select a board from %s or use "all".' % (
538            ', '.join(all_boards)))
539        raise IniEditorException('User must specify board if input_file '
540                                 'is not given.')
541      if self.args.board == ['all']:
542        logging.info('Applying on all boards.')
543        boards = all_boards
544      else:
545        boards = self.args.board
546
547      self.input_files = []
548      # Finds dsp.ini files in candidate boards directories.
549      for board in boards:
550        ini_file = os.path.join(self.args.config_dirs, board, 'dsp.ini')
551        if os.path.exists(ini_file):
552          self.input_files.append(ini_file)
553
554    if self.args.insert and not self.args.section:
555      raise IniEditorException('--insert must be used with --section')
556
557  def ProcessFiles(self):
558    """Processes the config files in input_files.
559
560    Showes or edits every selected config file.
561    """
562    for input_file in self.input_files:
563      logging.info('Looking at dsp.ini file at %s', input_file)
564      ini = Ini(input_file)
565      if self.args.insert:
566        self.InsertCommand(ini)
567      else:
568        self.PrintCommand(ini)
569
570  def PrintCommand(self, ini):
571    """Prints this Ini object.
572
573    Prints all sections or a section in input Ini object if
574    args.section is specified and there is such section in this Ini object.
575
576    Args:
577      ini: An Ini object.
578    """
579    if self.args.section:
580      if ini.HasSection(self.args.section):
581        logging.info('Printing section %s.', self.args.section)
582        ini.PrintSection(self.args.section)
583      else:
584        logging.info('There is no section %s in %s',
585                     self.args.section, ini.file_path)
586    else:
587      logging.info('Printing ini content.')
588      ini.Print()
589
590  def InsertCommand(self, ini):
591    """Processes insertion editing on Ini object.
592
593    Inserts args.insert to section named args.section in input Ini object.
594    If input Ini object does not have a section named args.section, this method
595    does not do anything. If the editing is valid, prints the changed section
596    to console. Writes the editied config file to the same path as input path
597    plus a suffix speficied in args.output_suffix. If that suffix is "None",
598    prompts and waits for user to confirm editing in-place.
599
600    Args:
601      ini: An Ini object.
602    """
603    if not ini.HasSection(self.args.section):
604      logging.info('There is no section %s in %s',
605                   self.args.section, ini.file_path)
606      return
607
608    ini.InsertLineToSection(self.args.section, self.args.insert)
609    logging.info('Changed section:')
610    ini.PrintSection(self.args.section)
611
612    if self.args.output_suffix == 'None':
613      answer = prompt(
614          'Writing output file in-place at %s ? [y/n]' % ini.file_path)
615      if not answer:
616        sys.exit('Abort!')
617      output_file = ini.file_path
618    else:
619      output_file = ini.file_path + self.args.output_suffix
620      logging.info('Writing output file to : %s.', output_file)
621    ini.Print(output_file)
622
623
624if __name__ == '__main__':
625  logging.basicConfig(
626     format='%(asctime)s:%(levelname)s:%(filename)s:%(lineno)d:%(message)s',
627     level=logging.DEBUG)
628  IniEditor().Main()
629