Turning CLI output into structured data¶
Several articles on this site reach for the same trick without dwelling on it: the
Python article passes use_textfsm=True to turn show
output into a list of dictionaries, and change validation with pyATS
leans on Genie to parse a command into a dictionary you can assert on. This article pulls that
thread into one place — how to turn a device's human-readable CLI output into data a program can act
on, which tool to use for it, and, just as important, when you shouldn't be parsing CLI at all.
Why raw CLI output is a liability¶
show commands were designed to be read by a person at a terminal, not consumed by a program. The
columns line up, the headers are words, and the layout can change between software releases. A script
that pulls a value out of that text with string slicing or a hand-rolled regular expression is
brittle: it works until the vendor adds a column, renames a field, or wraps a long line, and then
it silently returns the wrong thing. Fragile parsing is a leading cause of the kind of confident,
wrong output covered in diagnosing failures from logs and output.
The fix is to hand the parsing to a maintained library that already knows the shape of each command's output and returns a predictable data structure — a list of dictionaries or a nested dictionary — that you index by field name instead of by character position. Three libraries dominate, and they sit at different points on the effort/coverage curve.
TextFSM and the NTC templates¶
TextFSM is Google's template-based text parser. You describe the
output once as a small state machine of named values and regular expressions, and TextFSM turns any
matching text into rows of records. You rarely write those templates yourself:
ntc-templates is a community library of over a
thousand ready-made templates indexed by platform and command, and
Netmiko ships with it. Passing use_textfsm=True to
send_command looks up the right template and returns structured data instead of a string:
from netmiko import ConnectHandler
device = {"device_type": "cisco_ios", "host": "192.0.2.11",
"username": "automation", "password": PASSWORD}
with ConnectHandler(**device) as conn:
facts = conn.send_command("show version", use_textfsm=True)
# facts is a list of dicts, e.g. [{'version': '17.9.1', 'serial': ['FXS...'], ...}]
print(facts[0]["version"])
Two things to know. First, if no template matches the platform/command pair, Netmiko falls back to
returning the raw string — so code that expects a list has to handle a str too, or it will break
on the first unsupported command. Second, you can point Netmiko at your own template directory with the
NET_TEXTFSM environment variable (it must contain an index file) when you need a template the
community library doesn't ship.
Genie parsers¶
Genie, the parsing companion to
pyATS, takes the same idea further. It ships over a thousand parsers
that normalise output to OS-agnostic data models, so show ip interface brief returns the same
dictionary shape whether the box runs IOS XE, NX-OS, or IOS XR — you assert on fields, not on raw text.
The quickest way in is the pyats parse CLI against a testbed file:
pyats parse "show ip interface brief" --testbed-file testbed.yaml --devices r1
{
"interface": {
"GigabitEthernet1": { "ip_address": "10.0.0.1", "status": "up", "protocol": "up" },
"GigabitEthernet3": { "ip_address": "unassigned", "status": "down", "protocol": "down" }
}
}
From Python, every device in a loaded testbed has a .parse() method returning that same dictionary,
which is exactly what makes scripted validation readable — you check a value in a dict instead of
grepping a string:
from genie.testbed import load
tb = load("testbed.yaml")
dev = tb.devices["r1"]
dev.connect()
parsed = dev.parse("show ip interface brief")
if parsed["interface"]["GigabitEthernet1"]["status"] != "up":
raise SystemExit("GigabitEthernet1 is down")
Genie's deeper coverage is why pyATS's learn/diff change validation works: it can parse whole features
into a structured snapshot and diff two of them. (Netmiko can also invoke Genie with use_genie=True
if you'd rather drive it through a Netmiko session.)
NAPALM getters: structured by contract¶
NAPALM comes at the problem from the other
direction. Instead of parsing arbitrary commands, it defines a fixed set of getters — get_facts,
get_interfaces, get_bgp_neighbors, and so on — each with a documented, vendor-neutral return
schema. The same call returns the same keys on every supported platform:
from napalm import get_network_driver
driver = get_network_driver("ios")
dev = driver(hostname="192.0.2.11", username="automation", password=PASSWORD)
dev.open()
facts = dev.get_facts() # same keys on IOS XE, EOS, JUNOS, ...
print(facts["os_version"], facts["serial_number"])
dev.close()
get_facts() always returns the same keys — hostname, vendor, model, serial_number,
os_version, uptime, interface_list — so a cross-vendor inventory or asset-management job is the
same code everywhere. The trade-off is coverage: a getter only exists for data NAPALM has modelled, and
support varies by driver, so check each platform's support matrix
before relying on one. Where it fits, it's the least fragile of the three, because you're coding against
a contract rather than a template.
The best parse is no parse¶
Every option above is still, underneath, extracting data from text meant for humans — and
CLI output is unstructured by design,
so a template or parser can break when the format shifts. When the device can give you the data already
structured, take it. RESTCONF, NETCONF, and gNMI return
YANG-modelled JSON or XML straight from the box — no template to maintain, no column to misread, and a
schema that defines every field. A RESTCONF GET hands you a
dictionary directly; there is nothing to parse.
The practical rule: prefer model-driven data (RESTCONF/NETCONF/gNMI) when the platform and feature support it, and fall back to CLI parsing only for legacy gear or the features that still appear in the CLI first. Screen-scraping isn't wrong — plenty of production automation depends on it — but it should be the fallback, not the default.
Making parsing robust¶
When you do parse CLI, treat it like any other fragile boundary:
- Handle the fallback. Expect a raw string when no template matches, rather than assuming a list or dict always comes back.
- Assert on fields, not text. The whole point is to check
row["status"], never togrepthe original output again. - Pin your versions. ntc-templates and Genie parsers evolve; pin them (see secure coding practices) so a dependency bump can't silently change your parsed shape.
- Expect parser bugs. Both ntc-templates and Genie are maintained libraries — a command that won't parse is often a template/parser bug to report upstream, not a flaw in your code.
- Test against captured output. Save real
showoutput as a fixture and parse it offline in CI, so you catch a broken parse without touching a live device.
Key takeaways¶
- Raw CLI output is unstructured text for humans; parsing it by hand is brittle. Hand the job to a maintained library that returns data you index by field name.
- TextFSM + ntc-templates (via Netmiko
use_textfsm=True) gives per-command templates and the widest community coverage; it falls back to a raw string when no template matches. - Genie parses to OS-agnostic models and powers pyATS validation;
pyats parseor a device's.parse()returns a structured dictionary. - NAPALM getters return a fixed, vendor-neutral schema — the least fragile option where a getter exists, bounded by per-driver support.
- The best parse is no parse: prefer YANG-modelled data over RESTCONF/NETCONF/gNMI when the device supports it, and keep CLI parsing as the fallback for legacy or CLI-first features.
Sources: TextFSM, ntc-templates, Netmiko, Genie / pyATS parsers, NAPALM supported getters, Ivan Pepelnjak — Using Screen Scraping in Network Automation.