Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@jobec
Last active April 2, 2024 21:10
Show Gist options
  • Star 4 You must be signed in to star a gist
  • Fork 2 You must be signed in to fork a gist
  • Save jobec/004bc02e628f4c8c19c1f2f289e050d7 to your computer and use it in GitHub Desktop.
Save jobec/004bc02e628f4c8c19c1f2f289e050d7 to your computer and use it in GitHub Desktop.
Junos config parsing with pyparsing
import textwrap
from collections import OrderedDict
import pyparsing as pp
class Statement(str):
def __str__(self):
return super().__str__() + ";"
class Section:
def __init__(self, name=None, items=None):
self.name = name
self.statements = set()
self.sub_sections = OrderedDict()
self._process_items(items)
def __eq__(self, other):
is_same = (
self.statements == other.statements and
self.name == other.name and
self.sub_sections.keys() == other.sub_sections.keys()
)
if not is_same:
return False
else:
for x in self.sub_sections.keys():
if not (self.sub_sections[x] == other.sub_sections[x]):
return False
return True
def merge(self, other_section):
self.statements.update(other_section.statements)
for section in other_section.sub_sections:
if section in self.sub_sections:
self.sub_sections[section].merge(other_section.sub_sections[section])
else:
self.sub_sections[section] = other_section.sub_sections[section]
def _process_items(self, items):
for item in items:
if isinstance(item, list):
sub_section = Section(item[0], item[1])
if self.sub_sections.get(sub_section.name):
self.sub_sections[sub_section.name].merge(sub_section)
else:
self.sub_sections[sub_section.name] = sub_section
else:
statement = Statement(item)
self.statements.add(statement)
def __str__(self):
statements = [str(x) for x in sorted(self.statements)]
sections = [str(x) for x in self.sub_sections.values()]
body = "\n".join(statements + sections)
if self.name:
body = textwrap.indent(body, " " * 4)
return "%s {\n%s\n}" % (self.name, body)
else:
return body
class Config(Section):
def __init__(self, filename=None, config=None):
parser = self._build_parser()
if filename:
result = parser.parseFile(filename, parseAll=True)
else:
result = parser.parseString(config, parseAll=True)
super(Config, self).__init__(items=result.asList())
def _build_parser(self):
symbols = ":/.-_"
value = pp.dblQuotedString() | pp.Word(pp.alphas + pp.nums + symbols)
line_comment = pp.Suppress(pp.Optional(pp.pythonStyleComment))
multi_value = ('[' + pp.OneOrMore(value) + "]")
statement = (
pp.Combine(
pp.OneOrMore(value) + pp.Optional(multi_value),
joinString=" ", adjacent=False
) +
pp.Suppress(";") +
line_comment
)
section_name_part = pp.Word(pp.alphas + pp.nums + "-_.*/:")
section_name = pp.Combine(pp.OneOrMore(section_name_part), joinString=" ", adjacent=False)
section = pp.Forward()
section <<= pp.Group(
section_name +
pp.Suppress("{") +
line_comment +
pp.Group(
pp.ZeroOrMore(
pp.Suppress(pp.cStyleComment) |
statement |
section
)
) +
pp.Suppress("}") +
line_comment +
pp.Optional(pp.Suppress(pp.cStyleComment))
)
config = pp.OneOrMore(
pp.Suppress(pp.cStyleComment) |
section |
statement
)
return config
cfg1 = Config(filename="cfg1.txt")
cfg2 = Config(filename="cfg2.txt")
print(cfg1)
print("-"*80)
print(cfg2)
print(cfg1 == cfg2)
@steffann
Copy link

Interesting! What is the license on this code?

@keyereal
Copy link

Thanks! Also JunOS config can be saved as XML with command like 'show configuration | display xml | save config_file.xml'

@hkam40k
Copy link

hkam40k commented Mar 8, 2022

Hi @jobec , could you add license information in this gist? (If I may recommend, new BSD or Apache 2.0)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment