# textfsm-rust
A Rust implementation of Google's [TextFSM](https://github.com/google/textfsm) template-based state machine for parsing semi-formatted text.
## Overview
TextFSM is a template-based state machine originally developed by Google for parsing CLI output from network devices. This crate provides a native Rust implementation with the same template syntax and behavior.
## Features
- **Battle-tested compatibility** - 97.51% strict pass rate against [ntc-templates](https://github.com/networktocode/ntc-templates) (1762/1807 tests), matching Python's TextFSM exactly
- **Compile-time template validation** - Catch template errors at compile time with proc macros, not at runtime
- **Serde integration** - Deserialize parsed results directly into typed Rust structs
- **Thread-safe design** - `Template` and `CliTable` are `Send + Sync`; share across threads with zero overhead
- **Detailed error messages** - Template errors include line numbers, context, and suggestions
- **Full TextFSM syntax** - All value options (`Required`, `Filldown`, `Fillup`, `List`, `Key`) and actions (`Next`, `Continue`, `Record`, `Clear`, `Error`)
- **Efficient regex handling** - Uses [fancy-regex](https://docs.rs/fancy-regex) which delegates to the fast [regex](https://docs.rs/regex) crate when advanced features aren't needed
- **Python regex compatibility** - Handles Python regex quirks like `\<` and `\>` automatically
## Installation
Add to your `Cargo.toml`:
```toml
[dependencies]
textfsm-rust = "0.3.1"
```
## Quick Start
```rust
use textfsm_rust::Template;
let template = Template::parse_str(r#"
Value Name (\S+)
Value Age (\d+)
Start
^Name: ${Name}, Age: ${Age} -> Record
"#)?;
let mut parser = template.parser();
let input = "Name: Alice, Age: 30\nName: Bob, Age: 25\n";
let results = parser.parse_text(input)?;
```
## Compile-Time Template Validation
Catch template errors before your program runs:
```rust
use textfsm_rust::{validate_template, validate_templates, Template};
// Validate a single template at compile time
validate_template!("templates/cisco_show_version.textfsm");
// Validate all .textfsm files in a directory
validate_templates!("templates/");
// Parse at runtime when you need it
let template = Template::parse_str(
include_str!("templates/cisco_show_version.textfsm")
).expect("validated at compile time");
```
If a template is invalid, you get a compile error with the template line number:
```
error: Template validation failed for 'templates/bad.textfsm':
invalid variable substitution at line 8: unknown variable 'Interfce'
--> src/main.rs:4:21
|
```
## Template Syntax
TextFSM templates consist of two sections:
### Value Definitions
```
Value [Options] Name (Regex)
```
Options:
- `Required` - Record only saved if this value is matched
- `Filldown` - Value retained across records
- `Fillup` - Value fills upward into previous records
- `List` - Value is a list of matches
- `Key` - Marks value as a key field
### State Rules
```
StateName
^Regex -> [Actions]
```
Actions:
- `Next` - Continue to next line (default)
- `Continue` - Continue processing current line
- `Record` - Save current record
- `Clear` - Clear non-filldown values
- `Error` - Stop processing with error
## Serde Integration
Enable the `serde` feature to deserialize parsed results directly into typed Rust structs:
```toml
[dependencies]
textfsm-rust = { version = "0.3", features = ["serde"] }
```
```rust
use textfsm_rust::{Deserialize, Template};
#[derive(Deserialize, Debug)]
struct Interface {
interface: String,
status: String,
ip_address: Option<String>,
}
let template = Template::parse_str(r#"
Value Interface (\S+)
Start
^Interface: ${Interface} is ${Status}
^ IP: ${IP_Address} -> Record
"#)?;
let mut parser = template.parser();
let input = "Interface: eth0 is up\n IP: 192.168.1.1\nInterface: eth1 is down\n IP: 10.0.0.1\n";
// Deserialize directly into typed structs
let interfaces: Vec<Interface> = parser.parse_text_into(input)?;
assert_eq!(interfaces[0].interface, "eth0");
assert_eq!(interfaces[0].status, "up");
assert_eq!(interfaces[0].ip_address, Some("192.168.1.1".into()));
```
Field names are matched case-insensitively against template value names (underscores are preserved).
## Thread Safety
`Template` is `Send + Sync` and can be safely shared across threads. The compiled template (including all regexes) is immutable after creation, so you can wrap it in an `Arc` and share it freely.
`Parser` is a lightweight, stateful wrapper that borrows a `Template`. Create one per thread - they're cheap since they don't copy the compiled regexes.
```rust
use std::sync::Arc;
use std::thread;
let template = Arc::new(Template::parse_str(template_str)?);
```rust
#[derive(Deserialize)]
struct Version {
version: String,
hostname: String,
}
let versions: Vec<Version> = cli_table.parse_cmd_into(&cli_output, &attrs)?;
```
## Error Messages
Template errors include line numbers to help you fix issues quickly:
```
invalid variable substitution at line 8: unknown variable 'Interfce'
```
```
invalid Value definition at line 3: regex must be wrapped in parentheses
```
```
invalid rule at line 12: unclosed variable substitution
```
```
missing required 'Start' state
```
## NTC Templates Compatibility
This implementation is tested against the full [ntc-templates](https://github.com/networktocode/ntc-templates) test suite using a strict, field-by-field comparison harness. The Rust implementation matches Google's Python TextFSM exactly:
| Test Cases | 1807 | 1807 |
| Passed | 1762 | 1762 |
| Failed | 45 | 45 |
| Errors | 0 | 0 |
| **Pass Rate** | **97.51%** | **97.51%** |
The same 45 tests fail in both implementations -- all are test data issues in the ntc-templates repository (YAML expects `"None"` strings in lists, or references columns not defined in templates), not parser bugs.
These results were produced by two equivalent strict test harnesses included in this repository:
- **Python baseline**: [`tests/python_ntc_harness.py`](tests/python_ntc_harness.py) -- runs all ntc-templates test cases through Google's [Python TextFSM](https://github.com/google/textfsm) and performs strict field-by-field comparison against the expected YAML output.
- **Rust harness**: [`textfsm-rust/examples/strict_ntc_test.rs`](textfsm-rust/examples/strict_ntc_test.rs) -- runs the same test cases through textfsm-rs with identical discovery and comparison logic.
Both harnesses discover test cases the same way (walking the `tests/{platform}/{command}/` directory structure), derive the template from the directory path, and compare parsed results against the YAML expected output with the same normalization rules. You can run them yourself:
```bash
# Python (requires PyYAML)
python3 tests/python_ntc_harness.py /path/to/ntc-templates --textfsm-path /path/to/textfsm
# Rust
cargo run --example strict_ntc_test --features serde -- /path/to/ntc-templates
```
### Known Differences
While the pass/fail classification is identical, there is one minor behavioral difference in how empty list values are represented:
| List value with no matches | `[""]` (list with empty string) | `[]` (empty list) |
This does not affect the pass rate since both representations fail against YAML test data that expects `["None"]`.
## License
Licensed under either of:
- Apache License, Version 2.0 ([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
- MIT License ([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
at your option.
## Acknowledgments
This project is a Rust port of [Google's TextFSM](https://github.com/google/textfsm), originally developed by Google Inc. and licensed under Apache 2.0.
Thanks to [Network to Code](https://github.com/networktocode/ntc-templates) for their extensive collection of TextFSM templates and test data, which made it possible to validate this implementation against real-world use cases.