Home | History | Annotate | Download | only in cros
      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 
     19 This module implements a parser, intended to be used for converting
     20 human readable text, such as command output from a router CLI, into
     21 a list of records, containing values extracted from the input text.
     22 
     23 A simple template language is used to describe a state machine to
     24 parse a specific type of text input, returning a record of values
     25 for each input entity.
     26 
     27 Import it to ~/file/client/common_lib/cros/.
     28 """
     29 from __future__ import absolute_import
     30 from __future__ import division
     31 from __future__ import print_function
     32 
     33 __version__ = '0.3.2'
     34 
     35 import getopt
     36 import inspect
     37 import re
     38 import string
     39 import sys
     40 
     41 
     42 class Error(Exception):
     43   """Base class for errors."""
     44 
     45 
     46 class Usage(Exception):
     47   """Error in command line execution."""
     48 
     49 
     50 class TextFSMError(Error):
     51   """Error in the FSM state execution."""
     52 
     53 
     54 class TextFSMTemplateError(Error):
     55   """Errors while parsing templates."""
     56 
     57 
     58 # The below exceptions are internal state change triggers
     59 # and not used as Errors.
     60 class FSMAction(Exception):
     61   """Base class for actions raised with the FSM."""
     62 
     63 
     64 class SkipRecord(FSMAction):
     65   """Indicate a record is to be skipped."""
     66 
     67 
     68 class SkipValue(FSMAction):
     69   """Indicate a value is to be skipped."""
     70 
     71 
     72 class 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 
    195 class 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 
    337 class 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 
    358 class 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 
    517 class 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 
    992 def 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 
   1044 if __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