diff --git a/textfsm/debugger.py b/textfsm/debugger.py new file mode 100644 index 0000000..2eb9997 --- /dev/null +++ b/textfsm/debugger.py @@ -0,0 +1,335 @@ +#!/usr/bin/python +# +# Copyright 2011 Google Inc. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +""" Visual Debugger + +Provides a HTML-based debugging tool that allows authors of templates +to view the behavior of templates when applied to some example CLI text. +State changes are represented with color coding such that state +transitions are clearly represented during parsing. + +Matches on lines are highlighted to show extracted values and hovering +over a match shows the value and corresponding regex that was matched. +""" +from collections import namedtuple +from textwrap import dedent + +import re + +LINE_SATURATION = 40 +LINE_LIGHTNESS = 60 +MATCH_SATURATION = 100 +MATCH_LIGHTNESS = 30 + + +class LineHistory(namedtuple('LineHistory', ['line', 'state', 'matches', 'match_index_pairs'])): + """" The match history for a given line when parsed using the FSM. + + Contains the regex match objects for that line, + which are converted to indices for highlighting + """ + + +class MatchedPair(namedtuple('MatchPair', ['match_obj', 'rule'])): + """" Stores the line history when parsed using the FSM.""" + + +class StartStopIndex(namedtuple('StartStopIndex', ['start', 'end', 'value'])): + """Represents the start and stop indices of a match for a given template value.""" + def __eq__(self, other): + return self.start == other.start and self.end == other.end + + def __gt__(self, other): + return self.start > other.start + + +class VisualDebugger(object): + """Responsible for building the parse history of a TextFSM object into a visual html doc. """ + + def __init__(self, fsm, cli_text): + self.fsm = fsm + self.cli_text = cli_text + self.state_colormap = {} + + @staticmethod + def add_prelude_boilerplate(html_file): + prelude_lines = dedent(''' + + +
+ +\n"
+ ]
+
+ html_file.writelines(cli_text_prelude)
+
+ lines = self.cli_text.splitlines()
+ lines = [line + '\n' for line in lines]
+
+ # Process each line history and add highlighting where matches occur.
+ l_count = 0
+ for line_history in self.fsm.parse_history:
+ # Only process highlights where matches occur.
+ if line_history.match_index_pairs:
+ built_line = ""
+ prev_end = 0
+ match_count = 0
+
+ for index in line_history.match_index_pairs:
+ if index.start < 0 or index.end < 0:
+ continue
+
+ # Strip out useless pattern format characters and value label.
+ # Escape chevrons in regex pattern.
+ re_patterns = []
+ values = []
+ if type(index.value) is list:
+ values = index.value
+ for v in index.value:
+ value_pattern = self.fsm.value_map[v]
+ re_patterns.append(re.sub('\?P<.*?>', '', value_pattern).replace('<', '<').replace('>', '>'))
+ else:
+ values.append(index.value)
+ value_pattern = self.fsm.value_map[index.value]
+ re_patterns.append(re.sub('\?P<.*?>', '', value_pattern).replace('<', '<').replace('>', '>'))
+
+ # Build section of match and escape non HTML chevrons if present
+ built_line += (
+ lines[l_count][prev_end:index.start].replace('<', '<').replace('>', '>')
+ + "".format(line_history.state, l_count, match_count)
+ + lines[l_count][index.start:index.end].replace('<', '<').replace('>', '>')
+ + "{} >> {}".format(', '.join(re_patterns), ', '.join(values))
+ )
+ prev_end = index.end
+ match_count += 1
+
+ built_line += lines[l_count][line_history.match_index_pairs[-1].end:].replace('<', '<').replace('>', '>')
+ lines[l_count] = built_line
+ else:
+ # Escape non HTML tag chevrons if present
+ lines[l_count] = lines[l_count].replace('<', '<').replace('>', '>')
+
+ # Add final span wrapping tag for line state color
+ lines[l_count] = ("".format(line_history.state)
+ + lines[l_count] + "")
+ l_count += 1
+
+ # Close off document
+ end_body_end_html = dedent('''
+
+
+
+ ''')
+
+ html_file.writelines(lines)
+
+ html_file.write(end_body_end_html)
+
+ def build_debug_html(self):
+ """Calls HTML building procedures in sequence to create debug HTML doc."""
+ with open("debug.html", "w+") as f:
+ self.add_prelude_boilerplate(f)
+ self.build_state_colors()
+ self.add_css_styling(f)
+ self.add_cli_text(f)
diff --git a/textfsm/parser.py b/textfsm/parser.py
index 801c2f3..3a61de2 100755
--- a/textfsm/parser.py
+++ b/textfsm/parser.py
@@ -34,6 +34,10 @@
import string
import sys
+from debugger import MatchedPair
+from debugger import LineHistory
+from debugger import VisualDebugger
+
class Error(Exception):
"""Base class for errors."""
@@ -568,6 +572,10 @@ def __init__(self, template, options_class=_DEFAULT_OPTIONS):
self._cur_state = None
# Name of the current state.
self._cur_state_name = None
+ # Visual debug mode flag
+ self.visual_debug = False
+ # Keep track of parse history when in debug mode.
+ self.parse_history = []
# Read and parse FSM definition.
# Restore the file pointer once done.
@@ -923,9 +931,16 @@ def _CheckLine(self, line):
Args:
line: A string, the current input line.
"""
+ line_history = None
+ if self.visual_debug:
+ line_history = LineHistory(line, self._cur_state_name, [], [])
+
for rule in self._cur_state:
matched = self._CheckRule(rule, line)
if matched:
+ if self.visual_debug and line_history:
+ line_history.matches.append(MatchedPair(matched, rule))
+
for value in matched.groupdict():
self._AssignVar(matched, value)
@@ -937,6 +952,9 @@ def _CheckLine(self, line):
self._cur_state_name = rule.new_state
break
+ if self.visual_debug and line_history:
+ self.parse_history.append(line_history)
+
def _CheckRule(self, rule, line):
"""Check a line against the given rule.
@@ -1048,7 +1066,7 @@ def main(argv=None):
argv = sys.argv
try:
- opts, args = getopt.getopt(argv[1:], 'h', ['help'])
+ opts, args = getopt.getopt(argv[1:], 'h', ['help', 'visual-debug'])
except getopt.error as msg:
raise Usage(msg)
@@ -1073,7 +1091,17 @@ def main(argv=None):
with open(args[1], 'r') as f:
cli_input = f.read()
+ for opt, _ in opts:
+ if opt == '--visual-debug':
+ print("visual debug mode, open 'debug.html' for template behaviour when parsing the CLI text.")
+ fsm.visual_debug = True
+
table = fsm.ParseText(cli_input)
+
+ if fsm.visual_debug:
+ debugger = VisualDebugger(fsm, cli_input)
+ debugger.build_debug_html()
+
print('FSM Table:')
result = str(fsm.header) + '\n'
for line in table:
@@ -1094,7 +1122,7 @@ def main(argv=None):
if __name__ == '__main__':
- help_msg = '%s [--help] template [input_file [output_file]]\n' % sys.argv[0]
+ help_msg = '%s [--help] [--visual-debug] template [input_file [output_file]]\n' % sys.argv[0]
try:
sys.exit(main())
except Usage as err: