Parsing show Command Output: TextFSM, Genie, and TTP for Structured Data
The parsing tax
Every meaningful network automation project hits the same wall. SSH to a device gives you text. Decisions, dashboards, and configuration changes need data. The work of converting one to the other is parsing, and it is where most home-grown automation projects spend more time than they expected.
There is no single right tool. There are three good ones — TextFSM (with NTC Templates), Cisco’s pyATS/Genie parsers, and TTP — and the correct answer depends on what you are parsing, how often, and how much you mind shipping a dependency. This post walks through all three with the same example commands, calls out where each one is the right pick, and gives you a workflow for the inevitable case where none of the prebuilt parsers cover the command you actually need.
This is for engineers who already use Netmiko (or NAPALM, or Scrapli) and have started writing .split() and .findall() in their automation scripts. There is a better way.
The example
I am going to parse the same two commands across all three tools so the comparison stays honest:
csr1#show ip interface brief
Interface IP-Address OK? Method Status Protocol
GigabitEthernet1 10.0.0.1 YES NVRAM up up
GigabitEthernet2 unassigned YES NVRAM administratively down down
GigabitEthernet3 192.168.1.1 YES manual up up
Loopback0 1.1.1.1 YES NVRAM up up
csr1#show ip bgp summary
BGP router identifier 1.1.1.1, local AS number 65001
BGP table version is 14, main routing table version 14
Neighbor V AS MsgRcvd MsgSent TblVer InQ OutQ Up/Down State/PfxRcd
10.0.12.2 4 65002 2042 2041 14 0 0 1d04h 12
10.0.13.2 4 65003 876 875 14 0 0 14:22:11 8
192.168.1.5 4 65010 45 44 0 0 0 never Idle
The first is shallow and well-formed; the second has tabular output mixed with header lines and one device with an inconvenient state. Both are real-world common.
TextFSM and NTC Templates: the everyday workhorse
TextFSM is a Google-originated state-machine engine for parsing text. NTC Templates is a community-maintained repository of TextFSM templates for hundreds of show commands across Cisco, Arista, Juniper, and others. Together they are what most people reach for first, and they are right to.
Install:
pip install textfsm ntc-templates
Used directly with Netmiko:
from netmiko import ConnectHandler
with ConnectHandler(**device) as conn:
conn.enable()
rows = conn.send_command("show ip interface brief", use_textfsm=True)
# rows is now a list of dicts
# [{"intf": "GigabitEthernet1", "ipaddr": "10.0.0.1", "status": "up", "proto": "up"},
# {"intf": "GigabitEthernet2", "ipaddr": "unassigned", "status": "administratively down", "proto": "down"}, ...]
Used standalone — useful when you have raw output captured from elsewhere:
from ntc_templates.parse import parse_output
with open("captured_output.txt") as f:
raw = f.read()
rows = parse_output(platform="cisco_ios", command="show ip interface brief", data=raw)
The good things about TextFSM:
- Fast. Pure Python, no heavy dependencies.
- Templates are tiny text files. Easy to read, easy to modify, easy to write your own.
- NTC Templates covers the ninety percent. If you are parsing a common Cisco/Arista/Junos command, it almost certainly has a template.
- The output is plain Python dicts. No magic objects, no special framework.
- One pip install, no licence keys, no Cisco account required.
The limitations:
- Templates have to exist. If you need to parse
show some-obscure-feature, you write your own template (which is not hard, but is work). - TextFSM is line-oriented and state-machine-driven. Output that interleaves multiple kinds of records is awkward to template.
- Field naming is at the template author’s discretion, which means slightly inconsistent naming across NTC Templates (
intfvsinterface,ip_addressvsipaddr).
When TextFSM is the right pick:
- You are parsing common show commands across many devices.
- You want minimal dependencies.
- You want results to feed into pandas/CSV/JSON without further transformation.
If your automation does not need anything more than this, stop here. TextFSM via Netmiko’s use_textfsm=True covers most production use cases and is the lowest-friction path.
Writing your own TextFSM template
When you need a command that is not in NTC Templates, the workflow is reasonable. A template is a plain text file with two parts: a Value block declaring fields, and a state machine that matches lines and emits records.
For show ip interface brief, the template (call it show_ip_int_brief.template) looks something like:
Value INTF (\S+)
Value IPADDR (\S+)
Value STATUS (.+?)
Value PROTO (\S+)
Start
^Interface\s+IP-Address.*$$ -> Interfaces
Interfaces
^${INTF}\s+${IPADDR}\s+\S+\s+\S+\s+${STATUS}\s+${PROTO}\s*$$ -> Record
Test it with the textfsm CLI:
textfsm show_ip_int_brief.template captured_output.txt
It prints rows. Drop the template into NTC Templates’ search path and Netmiko picks it up automatically. To contribute it back, the networktocode/ntc-templates repo has a clear contribution process.
Genie and pyATS: full-fidelity parsing for complex output
Cisco’s pyATS framework includes a parser library called Genie. The parsers are written by Cisco engineers, often by the people who own the CLI on the platform side, and the field names match the YANG/operational data model rather than ad-hoc abbreviations. The output is structurally rich — nested dicts where TextFSM gives you flat ones — and the coverage on complex IOS-XE/IOS-XR/NX-OS commands is excellent.
Install:
pip install pyats[full]
That is a heavy install. Be aware. It pulls in a lot of dependencies and adds tens of megabytes. On airgapped boxes, plan accordingly.
Used with Netmiko’s Genie integration (Netmiko knows about Genie too):
with ConnectHandler(**device) as conn:
conn.enable()
parsed = conn.send_command("show ip bgp summary", use_genie=True)
# parsed is a deeply nested dict matching the operational data model:
# {
# "vrf": {
# "default": {
# "neighbor": {
# "10.0.12.2": {
# "address_family": {
# "ipv4 unicast": {
# "version": 4, "as": 65002, "msg_rcvd": 2042, ...,
# "state_pfxrcd": "12", "up_down": "1d04h"
# }
# }
# }, ...
# }
# }
# }
# }
What Genie gives you that TextFSM does not:
- Consistent, structured naming across platforms — the same parser works on IOS, XE, XR, and NX-OS where the platforms differ in CLI but not in semantics.
- Multi-line records and complex state are handled correctly.
- The model maps onto Cisco’s own operational schemas, which means if you also use NETCONF/RESTCONF, your data shapes are consistent.
- pyATS as a whole gives you connection management, test harnesses, and Robot integration if you want them.
What it costs:
- Heavy install. Not appropriate for small scripts where the ratio of dependency to functionality is wrong.
- Vendor lean. Genie’s coverage is excellent on Cisco and Arista, narrower elsewhere.
- The nested dict structure is more powerful but takes longer to navigate. You will write helper functions to flatten it for tabular use.
When Genie is the right pick:
- Cisco-heavy estate, complex commands.
- You already use pyATS for testing.
- You need consistent parsing across IOS, XE, XR, and NX-OS.
- You want the data model to align with operational state schemas you might consume later via NETCONF.
For the BGP summary command in our example, Genie wins. The mixed-state output (some neighbors with prefix counts, one in Idle state) is exactly the kind of thing that breaks naive parsing, and Genie’s parser handles it correctly out of the box.
TTP: ad-hoc parsing when no template exists
Template Text Parser (TTP) takes a different approach. Where TextFSM templates describe a state machine and Genie parsers are hand-coded, TTP templates look like the output you want to parse, with placeholders where the variable parts go. It is the easiest of the three to write a template for from scratch.
Install:
pip install ttp
A TTP template for the BGP summary:
<group name="neighbors">
{{ neighbor }} {{ version }} {{ asn }} {{ msg_rcvd }} {{ msg_sent }} {{ tbl_ver }} {{ in_q }} {{ out_q }} {{ up_down }} {{ state }}
</group>
Run it:
from ttp import ttp
with open("bgp_summary.txt") as f:
raw = f.read()
template = open("bgp_summary.ttp").read()
parser = ttp(data=raw, template=template)
parser.parse()
result = parser.result()
# result is a list of lists of dicts:
# [[{"neighbors": [{"neighbor": "10.0.12.2", "asn": "65002", ...}, ...]}]]
TTP’s strength is how fast you can write a template. You paste in a representative chunk of the output, replace the variable bits with {{ var_name }} placeholders, group related lines, and you have a parser. For the kind of one-off “I need to parse this weird vendor-specific command for tomorrow’s report” job, TTP is dramatically faster than writing a TextFSM template.
The flip side is that TTP is less battle-tested than TextFSM and the ecosystem of pre-written templates is much smaller. You will write your own templates more often. That is the trade-off.
When TTP is the right pick:
- One-off parsing tasks where no template exists in NTC Templates.
- Output where the structure is clear from looking at it.
- You want the template to be obviously correct to a reviewer who is not a TextFSM expert.
I keep TTP in my back pocket. Most of my day-to-day uses TextFSM. When TextFSM has no template and the command is too obscure to be worth writing one for, TTP is the second-fastest path.
A decision tree
The shortest version, in case you skim:
- Common Cisco/Arista/Junos show command, simple output, you want CSV/pandas output → TextFSM via Netmiko’s
use_textfsm=True. - Complex Cisco command, multi-platform, nested data, you can afford the install → Genie via
use_genie=True. - Output you understand at a glance but no template exists → TTP, write your own in five minutes.
- Output that needs context across multiple commands or full device state — pyATS testbeds and Genie learners go beyond parsing into operational state, and that is a different discussion.
Plugging parsers into your existing scripts
The pattern in the previous Netmiko post used use_textfsm=True directly. A more robust pattern, when you may want to switch parser per command, looks like this:
PARSER_MAP = {
"show ip interface brief": ("textfsm", None),
"show ip bgp summary": ("genie", None),
"show vendor-specific stuff": ("ttp", "templates/vendor_specific.ttp"),
}
def parse_command(conn, cmd):
parser, template_path = PARSER_MAP.get(cmd, ("raw", None))
if parser == "textfsm":
return conn.send_command(cmd, use_textfsm=True)
if parser == "genie":
return conn.send_command(cmd, use_genie=True)
if parser == "ttp":
from ttp import ttp
raw = conn.send_command(cmd)
with open(template_path) as f:
t = ttp(data=raw, template=f.read())
t.parse()
return t.result()
return conn.send_command(cmd)
This makes the parser choice a configuration concern rather than a code concern. As your automation grows, you will want this. New commands get a row in PARSER_MAP; the rest of the code does not change.
Storing the parsed output sensibly
Once you have structured data, where does it live? A small set of patterns covers most needs:
- CSV for one-off reports humans will look at in Excel.
csv.DictWriter, done. - JSON files for archival and for feeding into other Python scripts.
json.dump(..., indent=2)and you are done. - SQLite when you have a few hundred runs and want to query across them.
pandas.to_sqlor plainsqlite3both work. - Time-series database (InfluxDB, Prometheus via push gateway) when you are tracking interface counters or BGP neighbor counts as metrics over time.
- Structured logging to ELK/Loki when this is part of an operational pipeline rather than a one-off.
The decision about what to do with the data is downstream of the parsing, but it is worth knowing where you are heading before you start. A pile of CSV files is a reasonable interim and a terrible terminus. SQLite plus a small Streamlit dashboard, or a JSON file plus a Jupyter notebook, costs almost nothing more and is what most network-engineering automation actually wants.
What this saves you
A single show command parsed properly turns one-offs into pipelines. show ip interface brief becomes a job that runs nightly, writes one row per interface per device per run, and feeds an Excel report your manager actually reads. show ip bgp summary becomes a monitor that flags neighbors that have been Idle for more than fifteen minutes. show interface counters errors becomes a CRC trend by interface, and you finally have the data to argue for replacing those SFPs.
None of that is possible while the data is still text. Pick a parser, get the data into dicts, and the rest of the work becomes ordinary Python.
The next post in this series steps up a level: NAPALM gives you vendor-agnostic operations on top of Netmiko, with built-in parsing for getters and a config compare/replace/rollback model that is the right answer when you are doing more than reading.