• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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