Writing a parser¶
A parser is a class with a parse(text) method, registered for one or more command patterns.
from typing import Any
from clijson import Parser, register
from clijson.models import record, status
from clijson.textutils import match_lines, none_if
@register("vrp", "display vrrp [brief]", "display vrrp <interface> [brief]")
class DisplayVrrpBrief(Parser):
"""VRRP groups: state, interface, virtual IP."""
def parse(self, text: str) -> list[dict[str, Any]]:
out: list[dict[str, Any]] = []
for m in match_lines(r"^\s*(?P<vrid>\d+)\s+(?P<state>Master|Backup|Initialize)\s+(?P<intf>\S+)\s+(?P<type>\S+)\s+(?P<vip>\S+)", text):
out.append({"vrid": int(m["vrid"]), "state": m["state"], "interface": m["intf"], "type": m["type"], "virtual_ip": m["vip"]})
return out
The codebase is type-checked with mypy --strict, so annotate parse() (and normalize()) the same way.
uv run mypy checks everything under src/.
Command patterns¶
| syntax | meaning | example |
|---|---|---|
word |
keyword (abbreviations accepted) | show matches sh |
<name> |
one token captured as self.params["name"] |
show interfaces <interface> |
<name...> |
rest of the line | show route [<target...>] |
[ ... ] |
optional, may nest | show bgp [vrf <vrf>] summary |
( a \| b ) |
alternatives | show (ip\|ipv4) interface brief |
Tips:
- Register the most specific patterns you can. More keywords means a higher score. For example,
show route summarywins overshow route [<target...>]. - For commands whose options can come in any order (Junos
show route ...), register a catch-all<args...>and read the options in the parser (self.params["args"]). self.commandholds the command exactly as the user typed it. It is useful for things like the address family.
Helpers (clijson.textutils)¶
| helper | use |
|---|---|
match_lines(rx, text) |
per-line regex matching (never spans lines) |
parse_table(text, header=..., names=...) |
column-aligned tables sliced by header position |
blocks(text, start=rx) |
split output into per-entity blocks |
to_num, none_if, snake, normalize_mac, parse_duration |
value helpers |
compact(obj) |
drop None / empty values |
search(rx, text) |
first match as a dict of converted groups |
Normalized output¶
If the command maps to a common concept, pass intent="..." to @register and implement normalize(data).
It should return records built with clijson.models.record(intent, **fields), which rejects unknown fields
and fills in missing ones. Use status(), mac() and seconds() to standardise values. The schemas are in
models.md.
Fixtures (tests)¶
python scripts/fixture.py add capture.txt -p vrp -c "display vrrp brief" --name ne40e_v8r21
pytest tests/test_fixtures.py -k vrrp
The generated .json file holds the expected output. Review it before you commit. After an intentional
change, python scripts/fixture.py update rewrites every expectation so you can check the diff with git.
Shipping parsers from your own package¶
# your pyproject.toml
[project.entry-points."clijson.parsers"]
my_parsers = "my_package.clijson_parsers"
clijson imports that module on first use, and your @register decorators run.