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