1#!/usr/bin/python 2# 3# Copyright 2010 Google Inc. All Rights Reserved. 4# 5# Licensed under the Apache License, Version 2.0 (the "License"); 6# you may not use this file except in compliance with the License. 7# You may obtain a copy of the License at 8# 9# http://www.apache.org/licenses/LICENSE-2.0 10# 11# Unless required by applicable law or agreed to in writing, software 12# distributed under the License is distributed on an "AS IS" BASIS, 13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 14# implied. See the License for the specific language governing 15# permissions and limitations under the License. 16 17"""Template based text parser. 18 19This module implements a parser, intended to be used for converting 20human readable text, such as command output from a router CLI, into 21a list of records, containing values extracted from the input text. 22 23A simple template language is used to describe a state machine to 24parse a specific type of text input, returning a record of values 25for each input entity. 26 27Import it to ~/file/client/common_lib/cros/. 28""" 29from __future__ import absolute_import 30from __future__ import division 31from __future__ import print_function 32 33__version__ = '0.3.2' 34 35import getopt 36import inspect 37import re 38import string 39import sys 40 41 42class Error(Exception): 43 """Base class for errors.""" 44 45 46class Usage(Exception): 47 """Error in command line execution.""" 48 49 50class TextFSMError(Error): 51 """Error in the FSM state execution.""" 52 53 54class TextFSMTemplateError(Error): 55 """Errors while parsing templates.""" 56 57 58# The below exceptions are internal state change triggers 59# and not used as Errors. 60class FSMAction(Exception): 61 """Base class for actions raised with the FSM.""" 62 63 64class SkipRecord(FSMAction): 65 """Indicate a record is to be skipped.""" 66 67 68class SkipValue(FSMAction): 69 """Indicate a value is to be skipped.""" 70 71 72class TextFSMOptions(object): 73 """Class containing all valid TextFSMValue options. 74 75 Each nested class here represents a TextFSM option. The format 76 is "option<name>". 77 Each class may override any of the methods inside the OptionBase class. 78 79 A user of this module can extend options by subclassing 80 TextFSMOptionsBase, adding the new option class(es), then passing 81 that new class to the TextFSM constructor with the 'option_class' 82 argument. 83 """ 84 85 class OptionBase(object): 86 """Factory methods for option class. 87 88 Attributes: 89 value: A TextFSMValue, the parent Value. 90 """ 91 92 def __init__(self, value): 93 self.value = value 94 95 @property 96 def name(self): 97 return self.__class__.__name__.replace('option', '') 98 99 def OnCreateOptions(self): 100 """Called after all options have been parsed for a Value.""" 101 102 def OnClearVar(self): 103 """Called when value has been cleared.""" 104 105 def OnClearAllVar(self): 106 """Called when a value has clearalled.""" 107 108 def OnAssignVar(self): 109 """Called when a matched value is being assigned.""" 110 111 def OnGetValue(self): 112 """Called when the value name is being requested.""" 113 114 def OnSaveRecord(self): 115 """Called just prior to a record being committed.""" 116 117 @classmethod 118 def ValidOptions(cls): 119 """Returns a list of valid option names.""" 120 valid_options = [] 121 for obj_name in dir(cls): 122 obj = getattr(cls, obj_name) 123 if inspect.isclass(obj) and issubclass(obj, cls.OptionBase): 124 valid_options.append(obj_name) 125 return valid_options 126 127 @classmethod 128 def GetOption(cls, name): 129 """Returns the class of the requested option name.""" 130 return getattr(cls, name) 131 132 class Required(OptionBase): 133 """The Value must be non-empty for the row to be recorded.""" 134 135 def OnSaveRecord(self): 136 if not self.value.value: 137 raise SkipRecord 138 139 class Filldown(OptionBase): 140 """Value defaults to the previous line's value.""" 141 142 def OnCreateOptions(self): 143 self._myvar = None 144 145 def OnAssignVar(self): 146 self._myvar = self.value.value 147 148 def OnClearVar(self): 149 self.value.value = self._myvar 150 151 def OnClearAllVar(self): 152 self._myvar = None 153 154 class Fillup(OptionBase): 155 """Like Filldown, but upwards until it finds a non-empty entry.""" 156 157 def OnAssignVar(self): 158 # If value is set, copy up the results table, until we 159 # see a set item. 160 if self.value.value: 161 # Get index of relevant result column. 162 value_idx = self.value.fsm.values.index(self.value) 163 # Go up the list from the end until we see a filled value. 164 # pylint: disable=protected-access 165 for result in reversed(self.value.fsm._result): 166 if result[value_idx]: 167 # Stop when a record has this column already. 168 break 169 # Otherwise set the column value. 170 result[value_idx] = self.value.value 171 172 class Key(OptionBase): 173 """Value constitutes part of the Key of the record.""" 174 175 class List(OptionBase): 176 """Value takes the form of a list.""" 177 178 def OnCreateOptions(self): 179 self.OnClearAllVar() 180 181 def OnAssignVar(self): 182 self._value.append(self.value.value) 183 184 def OnClearVar(self): 185 if 'Filldown' not in self.value.OptionNames(): 186 self._value = [] 187 188 def OnClearAllVar(self): 189 self._value = [] 190 191 def OnSaveRecord(self): 192 self.value.value = list(self._value) 193 194 195class TextFSMValue(object): 196 """A TextFSM value. 197 198 A value has syntax like: 199 200 'Value Filldown,Required helloworld (.*)' 201 202 Where 'Value' is a keyword. 203 'Filldown' and 'Required' are options. 204 'helloworld' is the value name. 205 '(.*) is the regular expression to match in the input data. 206 207 Attributes: 208 max_name_len: (int), maximum character length os a variable name. 209 name: (str), Name of the value. 210 options: (list), A list of current Value Options. 211 regex: (str), Regex which the value is matched on. 212 template: (str), regexp with named groups added. 213 fsm: A TextFSMBase(), the containing FSM. 214 value: (str), the current value. 215 """ 216 # The class which contains valid options. 217 218 def __init__(self, fsm=None, max_name_len=48, options_class=None): 219 """Initialise a new TextFSMValue.""" 220 self.max_name_len = max_name_len 221 self.name = None 222 self.options = [] 223 self.regex = None 224 self.value = None 225 self.fsm = fsm 226 self._options_cls = options_class 227 228 def AssignVar(self, value): 229 """Assign a value to this Value.""" 230 self.value = value 231 # Call OnAssignVar on options. 232 _ = [option.OnAssignVar() for option in self.options] 233 234 def ClearVar(self): 235 """Clear this Value.""" 236 self.value = None 237 # Call OnClearVar on options. 238 _ = [option.OnClearVar() for option in self.options] 239 240 def ClearAllVar(self): 241 """Clear this Value.""" 242 self.value = None 243 # Call OnClearAllVar on options. 244 _ = [option.OnClearAllVar() for option in self.options] 245 246 def Header(self): 247 """Fetch the header name of this Value.""" 248 # Call OnGetValue on options. 249 _ = [option.OnGetValue() for option in self.options] 250 return self.name 251 252 def OptionNames(self): 253 """Returns a list of option names for this Value.""" 254 return [option.name for option in self.options] 255 256 def Parse(self, value): 257 """Parse a 'Value' declaration. 258 259 Args: 260 value: String line from a template file, must begin with 'Value '. 261 262 Raises: 263 TextFSMTemplateError: Value declaration contains an error. 264 265 """ 266 267 value_line = value.split(' ') 268 if len(value_line) < 3: 269 raise TextFSMTemplateError('Expect at least 3 tokens on line.') 270 271 if not value_line[2].startswith('('): 272 # Options are present 273 options = value_line[1] 274 for option in options.split(','): 275 self._AddOption(option) 276 # Call option OnCreateOptions callbacks 277 _ = [option.OnCreateOptions() for option in self.options] 278 279 self.name = value_line[2] 280 self.regex = ' '.join(value_line[3:]) 281 else: 282 # There were no valid options, so there are no options. 283 # Treat this argument as the name. 284 self.name = value_line[1] 285 self.regex = ' '.join(value_line[2:]) 286 287 if len(self.name) > self.max_name_len: 288 raise TextFSMTemplateError( 289 "Invalid Value name '%s' or name too long." % self.name) 290 291 if (not re.match(r'^\(.*\)$', self.regex) or 292 self.regex.count('(') != self.regex.count(')')): 293 raise TextFSMTemplateError( 294 "Value '%s' must be contained within a '()' pair." % self.regex) 295 296 self.template = re.sub(r'^\(', '(?P<%s>' % self.name, self.regex) 297 298 def _AddOption(self, name): 299 """Add an option to this Value. 300 301 Args: 302 name: (str), the name of the Option to add. 303 304 Raises: 305 TextFSMTemplateError: If option is already present or 306 the option does not exist. 307 """ 308 309 # Check for duplicate option declaration 310 if name in [option.name for option in self.options]: 311 raise TextFSMTemplateError('Duplicate option "%s"' % name) 312 313 # Create the option object 314 try: 315 option = self._options_cls.GetOption(name)(self) 316 except AttributeError: 317 raise TextFSMTemplateError('Unknown option "%s"' % name) 318 319 self.options.append(option) 320 321 def OnSaveRecord(self): 322 """Called just prior to a record being committed.""" 323 _ = [option.OnSaveRecord() for option in self.options] 324 325 def __str__(self): 326 """Prints out the FSM Value, mimic the input file.""" 327 328 if self.options: 329 return 'Value %s %s %s' % ( 330 ','.join(self.OptionNames()), 331 self.name, 332 self.regex) 333 else: 334 return 'Value %s %s' % (self.name, self.regex) 335 336 337class CopyableRegexObject(object): 338 """Like a re.RegexObject, but can be copied.""" 339 # pylint: disable=C6409 340 341 def __init__(self, pattern): 342 self.pattern = pattern 343 self.regex = re.compile(pattern) 344 345 def match(self, *args, **kwargs): 346 return self.regex.match(*args, **kwargs) 347 348 def sub(self, *args, **kwargs): 349 return self.regex.sub(*args, **kwargs) 350 351 def __copy__(self): 352 return CopyableRegexObject(self.pattern) 353 354 def __deepcopy__(self, unused_memo): 355 return self.__copy__() 356 357 358class TextFSMRule(object): 359 """A rule in each FSM state. 360 361 A value has syntax like: 362 363 ^<regexp> -> Next.Record State2 364 365 Where '<regexp>' is a regular expression. 366 'Next' is a Line operator. 367 'Record' is a Record operator. 368 'State2' is the next State. 369 370 Attributes: 371 match: Regex to match this rule. 372 regex: match after template substitution. 373 line_op: Operator on input line on match. 374 record_op: Operator on output record on match. 375 new_state: Label to jump to on action 376 regex_obj: Compiled regex for which the rule matches. 377 line_num: Integer row number of Value. 378 """ 379 # Implicit default is '(regexp) -> Next.NoRecord' 380 MATCH_ACTION = re.compile(r'(?P<match>.*)(\s->(?P<action>.*))') 381 382 # The structure to the right of the '->'. 383 LINE_OP = ('Continue', 'Next', 'Error') 384 RECORD_OP = ('Clear', 'Clearall', 'Record', 'NoRecord') 385 386 # Line operators. 387 LINE_OP_RE = '(?P<ln_op>%s)' % '|'.join(LINE_OP) 388 # Record operators. 389 RECORD_OP_RE = '(?P<rec_op>%s)' % '|'.join(RECORD_OP) 390 # Line operator with optional record operator. 391 OPERATOR_RE = r'(%s(\.%s)?)' % (LINE_OP_RE, RECORD_OP_RE) 392 # New State or 'Error' string. 393 NEWSTATE_RE = r'(?P<new_state>\w+|\".*\")' 394 395 # Compound operator (line and record) with optional new state. 396 ACTION_RE = re.compile(r'\s+%s(\s+%s)?$' % (OPERATOR_RE, NEWSTATE_RE)) 397 # Record operator with optional new state. 398 ACTION2_RE = re.compile(r'\s+%s(\s+%s)?$' % (RECORD_OP_RE, NEWSTATE_RE)) 399 # Default operators with optional new state. 400 ACTION3_RE = re.compile(r'(\s+%s)?$' % (NEWSTATE_RE)) 401 402 def __init__(self, line, line_num=-1, var_map=None): 403 """Initialise a new rule object. 404 405 Args: 406 line: (str), a template rule line to parse. 407 line_num: (int), Optional line reference included in error reporting. 408 var_map: Map for template (${var}) substitutions. 409 410 Raises: 411 TextFSMTemplateError: If 'line' is not a valid format for a Value entry. 412 """ 413 self.match = '' 414 self.regex = '' 415 self.regex_obj = None 416 self.line_op = '' # Equivalent to 'Next'. 417 self.record_op = '' # Equivalent to 'NoRecord'. 418 self.new_state = '' # Equivalent to current state. 419 self.line_num = line_num 420 421 line = line.strip() 422 if not line: 423 raise TextFSMTemplateError('Null data in FSMRule. Line: %s' 424 % self.line_num) 425 426 # Is there '->' action present. 427 match_action = self.MATCH_ACTION.match(line) 428 if match_action: 429 self.match = match_action.group('match') 430 else: 431 self.match = line 432 433 # Replace ${varname} entries. 434 self.regex = self.match 435 if var_map: 436 try: 437 self.regex = string.Template(self.match).substitute(var_map) 438 except (ValueError, KeyError): 439 raise TextFSMTemplateError( 440 "Duplicate or invalid variable substitution: '%s'. Line: %s." % 441 (self.match, self.line_num)) 442 443 try: 444 # Work around a regression in Python 2.6 that makes RE Objects uncopyable. 445 self.regex_obj = CopyableRegexObject(self.regex) 446 except re.error: 447 raise TextFSMTemplateError( 448 "Invalid regular expression: '%s'. Line: %s." % 449 (self.regex, self.line_num)) 450 451 # No '->' present, so done. 452 if not match_action: 453 return 454 455 # Attempt to match line.record operation. 456 action_re = self.ACTION_RE.match(match_action.group('action')) 457 if not action_re: 458 # Attempt to match record operation. 459 action_re = self.ACTION2_RE.match(match_action.group('action')) 460 if not action_re: 461 # Math implicit defaults with an optional new state. 462 action_re = self.ACTION3_RE.match(match_action.group('action')) 463 if not action_re: 464 # Last attempt, match an optional new state only. 465 raise TextFSMTemplateError("Badly formatted rule '%s'. Line: %s." % 466 (line, self.line_num)) 467 468 # We have an Line operator. 469 if 'ln_op' in action_re.groupdict() and action_re.group('ln_op'): 470 self.line_op = action_re.group('ln_op') 471 472 # We have a record operator. 473 if 'rec_op' in action_re.groupdict() and action_re.group('rec_op'): 474 self.record_op = action_re.group('rec_op') 475 476 # A new state was specified. 477 if 'new_state' in action_re.groupdict() and action_re.group('new_state'): 478 self.new_state = action_re.group('new_state') 479 480 # Only 'Next' (or implicit 'Next') line operator can have a new_state. 481 # But we allow error to have one as a warning message so we are left 482 # checking that Continue does not. 483 if self.line_op == 'Continue' and self.new_state: 484 raise TextFSMTemplateError( 485 "Action '%s' with new state %s specified. Line: %s." 486 % (self.line_op, self.new_state, self.line_num)) 487 488 # Check that an error message is present only with the 'Error' operator. 489 if self.line_op != 'Error' and self.new_state: 490 if not re.match(r'\w+', self.new_state): 491 raise TextFSMTemplateError( 492 'Alphanumeric characters only in state names. Line: %s.' 493 % (self.line_num)) 494 495 def __str__(self): 496 """Prints out the FSM Rule, mimic the input file.""" 497 498 operation = '' 499 if self.line_op and self.record_op: 500 operation = '.' 501 502 operation = '%s%s%s' % (self.line_op, operation, self.record_op) 503 504 if operation and self.new_state: 505 new_state = ' ' + self.new_state 506 else: 507 new_state = self.new_state 508 509 # Print with implicit defaults. 510 if not (operation or new_state): 511 return ' %s' % self.match 512 513 # Non defaults. 514 return ' %s -> %s%s' % (self.match, operation, new_state) 515 516 517class TextFSM(object): 518 """Parses template and creates Finite State Machine (FSM). 519 520 Attributes: 521 states: (str), Dictionary of FSMState objects. 522 values: (str), List of FSMVariables. 523 value_map: (map), For substituting values for names in the expressions. 524 header: Ordered list of values. 525 state_list: Ordered list of valid states. 526 """ 527 # Variable and State name length. 528 MAX_NAME_LEN = 48 529 comment_regex = re.compile(r'^\s*#') 530 state_name_re = re.compile(r'^(\w+)$') 531 _DEFAULT_OPTIONS = TextFSMOptions 532 533 def __init__(self, template, options_class=_DEFAULT_OPTIONS): 534 """Initialises and also parses the template file.""" 535 536 self._options_cls = options_class 537 self.states = {} 538 # Track order of state definitions. 539 self.state_list = [] 540 self.values = [] 541 self.value_map = {} 542 # Track where we are for error reporting. 543 self._line_num = 0 544 # Run FSM in this state 545 self._cur_state = None 546 # Name of the current state. 547 self._cur_state_name = None 548 549 # Read and parse FSM definition. 550 # Restore the file pointer once done. 551 try: 552 self._Parse(template) 553 finally: 554 template.seek(0) 555 556 # Initialise starting data. 557 self.Reset() 558 559 def __str__(self): 560 """Returns the FSM template, mimic the input file.""" 561 562 result = '\n'.join([str(value) for value in self.values]) 563 result += '\n' 564 565 for state in self.state_list: 566 result += '\n%s\n' % state 567 state_rules = '\n'.join([str(rule) for rule in self.states[state]]) 568 if state_rules: 569 result += state_rules + '\n' 570 571 return result 572 573 def Reset(self): 574 """Preserves FSM but resets starting state and current record.""" 575 576 # Current state is Start state. 577 self._cur_state = self.states['Start'] 578 self._cur_state_name = 'Start' 579 580 # Clear table of results and current record. 581 self._result = [] 582 self._ClearAllRecord() 583 584 @property 585 def header(self): 586 """Returns header.""" 587 return self._GetHeader() 588 589 def _GetHeader(self): 590 """Returns header.""" 591 header = [] 592 for value in self.values: 593 try: 594 header.append(value.Header()) 595 except SkipValue: 596 continue 597 return header 598 599 def _GetValue(self, name): 600 """Returns the TextFSMValue object natching the requested name.""" 601 for value in self.values: 602 if value.name == name: 603 return value 604 605 def _AppendRecord(self): 606 """Adds current record to result if well formed.""" 607 608 # If no Values then don't output. 609 if not self.values: 610 return 611 612 cur_record = [] 613 for value in self.values: 614 try: 615 value.OnSaveRecord() 616 except SkipRecord: 617 self._ClearRecord() 618 return 619 except SkipValue: 620 continue 621 622 # Build current record into a list. 623 cur_record.append(value.value) 624 625 # If no Values in template or whole record is empty then don't output. 626 if len(cur_record) == (cur_record.count(None) + cur_record.count([])): 627 return 628 629 # Replace any 'None' entries with null string ''. 630 while None in cur_record: 631 cur_record[cur_record.index(None)] = '' 632 633 self._result.append(cur_record) 634 self._ClearRecord() 635 636 def _Parse(self, template): 637 """Parses template file for FSM structure. 638 639 Args: 640 template: Valid template file. 641 642 Raises: 643 TextFSMTemplateError: If template file syntax is invalid. 644 """ 645 646 if not template: 647 raise TextFSMTemplateError('Null template.') 648 649 # Parse header with Variables. 650 self._ParseFSMVariables(template) 651 652 # Parse States. 653 while self._ParseFSMState(template): 654 pass 655 656 # Validate destination states. 657 self._ValidateFSM() 658 659 def _ParseFSMVariables(self, template): 660 """Extracts Variables from start of template file. 661 662 Values are expected as a contiguous block at the head of the file. 663 These will be line separated from the State definitions that follow. 664 665 Args: 666 template: Valid template file, with Value definitions at the top. 667 668 Raises: 669 TextFSMTemplateError: If syntax or semantic errors are found. 670 """ 671 672 self.values = [] 673 674 for line in template: 675 self._line_num += 1 676 line = line.rstrip() 677 678 # Blank line signifies end of Value definitions. 679 if not line: 680 return 681 682 # Skip commented lines. 683 if self.comment_regex.match(line): 684 continue 685 686 if line.startswith('Value '): 687 try: 688 value = TextFSMValue( 689 fsm=self, max_name_len=self.MAX_NAME_LEN, 690 options_class=self._options_cls) 691 value.Parse(line) 692 except TextFSMTemplateError as error: 693 raise TextFSMTemplateError('%s Line %s.' % (error, self._line_num)) 694 695 if value.name in self.header: 696 raise TextFSMTemplateError( 697 "Duplicate declarations for Value '%s'. Line: %s." 698 % (value.name, self._line_num)) 699 700 try: 701 self._ValidateOptions(value) 702 except TextFSMTemplateError as error: 703 raise TextFSMTemplateError('%s Line %s.' % (error, self._line_num)) 704 705 self.values.append(value) 706 self.value_map[value.name] = value.template 707 # The line has text but without the 'Value ' prefix. 708 elif not self.values: 709 raise TextFSMTemplateError('No Value definitions found.') 710 else: 711 raise TextFSMTemplateError( 712 'Expected blank line after last Value entry. Line: %s.' 713 % (self._line_num)) 714 715 def _ValidateOptions(self, value): 716 """Checks that combination of Options is valid.""" 717 # Always passes in base class. 718 pass 719 720 def _ParseFSMState(self, template): 721 """Extracts State and associated Rules from body of template file. 722 723 After the Value definitions the remainder of the template is 724 state definitions. The routine is expected to be called iteratively 725 until no more states remain - indicated by returning None. 726 727 The routine checks that the state names are a well formed string, do 728 not clash with reserved names and are unique. 729 730 Args: 731 template: Valid template file after Value definitions 732 have already been read. 733 734 Returns: 735 Name of the state parsed from file. None otherwise. 736 737 Raises: 738 TextFSMTemplateError: If any state definitions are invalid. 739 """ 740 741 if not template: 742 return 743 744 state_name = '' 745 # Strip off extra white space lines (including comments). 746 for line in template: 747 self._line_num += 1 748 line = line.rstrip() 749 750 # First line is state definition 751 if line and not self.comment_regex.match(line): 752 # Ensure statename has valid syntax and is not a reserved word. 753 if (not self.state_name_re.match(line) or 754 len(line) > self.MAX_NAME_LEN or 755 line in TextFSMRule.LINE_OP or 756 line in TextFSMRule.RECORD_OP): 757 raise TextFSMTemplateError("Invalid state name: '%s'. Line: %s" 758 % (line, self._line_num)) 759 760 state_name = line 761 if state_name in self.states: 762 raise TextFSMTemplateError("Duplicate state name: '%s'. Line: %s" 763 % (line, self._line_num)) 764 self.states[state_name] = [] 765 self.state_list.append(state_name) 766 break 767 768 # Parse each rule in the state. 769 for line in template: 770 self._line_num += 1 771 line = line.rstrip() 772 773 # Finish rules processing on blank line. 774 if not line: 775 break 776 777 if self.comment_regex.match(line): 778 continue 779 780 # A rule within a state, starts with whitespace 781 if not (line.startswith(' ^') or line.startswith('\t^')): 782 raise TextFSMTemplateError( 783 "Missing white space or carat ('^') before rule. Line: %s" % 784 self._line_num) 785 786 self.states[state_name].append( 787 TextFSMRule(line, self._line_num, self.value_map)) 788 789 return state_name 790 791 def _ValidateFSM(self): 792 """Checks state names and destinations for validity. 793 794 Each destination state must exist, be a valid name and 795 not be a reserved name. 796 There must be a 'Start' state and if 'EOF' or 'End' states are specified, 797 they must be empty. 798 799 Returns: 800 True if FSM is valid. 801 802 Raises: 803 TextFSMTemplateError: If any state definitions are invalid. 804 """ 805 806 # Must have 'Start' state. 807 if 'Start' not in self.states: 808 raise TextFSMTemplateError("Missing state 'Start'.") 809 810 # 'End/EOF' state (if specified) must be empty. 811 if self.states.get('End'): 812 raise TextFSMTemplateError("Non-Empty 'End' state.") 813 814 if self.states.get('EOF'): 815 raise TextFSMTemplateError("Non-Empty 'EOF' state.") 816 817 # Remove 'End' state. 818 if 'End' in self.states: 819 del self.states['End'] 820 self.state_list.remove('End') 821 822 # Ensure jump states are all valid. 823 for state in self.states: 824 for rule in self.states[state]: 825 if rule.line_op == 'Error': 826 continue 827 828 if not rule.new_state or rule.new_state in ('End', 'EOF'): 829 continue 830 831 if rule.new_state not in self.states: 832 raise TextFSMTemplateError( 833 "State '%s' not found, referenced in state '%s'" % 834 (rule.new_state, state)) 835 836 return True 837 838 def ParseText(self, text, eof=True): 839 """Passes CLI output through FSM and returns list of tuples. 840 841 First tuple is the header, every subsequent tuple is a row. 842 843 Args: 844 text: (str), Text to parse with embedded newlines. 845 eof: (boolean), Set to False if we are parsing only part of the file. 846 Suppresses triggering EOF state. 847 848 Raises: 849 TextFSMError: An error occurred within the FSM. 850 851 Returns: 852 List of Lists. 853 """ 854 855 lines = [] 856 if text: 857 lines = text.splitlines() 858 859 for line in lines: 860 self._CheckLine(line) 861 if self._cur_state_name in ('End', 'EOF'): 862 break 863 864 if self._cur_state_name != 'End' and 'EOF' not in self.states and eof: 865 # Implicit EOF performs Next.Record operation. 866 # Suppressed if Null EOF state is instantiated. 867 self._AppendRecord() 868 869 return self._result 870 871 def _CheckLine(self, line): 872 """Passes the line through each rule until a match is made. 873 874 Args: 875 line: A string, the current input line. 876 """ 877 for rule in self._cur_state: 878 matched = self._CheckRule(rule, line) 879 if matched: 880 for value in matched.groupdict(): 881 self._AssignVar(matched, value) 882 883 if self._Operations(rule): 884 # Not a Continue so check for state transition. 885 if rule.new_state: 886 if rule.new_state not in ('End', 'EOF'): 887 self._cur_state = self.states[rule.new_state] 888 self._cur_state_name = rule.new_state 889 break 890 891 def _CheckRule(self, rule, line): 892 """Check a line against the given rule. 893 894 This is a separate method so that it can be overridden by 895 a debugging tool. 896 897 Args: 898 rule: A TextFSMRule(), the rule to check. 899 line: A str, the line to check. 900 901 Returns: 902 A regex match object. 903 """ 904 return rule.regex_obj.match(line) 905 906 def _AssignVar(self, matched, value): 907 """Assigns variable into current record from a matched rule. 908 909 If a record entry is a list then append, otherwise values are replaced. 910 911 Args: 912 matched: (regexp.match) Named group for each matched value. 913 value: (str) The matched value. 914 """ 915 self._GetValue(value).AssignVar(matched.group(value)) 916 917 def _Operations(self, rule): 918 """Operators on the data record. 919 920 Operators come in two parts and are a '.' separated pair: 921 922 Operators that effect the input line or the current state (line_op). 923 'Next' Get next input line and restart parsing (default). 924 'Continue' Keep current input line and continue resume parsing. 925 'Error' Unrecoverable input discard result and raise Error. 926 927 Operators that affect the record being built for output (record_op). 928 'NoRecord' Does nothing (default) 929 'Record' Adds the current record to the result. 930 'Clear' Clears non-Filldown data from the record. 931 'Clearall' Clears all data from the record. 932 933 Args: 934 rule: FSMRule object. 935 936 Returns: 937 True if state machine should restart state with new line. 938 939 Raises: 940 TextFSMError: If Error state is encountered. 941 """ 942 # First process the Record operators. 943 if rule.record_op == 'Record': 944 self._AppendRecord() 945 946 elif rule.record_op == 'Clear': 947 # Clear record. 948 self._ClearRecord() 949 950 elif rule.record_op == 'Clearall': 951 # Clear all record entries. 952 self._ClearAllRecord() 953 954 # Lastly process line operators. 955 if rule.line_op == 'Error': 956 if rule.new_state: 957 raise TextFSMError('Error: %s. Line: %s.' 958 % (rule.new_state, rule.line_num)) 959 960 raise TextFSMError('State Error raised. Line: %s.' 961 % (rule.line_num)) 962 963 elif rule.line_op == 'Continue': 964 # Continue with current line without returning to the start of the state. 965 return False 966 967 # Back to start of current state with a new line. 968 return True 969 970 def _ClearRecord(self): 971 """Remove non 'Filldown' record entries.""" 972 _ = [value.ClearVar() for value in self.values] 973 974 def _ClearAllRecord(self): 975 """Remove all record entries.""" 976 _ = [value.ClearAllVar() for value in self.values] 977 978 def GetValuesByAttrib(self, attribute): 979 """Returns the list of values that have a particular attribute.""" 980 981 if attribute not in self._options_cls.ValidOptions(): 982 raise ValueError("'%s': Not a valid attribute." % attribute) 983 984 result = [] 985 for value in self.values: 986 if attribute in value.OptionNames(): 987 result.append(value.name) 988 989 return result 990 991 992def main(argv=None): 993 """Validate text parsed with FSM or validate an FSM via command line.""" 994 995 if argv is None: 996 argv = sys.argv 997 998 try: 999 opts, args = getopt.getopt(argv[1:], 'h', ['help']) 1000 except getopt.error as msg: 1001 raise Usage(msg) 1002 1003 for opt, _ in opts: 1004 if opt in ('-h', '--help'): 1005 print(__doc__) 1006 print(help_msg) 1007 return 0 1008 1009 if not args or len(args) > 4: 1010 raise Usage('Invalid arguments.') 1011 1012 # If we have an argument, parse content of file and display as a template. 1013 # Template displayed will match input template, minus any comment lines. 1014 with open(args[0], 'r') as template: 1015 fsm = TextFSM(template) 1016 print('FSM Template:\n%s\n' % fsm) 1017 1018 if len(args) > 1: 1019 # Second argument is file with example cli input. 1020 # Prints parsed tabular result. 1021 with open(args[1], 'r') as f: 1022 cli_input = f.read() 1023 1024 table = fsm.ParseText(cli_input) 1025 print('FSM Table:') 1026 result = str(fsm.header) + '\n' 1027 for line in table: 1028 result += str(line) + '\n' 1029 print(result, end='') 1030 1031 if len(args) > 2: 1032 # Compare tabular result with data in third file argument. 1033 # Exit value indicates if processed data matched expected result. 1034 with open(args[2], 'r') as f: 1035 ref_table = f.read() 1036 1037 if ref_table != result: 1038 print('Data mis-match!') 1039 return 1 1040 else: 1041 print('Data match!') 1042 1043 1044if __name__ == '__main__': 1045 help_msg = '%s [--help] template [input_file [output_file]]\n' % sys.argv[0] 1046 try: 1047 sys.exit(main()) 1048 except Usage as err: 1049 print(err, file=sys.stderr) 1050 print('For help use --help', file=sys.stderr) 1051 sys.exit(2) 1052 except (IOError, TextFSMError, TextFSMTemplateError) as err: 1053 print(err, file=sys.stderr) 1054 sys.exit(2) 1055