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