textfsm_rs/
lib.rs

1pub mod error;
2pub use error::{Result, TextFsmError};
3use log::{debug, trace, warn};
4pub use pest::Parser;
5pub use pest::iterators::Pair;
6use pest_derive::Parser;
7use regex::Regex;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, VecDeque};
10use std::fmt;
11
12#[cfg(feature = "clitable")]
13pub mod cli_table;
14pub mod export;
15pub mod varsubst;
16#[cfg(feature = "clitable")]
17pub use cli_table::CliTable;
18pub use export::{OutputFormat, TextFsmExport};
19
20/// An iterator that parses input line-by-line and yields `DataRecord`s.
21pub struct TextFsmIter<R> {
22    fsm: TextFSM,
23    lines: std::io::Lines<R>,
24    eof_processed: bool,
25    current_line: Option<String>,
26}
27
28impl<R: std::io::BufRead> Iterator for TextFsmIter<R> {
29    type Item = Result<DataRecord>;
30
31    fn next(&mut self) -> Option<Self::Item> {
32        // If we have accumulated records from previous lines (e.g. from 'Record' actions), return them first.
33        if !self.fsm.records.is_empty() {
34            return Some(Ok(self.fsm.records.pop_front().unwrap()));
35        }
36
37        if self.eof_processed {
38            return None;
39        }
40
41        loop {
42            let line = if let Some(ref l) = self.current_line {
43                l.clone()
44            } else {
45                match self.lines.next() {
46                    Some(Ok(l)) => l,
47                    Some(Err(e)) => return Some(Err(TextFsmError::IoError(e))),
48                    None => {
49                        // End of input. Handle EOF state transition logic.
50                        if self.fsm.curr_state != "End" {
51                            if let Err(e) = self.fsm.set_curr_state("EOF") {
52                                return Some(Err(e));
53                            }
54                            if let Err(e) = self.fsm.parse_line("") {
55                                return Some(Err(e));
56                            }
57                            if !self.fsm.records.is_empty() {
58                                // Don't return None yet, next call will return None after popping records
59                                return Some(Ok(self.fsm.records.pop_front().unwrap()));
60                            }
61                            if let Err(e) = self.fsm.set_curr_state("End") {
62                                return Some(Err(e));
63                            }
64                        }
65                        self.eof_processed = true;
66                        return None;
67                    }
68                }
69            };
70
71            // trace!("LINE: '{}'", &line);
72            match self.fsm.parse_line(&line) {
73                Ok(ParseStatus::NextLine(maybe_next_state)) => {
74                    self.current_line = None;
75                    if let Some(next_state) = maybe_next_state {
76                        match next_state {
77                            NextState::Error(msg) => {
78                                return Some(Err(TextFsmError::StateError(format!(
79                                    "Error state reached! msg: {:?}",
80                                    msg
81                                ))));
82                            }
83                            NextState::NamedState(name) => {
84                                if let Err(e) = self.fsm.set_curr_state(&name) {
85                                    return Some(Err(e));
86                                }
87                            }
88                        }
89                    }
90                }
91                Ok(ParseStatus::SameLine(maybe_next_state)) => {
92                    // Keep current_line set
93                    self.current_line = Some(line.clone());
94                    if let Some(next_state) = maybe_next_state {
95                        match next_state {
96                            NextState::Error(msg) => {
97                                return Some(Err(TextFsmError::StateError(format!(
98                                    "Error state reached! msg: {:?}",
99                                    msg
100                                ))));
101                            }
102                            NextState::NamedState(name) => {
103                                if let Err(e) = self.fsm.set_curr_state(&name) {
104                                    return Some(Err(e));
105                                }
106                            }
107                        }
108                    }
109                }
110                Err(e) => return Some(Err(e)),
111            }
112
113            if self.fsm.curr_state == "EOF" || self.fsm.curr_state == "End" {
114                self.eof_processed = true;
115                if !self.fsm.records.is_empty() {
116                    return Some(Ok(self.fsm.records.pop_front().unwrap()));
117                }
118                return None; // Should break loop and return None
119            }
120
121            // If records were generated by this line, return the first one.
122            if !self.fsm.records.is_empty() {
123                return Some(Ok(self.fsm.records.pop_front().unwrap()));
124            }
125        }
126    }
127}
128
129/// Represents a single row of extracted data from a TextFSM template.
130#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Default)]
131pub struct DataRecord {
132    /// Map of value names to their extracted values.
133    #[serde(flatten)]
134    pub fields: HashMap<String, Value>,
135    /// An optional key used to identify the record, constructed from fields marked as 'Key'.
136    #[serde(skip_deserializing)]
137    pub record_key: Option<String>,
138}
139
140impl DataRecord {
141    /// Creates a new, empty `DataRecord`.
142    pub fn new() -> Self {
143        Default::default()
144    }
145
146    /// Overwrites existing fields in this record with fields from another record.
147    pub fn overwrite_from(&mut self, from: DataRecord) {
148        for (k, v) in from.fields {
149            self.fields.insert(k, v);
150        }
151    }
152
153    /// Compares two sets of records and returns differences.
154    /// Returns a tuple of (fields only in result, fields only in other).
155    pub fn compare_sets(result: &[Self], other: &[Self]) -> (Vec<Vec<String>>, Vec<Vec<String>>) {
156        let mut only_in_result: Vec<Vec<String>> = vec![];
157        let mut only_in_other: Vec<Vec<String>> = vec![];
158
159        for (i, irec) in result.iter().enumerate() {
160            let mut vo: Vec<String> = vec![];
161            for (k, v) in &irec.fields {
162                if i < other.len() {
163                    let v0 = other[i].get(k);
164                    if v0.is_none() || v0.unwrap() != v {
165                        vo.push(format!("{}:{:?}", &k, &v));
166                    }
167                } else {
168                    vo.push(format!("{}:{:?}", &k, &v));
169                }
170            }
171            only_in_result.push(vo);
172        }
173
174        for (i, irec) in other.iter().enumerate() {
175            let mut vo: Vec<String> = vec![];
176            for (k, v) in &irec.fields {
177                if i < result.len() {
178                    let v0 = result[i].get(k);
179                    if v0.is_none() || v0.unwrap() != v {
180                        vo.push(format!("{}:{:?}", &k, &v));
181                    }
182                } else {
183                    vo.push(format!("{}:{:?}", &k, &v));
184                }
185            }
186            only_in_other.push(vo);
187        }
188        (only_in_result, only_in_other)
189    }
190
191    /// Inserts a single string value into the record.
192    /// If the key already exists, it converts the value to a list or appends to it.
193    pub fn insert(&mut self, name: String, value: String) {
194        use std::collections::hash_map::Entry;
195        match self.fields.entry(name) {
196            Entry::Occupied(mut entry) => {
197                let old_value = entry.get_mut();
198                if let Value::Single(old_str) = old_value {
199                    let s = std::mem::take(old_str);
200                    *old_value = Value::List(vec![s, value]);
201                } else if let Value::List(list) = old_value {
202                    list.push(value);
203                }
204            }
205            Entry::Vacant(entry) => {
206                entry.insert(Value::Single(value));
207            }
208        }
209    }
210
211    /// Appends a `Value` to the record.
212    pub fn append_value(&mut self, name: String, value: Value) {
213        if let Some(old_value) = self.fields.get_mut(&name) {
214            match old_value {
215                Value::Single(old_str_ref) => match value {
216                    Value::Single(val) => {
217                        *old_value = Value::Single(val);
218                    }
219                    Value::List(lst) => {
220                        panic!(
221                            "can not append list {:?} to single {:?} in var {}",
222                            &lst, &old_str_ref, &name
223                        );
224                    }
225                },
226                Value::List(list) => match value {
227                    Value::Single(val) => {
228                        list.push(val);
229                    }
230                    Value::List(mut lst) => {
231                        list.append(&mut lst);
232                    }
233                },
234            }
235        } else {
236            self.fields.insert(name, value);
237        }
238    }
239
240    /// Removes a field from the record.
241    pub fn remove(&mut self, key: &str) {
242        self.fields.remove(key);
243    }
244
245    /// Returns an iterator over the field names.
246    pub fn keys(&self) -> std::collections::hash_map::Keys<'_, String, Value> {
247        self.fields.keys()
248    }
249
250    /// Retrieves a reference to a field's value.
251    pub fn get(&self, key: &str) -> Option<&Value> {
252        self.fields.get(key)
253    }
254
255    /// Returns an iterator over the record's fields.
256    pub fn iter(&self) -> std::collections::hash_map::Iter<'_, String, Value> {
257        self.fields.iter()
258    }
259}
260
261/// Represents an extracted value, which can be either a single string or a list of strings.
262#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
263#[serde(untagged)]
264pub enum Value {
265    /// A single extracted string.
266    Single(String),
267    /// A list of extracted strings (used for fields with 'List' option).
268    List(Vec<String>),
269}
270
271impl fmt::Display for Value {
272    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
273        match self {
274            Value::Single(s) => write!(f, "{}", s),
275            Value::List(l) => write!(f, "{:?}", l),
276        }
277    }
278}
279
280/// The compiled TextFSM parser containing value definitions and state machines.
281#[derive(Parser, Debug, Default, Clone)]
282#[grammar = "textfsm.pest"]
283pub struct TextFSMParser {
284    /// Definitions of all values declared in the template.
285    pub values: HashMap<String, ValueDefinition>,
286    /// List of value names that are marked as 'Required'.
287    pub mandatory_values: Vec<String>,
288    /// Compiled state machine states.
289    pub states: HashMap<String, StateCompiled>,
290}
291
292/// The runtime engine for TextFSM parsing.
293#[derive(Debug, Default, Clone)]
294pub struct TextFSM {
295    /// The underlying compiled parser.
296    pub parser: TextFSMParser,
297    /// The current state of the engine.
298    pub curr_state: String,
299    /// The record currently being populated.
300    pub curr_record: DataRecord,
301    /// Record containing values to be 'filled down' to subsequent records.
302    pub filldown_record: DataRecord,
303    /// List of all successfully parsed records.
304    pub records: VecDeque<DataRecord>,
305}
306
307/// Action to take regarding the current line of input.
308#[derive(Debug, PartialEq, Clone)]
309pub enum LineAction {
310    /// Continue processing subsequent rules in the current state for the same line.
311    Continue(Option<NextState>),
312    /// Move to the next line of input, optionally transitioning to a new state.
313    Next(Option<NextState>),
314}
315
316pub enum ParseStatus {
317    NextLine(Option<NextState>),
318    SameLine(Option<NextState>),
319}
320
321impl Default for LineAction {
322    fn default() -> LineAction {
323        LineAction::Next(None)
324    }
325}
326
327/// Action to take regarding record lifecycle.
328#[derive(Debug, Default, PartialEq, Clone)]
329pub enum RecordAction {
330    /// No action on the record.
331    #[default]
332    NoRecord,
333    /// Save the current record and start a new one.
334    Record,
335    /// Clear the current record (respecting Filldown).
336    Clear,
337    /// Clear all values in the current record including Filldown.
338    Clearall,
339}
340
341/// Represents the next state or an error condition.
342#[derive(Debug, PartialEq, Clone)]
343pub enum NextState {
344    /// Transition to an error state with an optional message.
345    Error(Option<String>),
346    /// Transition to a named state (e.g., 'Start', 'EOF').
347    NamedState(String),
348}
349
350/// Combines line and record actions for a rule match.
351#[derive(Debug, Default, PartialEq, Clone)]
352pub struct RuleTransition {
353    line_action: LineAction,
354    record_action: RecordAction,
355}
356
357/// A single rule within a TextFSM state.
358#[derive(Debug, Default, PartialEq, Clone)]
359pub struct StateRule {
360    /// The regex pattern to match against the input line.
361    rule_match: String,
362    /// The transition to perform if the rule matches.
363    transition: RuleTransition,
364}
365
366/// Metadata and regex definition for an extracted value.
367#[derive(Debug, Default, PartialEq, Clone)]
368pub struct ValueDefinition {
369    /// Name of the value.
370    name: String,
371    /// Whether the value should be preserved across records until overwritten.
372    is_filldown: bool,
373    /// Whether this value is part of the record's unique key.
374    is_key: bool,
375    /// Whether a record must have this value populated to be valid.
376    is_required: bool,
377    /// Whether this value can collect multiple matches into a list.
378    is_list: bool,
379    /// Whether this value should be filled up into previous records.
380    is_fillup: bool,
381    /// The regex pattern used to extract this value.
382    regex_pattern: String,
383    /// Original raw options string.
384    options: Option<String>,
385}
386
387/// Wrapper for different regex engines (standard or fancy for lookarounds).
388#[derive(Debug, Clone)]
389pub enum MultiRegex {
390    /// Standard Rust regex.
391    Classic(regex::Regex),
392    /// fancy-regex for advanced features like lookahead/lookbehind.
393    Fancy(fancy_regex::Regex),
394}
395
396/// Metadata about a variable captured by a rule.
397#[derive(Debug, Clone)]
398pub struct CapturedVariable {
399    pub name: String,
400    pub is_list: bool,
401    pub is_key: bool,
402    pub is_filldown: bool,
403    pub is_fillup: bool,
404}
405
406/// A compiled version of a `StateRule` ready for execution.
407#[derive(Debug, Clone)]
408pub struct StateRuleCompiled {
409    _rule_match: String,
410    _expanded_rule_match: String,
411    /// Variables captured by this rule and their metadata.
412    captured_vars: Vec<CapturedVariable>,
413    /// The compiled regex (if any).
414    maybe_regex: Option<MultiRegex>,
415    /// The transition to perform.
416    transition: RuleTransition,
417}
418
419/// A compiled state containing a list of rules.
420#[derive(Debug, Clone)]
421pub struct StateCompiled {
422    /// Name of the state.
423    name: String,
424    /// Rules belonging to this state.
425    rules: Vec<StateRuleCompiled>,
426}
427
428/// Transformation options for extracted records.
429#[derive(Debug, Clone)]
430pub enum DataRecordConversion {
431    /// Convert all field names to lowercase.
432    LowercaseKeys,
433}
434
435impl TextFSMParser {
436    fn _log_pair(indent: usize, pair: &Pair<'_, Rule>) {
437        let spaces = " ".repeat(indent);
438        trace!("{}Rule:    {:?}", spaces, pair.as_rule());
439        trace!("{}Span:    {:?}", spaces, pair.as_span());
440        trace!("{}Text:    {}", spaces, pair.as_str());
441        for p in pair.clone().into_inner() {
442            Self::_log_pair(indent + 2, &p);
443        }
444    }
445    pub fn parse_state_rule_transition(pair: &Pair<'_, Rule>) -> RuleTransition {
446        let mut record_action: RecordAction = Default::default();
447        let mut line_action: LineAction = Default::default();
448        // Self::print_pair(5, pair);
449        for pair in pair.clone().into_inner() {
450            match pair.as_rule() {
451                Rule::record_action => {
452                    record_action = match pair.as_str() {
453                        "Record" => RecordAction::Record,
454                        "NoRecord" => RecordAction::NoRecord,
455                        "Clear" => RecordAction::Clear,
456                        "Clearall" => RecordAction::Clearall,
457                        x => panic!("Record action {} not supported", x),
458                    };
459                }
460                Rule::line_action => {
461                    line_action = match pair.as_str() {
462                        "Continue" => LineAction::Continue(None),
463                        "Next" => LineAction::Next(None),
464                        x => panic!("Record action {} not supported", x),
465                    };
466                }
467                Rule::err_state => {
468                    let mut maybe_err_msg: Option<String> = None;
469                    for p in pair.clone().into_inner() {
470                        if p.as_rule() == Rule::err_msg {
471                            maybe_err_msg = Some(p.as_str().to_string());
472                        }
473                    }
474                    let next_state = NextState::Error(maybe_err_msg);
475                    line_action = LineAction::Next(Some(next_state));
476                }
477                Rule::next_state => {
478                    let next_state = NextState::NamedState(pair.as_str().to_string());
479                    match line_action {
480                        LineAction::Next(None) => {
481                            line_action = LineAction::Next(Some(next_state));
482                        }
483                        LineAction::Continue(None) => {
484                            line_action = LineAction::Continue(Some(next_state));
485                        }
486                        _ => {
487                            panic!(
488                                "Line action {:?} does not support next state (attempted {:?})",
489                                &line_action,
490                                pair.as_str()
491                            );
492                        }
493                    }
494                }
495                x => {
496                    panic!("Rule {:?} not supported!", &x);
497                }
498            }
499        }
500        RuleTransition {
501            record_action,
502            line_action,
503        }
504    }
505    pub fn parse_state_rule(pair: &Pair<'_, Rule>) -> StateRule {
506        let mut rule_match: Option<String> = None;
507        // println!("----- state rule ---");
508        // Self::print_pair(10, pair);
509        // println!("--------");
510        let mut transition: RuleTransition = Default::default();
511        let mut has_action = false;
512        let spaces = "";
513        for pair in pair.clone().into_inner() {
514            match pair.as_rule() {
515                Rule::rule_match => {
516                    rule_match = Some(pair.as_str().to_string());
517                }
518                Rule::transition_action => {
519                    has_action = true;
520                    transition = Self::parse_state_rule_transition(&pair);
521                    // println!("TRANSITION: {:?}", &transition);
522                }
523                x => {
524                    println!("{}state Rule:    {:?}", spaces, pair.as_rule());
525                    println!("{}Span:    {:?}", spaces, pair.as_span());
526                    println!("{}Text:    {}", spaces, pair.as_str());
527                    panic!("state rule {:?} not supported", &x);
528                }
529            }
530        }
531        let mut rule_match = rule_match.expect("rule_match must be always set");
532        if (rule_match.ends_with(" ") || rule_match.ends_with("\t")) && !has_action {
533            println!(
534                "WARNING: '{}' has trailing spaces without transition action!",
535                &rule_match
536            );
537            rule_match = rule_match.trim_end().to_string();
538        }
539        if rule_match.contains(r#"\<"#) {
540            println!("WARNING: replacing \\< with < in '{}'", &rule_match);
541            rule_match = rule_match.replace("\\<", "<");
542        }
543        if rule_match.contains(r#"\>"#) {
544            println!("WARNING: replacing \\> with > in '{}'", &rule_match);
545            rule_match = rule_match.replace("\\>", ">");
546        }
547        StateRule {
548            rule_match,
549            transition,
550        }
551    }
552
553    pub fn compile_state_rule(
554        rule: &StateRule,
555        values: &HashMap<String, ValueDefinition>,
556    ) -> Result<StateRuleCompiled> {
557        let mut expanded_rule_match: String = String::new();
558        let rule_match = rule.rule_match.clone();
559        let mut captured_vars: Vec<CapturedVariable> = vec![];
560        let varsubst = varsubst::VariableParser::parse_dollar_string(&rule_match)
561            .map_err(|e| TextFsmError::ParseError(e.to_string()))?;
562        // println!("DOLLAR STR: {:?}", &varsubst);
563        {
564            use varsubst::ParseChunk;
565            for i in &varsubst {
566                match i {
567                    ParseChunk::DollarDollar => expanded_rule_match.push('$'),
568                    ParseChunk::Text(s) => expanded_rule_match.push_str(s),
569                    ParseChunk::Variable(v) => match values.get(v) {
570                        Some(val) => {
571                            let v_out = format!("(?P<{}>{})", v, val.regex_pattern);
572                            expanded_rule_match.push_str(&v_out);
573                            captured_vars.push(CapturedVariable {
574                                name: v.clone(),
575                                is_list: val.is_list,
576                                is_key: val.is_key,
577                                is_filldown: val.is_filldown,
578                                is_fillup: val.is_fillup,
579                            });
580                        }
581                        None => {
582                            return Err(TextFsmError::ParseError(format!(
583                                "Can not find variable '{}' while parsing rule_match '{}'",
584                                &v, &rule.rule_match
585                            )));
586                        }
587                    },
588                }
589            }
590        }
591        // println!("OUT_STR: {}", expanded_rule_match);
592
593        let regex_val = match Regex::new(&expanded_rule_match) {
594            Ok(r) => MultiRegex::Classic(r),
595            Err(_e) => {
596                use fancy_regex::Error;
597                use fancy_regex::ParseError;
598
599                let freg = loop {
600                    let fancy_regex = fancy_regex::Regex::new(&expanded_rule_match);
601                    match fancy_regex {
602                        Ok(x) => {
603                            break x;
604                        }
605                        Err(Error::ParseError(pos, e)) => {
606                            println!("STR:{}", &expanded_rule_match[0..pos + 1]);
607                            println!("ERR:{}^", " ".repeat(pos));
608                            match e {
609                                ParseError::TargetNotRepeatable => {
610                                    if let Some(char_index) =
611                                        expanded_rule_match.char_indices().nth(pos)
612                                    {
613                                        println!(
614                                            "WARNING: repeat quantifier on a lookahead, lookbehind or other zero-width item"
615                                        );
616                                        expanded_rule_match.remove(char_index.0);
617                                    } else {
618                                        return Err(TextFsmError::ParseError(
619                                            "Can not fix up regex!".to_string(),
620                                        ));
621                                    }
622                                }
623                                e => {
624                                    return Err(TextFsmError::ParseError(format!(
625                                        "Error: {:?}",
626                                        &e
627                                    )));
628                                }
629                            }
630                        }
631                        x => {
632                            return Err(TextFsmError::ParseError(format!("Error: {:?}", &x)));
633                        }
634                    }
635                };
636                MultiRegex::Fancy(freg)
637            }
638        };
639        let maybe_regex = Some(regex_val);
640        let transition = rule.transition.clone();
641        let _rule_match = rule_match;
642        let _expanded_rule_match = expanded_rule_match;
643
644        Ok(StateRuleCompiled {
645            _rule_match,
646            _expanded_rule_match,
647            captured_vars,
648            maybe_regex,
649            transition,
650        })
651    }
652    pub fn parse_and_compile_state_definition(
653        pair: &Pair<'_, Rule>,
654        values: &HashMap<String, ValueDefinition>,
655    ) -> Result<StateCompiled> {
656        let mut name: Option<String> = None;
657        // Self::print_pair(20, pair);
658        let mut rules: Vec<StateRuleCompiled> = vec![];
659
660        for pair in pair.clone().into_inner() {
661            match pair.as_rule() {
662                Rule::state_header => {
663                    name = Some(pair.as_str().to_string());
664                    // println!("SET STATE NAME: {:?}", &state_name);
665                }
666                Rule::rules => {
667                    for pair in pair.clone().into_inner() {
668                        let rule = Self::parse_state_rule(&pair);
669                        trace!("PARSED RULE [{:?}]: {:#?}", &name, &rule);
670                        let compiled_rule = Self::compile_state_rule(&rule, values)?;
671                        rules.push(compiled_rule);
672                    }
673                }
674                x => {
675                    let spaces = "";
676                    println!("{}state def Rule:    {:?}", spaces, pair.as_rule());
677                    println!("{}Span:    {:?}", spaces, pair.as_span());
678                    println!("{}Text:    {}", spaces, pair.as_str());
679                    return Err(TextFsmError::ParseError(format!(
680                        "Rule not supported in state definition: {:?}",
681                        &x
682                    )));
683                }
684            }
685        }
686        let name =
687            name.ok_or_else(|| TextFsmError::InternalError("state must have a name".to_string()))?;
688        Ok(StateCompiled { name, rules })
689    }
690
691    pub fn parse_value_definition(pair: &Pair<'_, Rule>) -> Result<ValueDefinition> {
692        // println!("value definition");
693        let mut name: Option<String> = None;
694        let mut regex_pattern: Option<String> = None;
695        let mut options: Option<String> = None;
696        let mut is_filldown = false;
697        let mut is_key = false;
698        let mut is_required = false;
699        let mut is_list = false;
700        let mut is_fillup = false;
701
702        for p in pair.clone().into_inner() {
703            match p.as_rule() {
704                Rule::options => options = Some(p.as_str().to_string()),
705                Rule::identifier => name = Some(p.as_str().to_string()),
706                Rule::regex_pattern => {
707                    regex_pattern = Some(p.as_str().to_string());
708                }
709                x => {
710                    return Err(TextFsmError::ParseError(format!(
711                        "Rule {:?} in value definition",
712                        x
713                    )));
714                }
715            }
716            // Self::print_pair(indent + 2, &p);
717        }
718        if let (Some(name), Some(mut regex_pattern)) = (name.clone(), regex_pattern.clone()) {
719            if let Some(ref opts) = options {
720                let opts = opts.split(",");
721                for word in opts {
722                    match word {
723                        "Filldown" => is_filldown = true,
724                        "Key" => is_key = true,
725                        "Required" => is_required = true,
726                        "List" => is_list = true,
727                        "Fillup" => is_fillup = true,
728                        x => {
729                            return Err(TextFsmError::ParseError(format!(
730                                "Unknown option {:?}",
731                                &x
732                            )));
733                        }
734                    }
735                }
736            }
737            if regex_pattern.contains(r#"\<"#) {
738                println!("WARNING: replacing \\< with < in value '{}'", &name);
739                regex_pattern = regex_pattern.replace("\\<", "<");
740            }
741            if regex_pattern.contains(r#"\>"#) {
742                println!("WARNING: replacing \\> with > in value '{}'", &name);
743                regex_pattern = regex_pattern.replace("\\>", ">");
744            }
745            Ok(ValueDefinition {
746                name,
747                regex_pattern,
748                is_filldown,
749                is_key,
750                is_required,
751                is_list,
752                is_fillup,
753                options,
754            })
755        } else {
756            Err(TextFsmError::ParseError(format!(
757                "Error parsing value: {:?} {:?} [ {:?} ]",
758                &name, &regex_pattern, &options
759            )))
760        }
761    }
762    pub fn parse_value_defs(
763        pair: &Pair<'_, Rule>,
764    ) -> Result<(HashMap<String, ValueDefinition>, Vec<String>)> {
765        let mut vals = HashMap::new();
766        let mut mandatory_values: Vec<String> = vec![];
767        for pair in pair.clone().into_inner() {
768            if Rule::value_definition == pair.as_rule() {
769                let val = Self::parse_value_definition(&pair)?;
770                if val.is_required {
771                    mandatory_values.push(val.name.clone());
772                }
773                vals.insert(val.name.clone(), val);
774            }
775        }
776        Ok((vals, mandatory_values))
777    }
778
779    /// Parses and compiles a TextFSM template from a string.
780    pub fn from_string(content: &str) -> Result<Self> {
781        let mut template = content.to_string();
782        // pad with newlines, because dealing with a missing one within grammar is a PITA
783        if !template.ends_with('\n') {
784            template.push('\n');
785        }
786        template.push_str("\n\n");
787
788        let mut seen_eoi = false;
789        let mut values: HashMap<String, ValueDefinition> = HashMap::new();
790        let mut states: HashMap<String, StateCompiled> = HashMap::new();
791        let mut mandatory_values: Vec<String> = vec![];
792
793        let end_state = NextState::NamedState("End".to_string());
794        let eof_rule = StateRule {
795            rule_match: ".*".to_string(),
796            transition: RuleTransition {
797                line_action: LineAction::Next(Some(end_state)),
798                record_action: RecordAction::Record,
799            },
800        };
801
802        let compiled_eof_rule = Self::compile_state_rule(&eof_rule, &values)?;
803
804        let eof_state = StateCompiled {
805            name: "EOF".to_string(),
806            rules: vec![compiled_eof_rule],
807        };
808        states.insert(eof_state.name.clone(), eof_state);
809
810        match TextFSMParser::parse(Rule::file, &template) {
811            Ok(pairs) => {
812                for pair in pairs.clone() {
813                    match pair.as_rule() {
814                        Rule::value_definitions => {
815                            (values, mandatory_values) = Self::parse_value_defs(&pair)?;
816                        }
817                        Rule::state_definitions => {
818                            for pair in pair.clone().into_inner() {
819                                match pair.as_rule() {
820                                    Rule::state_definition => {
821                                        trace!("STATE DEFINITION");
822                                        Self::_log_pair(0, &pair);
823                                        let state = Self::parse_and_compile_state_definition(
824                                            &pair, &values,
825                                        )?;
826                                        trace!("STATE DEFINITION END: {:?}", &state);
827                                        if &state.name != "EOF" && states.contains_key(&state.name)
828                                        {
829                                            return Err(TextFsmError::StateError(format!(
830                                                "State {} already defined in the file!",
831                                                &state.name
832                                            )));
833                                        }
834                                        states.insert(state.name.clone(), state);
835                                    }
836                                    x => {
837                                        return Err(TextFsmError::ParseError(format!(
838                                            "state definition rule {:?} not supported",
839                                            x
840                                        )));
841                                    }
842                                }
843                            }
844                        }
845                        Rule::EOI => {
846                            seen_eoi = true;
847                        }
848                        x => {
849                            return Err(TextFsmError::ParseError(format!(
850                                "RULE {:?} not supported",
851                                &x
852                            )));
853                        }
854                    }
855                    // Self::process_pair(0, &pair);
856                }
857
858                if !seen_eoi {
859                    println!("WARNING: EOI token not seen");
860                }
861
862                if !states.contains_key("Start") {
863                    return Err(TextFsmError::StateError(
864                        "Start state not found".to_string(),
865                    ));
866                }
867
868                Ok(TextFSMParser {
869                    values,
870                    mandatory_values,
871                    states,
872                })
873            }
874            Err(e) => Err(TextFsmError::ParseError(format!("Error: {}", e))),
875        }
876    }
877
878    /// Parses and compiles a TextFSM template from a file.
879    pub fn from_file<P: AsRef<std::path::Path>>(fname: P) -> Result<Self> {
880        let path = fname.as_ref();
881        let content = std::fs::read_to_string(path)?;
882        Self::from_string(&content)
883            .map_err(|e| TextFsmError::ParseError(format!("file {} Error: {}", path.display(), e)))
884    }
885}
886
887impl TextFSM {
888    /// Creates a new `TextFSM` instance from a template string.
889    pub fn from_string(content: &str) -> Result<Self> {
890        let parser = TextFSMParser::from_string(content)?;
891        let curr_state = "Start".to_string();
892        Ok(TextFSM {
893            parser,
894            curr_state,
895            ..Default::default()
896        })
897    }
898
899    /// Creates a new `TextFSM` instance from a template file.
900    pub fn from_file<P: AsRef<std::path::Path>>(fname: P) -> Result<Self> {
901        let parser = TextFSMParser::from_file(fname)?;
902        let curr_state = "Start".to_string();
903        Ok(TextFSM {
904            parser,
905            curr_state,
906            ..Default::default()
907        })
908    }
909
910    /// Resets the engine to its initial state, clearing all records and resetting variables.
911    /// This allows reusing the parsed template for a new file.
912    pub fn reset(&mut self) {
913        self.curr_state = "Start".to_string();
914        self.curr_record = DataRecord::default();
915        self.filldown_record = DataRecord::default();
916        self.records.clear();
917    }
918
919    /// Sets the current state of the engine.
920    pub fn set_curr_state(&mut self, state_name: &str) -> Result<()> {
921        if state_name != "End" && !self.parser.states.contains_key(state_name) {
922            return Err(TextFsmError::StateError(format!(
923                "State '{}' not found!",
924                state_name
925            )));
926        }
927        self.curr_state = state_name.to_string();
928        Ok(())
929    }
930
931    pub fn is_key_value(&self, value_name: &str) -> Option<bool> {
932        self.parser.values.get(value_name).map(|val| val.is_key)
933    }
934
935    pub fn is_filldown_value(&self, value_name: &str) -> Option<bool> {
936        self.parser
937            .values
938            .get(value_name)
939            .map(|val| val.is_filldown)
940    }
941
942    pub fn is_fillup_value(&self, value_name: &str) -> Option<bool> {
943        self.parser.values.get(value_name).map(|val| val.is_fillup)
944    }
945
946    pub fn is_list_value(&self, value_name: &str) -> Option<bool> {
947        self.parser.values.get(value_name).map(|val| val.is_list)
948    }
949
950    /// Optimized value insertion into records.
951    pub fn insert_value_optimized(
952        &self,
953        curr_record: &mut DataRecord,
954        filldown_record: &mut DataRecord,
955        var_info: &CapturedVariable,
956        maybe_value: Option<&str>,
957        aline: &str,
958    ) -> Result<()> {
959        let name = &var_info.name;
960
961        let ins_value = if let Some(value) = maybe_value {
962            trace!("SET VAR '{}' = '{}'", name, value);
963
964            if var_info.is_list {
965                Value::List(vec![value.to_string()])
966            } else {
967                Value::Single(value.to_string())
968            }
969        } else {
970            warn!(
971                "WARNING: Could not capture '{}' from string '{}'",
972                name, aline
973            );
974
975            if var_info.is_list {
976                Value::List(vec![format!("None")])
977            } else {
978                Value::Single(String::new())
979            }
980        };
981
982        curr_record.fields.insert(name.clone(), ins_value.clone());
983
984        if var_info.is_key {
985            curr_record.record_key = if let Some(k) = curr_record.record_key.as_ref() {
986                Some(format!("{}/{:?}", k, &ins_value))
987            } else {
988                Some(format!("{:?}", &ins_value))
989            };
990
991            trace!("RECORD KEY: '{:?}'", &curr_record.record_key);
992        }
993
994        if var_info.is_filldown {
995            filldown_record.fields.insert(name.clone(), ins_value);
996        }
997
998        Ok(())
999    }
1000
1001    fn process_record_action(
1002        curr_record: &mut DataRecord,
1003        filldown_record: &mut DataRecord,
1004        records: &mut VecDeque<DataRecord>,
1005        mandatory_values: &[String],
1006        values: &HashMap<String, ValueDefinition>,
1007        action: RecordAction,
1008    ) -> Result<()> {
1009        match action {
1010            RecordAction::Record => {
1011                let mut mandatory_count = 0;
1012                let number_of_values = curr_record.keys().len();
1013
1014                for k in mandatory_values {
1015                    if curr_record.get(k).is_some() {
1016                        mandatory_count += 1;
1017                    }
1018                }
1019                if number_of_values > 0 {
1020                    if mandatory_count == mandatory_values.len() {
1021                        let mut new_rec: DataRecord = filldown_record.clone();
1022                        /* swap with the current record */
1023                        std::mem::swap(&mut new_rec, curr_record);
1024                        // Set the values that aren't set yet - FIXME: this feature should be
1025                        // possible to be disabled as "" and nothing are very different things.
1026                        for v in values.values() {
1027                            if new_rec.get(&v.name).is_none() {
1028                                if v.is_list {
1029                                    new_rec.fields.insert(v.name.clone(), Value::List(vec![]));
1030                                } else {
1031                                    new_rec
1032                                        .fields
1033                                        .insert(v.name.clone(), Value::Single(String::new()));
1034                                }
1035                            }
1036                        }
1037                        trace!("RECORD: {:?}", &new_rec);
1038                        records.push_back(new_rec);
1039                    } else {
1040                        trace!("RECORD: no required fields set");
1041                    }
1042                } else {
1043                    trace!("RECORD: record is empty, not dumping");
1044                }
1045            }
1046            RecordAction::NoRecord => {} // Do nothing
1047            RecordAction::Clear => {
1048                let mut rem_keys: Vec<String> = vec![];
1049                for (ref k, _v) in curr_record.iter() {
1050                    if let Some(val) = values.get(*k) {
1051                        if !val.is_filldown {
1052                            rem_keys.push(k.to_string());
1053                        }
1054                    } else {
1055                        return Err(TextFsmError::InternalError(format!(
1056                            "is_filldown_value for {} failed",
1057                            k
1058                        )));
1059                    }
1060                }
1061                for k in rem_keys {
1062                    curr_record.remove(&k);
1063                }
1064            }
1065            RecordAction::Clearall => {
1066                // reset the current record
1067                *curr_record = Default::default();
1068                *filldown_record = Default::default();
1069            }
1070        }
1071        Ok(())
1072    }
1073
1074    /// Processes a single line of input against the current state's rules.
1075    pub fn parse_line(&mut self, aline: &str) -> Result<ParseStatus> {
1076        // Reuse these record structures to avoid reallocating on every rule match
1077        let mut tmp_datarec = DataRecord::new();
1078        let mut tmp_filldown_rec = DataRecord::new();
1079        // Keep track of which fields are fillup to avoid lookups later
1080        let mut fillup_fields: Vec<String> = vec![];
1081
1082        let state_name = &self.curr_state;
1083        let state_def = self.parser.states.get(state_name);
1084
1085        if let Some(curr_state) = state_def {
1086            trace!("CURR STATE: {:?}", &curr_state);
1087            for rule in &curr_state.rules {
1088                let mut transition = RuleTransition {
1089                    line_action: LineAction::Continue(None),
1090                    ..Default::default()
1091                };
1092                trace!("TRY RULE: {:?}", &rule);
1093                let mut capture_matched = false;
1094                tmp_datarec.fields.clear();
1095                tmp_datarec.record_key = None;
1096                tmp_filldown_rec.fields.clear();
1097                fillup_fields.clear();
1098
1099                match &rule.maybe_regex {
1100                    Some(MultiRegex::Classic(rx)) => {
1101                        debug!("RULE(CLASSIC REGEX): {:?}", &rule);
1102                        if let Some(caps) = rx.captures(aline) {
1103                            for var in &rule.captured_vars {
1104                                let maybe_value = caps.name(&var.name).map(|x| x.as_str());
1105                                self.insert_value_optimized(
1106                                    &mut tmp_datarec,
1107                                    &mut tmp_filldown_rec,
1108                                    var,
1109                                    maybe_value,
1110                                    aline,
1111                                )?;
1112                                if var.is_fillup {
1113                                    fillup_fields.push(var.name.clone());
1114                                }
1115                            }
1116                            capture_matched = true;
1117                        }
1118                    }
1119                    Some(MultiRegex::Fancy(rx)) => {
1120                        debug!("RULE(FANCY REGEX): {:?}", &rule);
1121                        if let Ok(Some(caps)) = rx.captures(aline) {
1122                            for var in &rule.captured_vars {
1123                                let maybe_value = caps.name(&var.name).map(|x| x.as_str());
1124                                self.insert_value_optimized(
1125                                    &mut tmp_datarec,
1126                                    &mut tmp_filldown_rec,
1127                                    var,
1128                                    maybe_value,
1129                                    aline,
1130                                )?;
1131                                if var.is_fillup {
1132                                    fillup_fields.push(var.name.clone());
1133                                }
1134                            }
1135                            capture_matched = true;
1136                        }
1137                    }
1138                    x => {
1139                        return Err(TextFsmError::ParseError(format!(
1140                            "Regex {:?} on rule is not supported",
1141                            &x
1142                        )));
1143                    }
1144                }
1145                if capture_matched {
1146                    trace!("TMP_REC: {:?}", &tmp_datarec);
1147                    trace!("TMP_FILLDOWN: {:?}", &tmp_filldown_rec);
1148                    for (name, v) in tmp_datarec.fields.drain() {
1149                        if fillup_fields.contains(&name) {
1150                            let name_ref = &name;
1151                            for fillup_record in self.records.iter_mut().rev() {
1152                                if let Some(ref oldval) = fillup_record.fields.get(name_ref) {
1153                                    match oldval {
1154                                        Value::Single(s) => {
1155                                            if !s.is_empty() {
1156                                                break;
1157                                            }
1158                                        }
1159                                        Value::List(_lst) => {
1160                                            return Err(TextFsmError::ParseError(
1161                                                "fillup not supported for lists!".to_string(),
1162                                            ));
1163                                        }
1164                                    }
1165                                }
1166                                fillup_record.fields.insert(name.clone(), v.clone());
1167                            }
1168                        }
1169                        self.curr_record.append_value(name, v);
1170                    }
1171                    trace!("TMP KEY: {:?}", &tmp_datarec.record_key);
1172                    self.curr_record.record_key = tmp_datarec.record_key;
1173                    // The below is incorrect:
1174                    // self.filldown_record.overwrite_from(tmp_filldown_rec);
1175                    // This is correct:
1176                    for (name, v) in tmp_filldown_rec.fields.drain() {
1177                        self.filldown_record.append_value(name, v);
1178                    }
1179                    transition = rule.transition.clone();
1180                }
1181                // println!("TRANS: {:?}", &transition);
1182
1183                Self::process_record_action(
1184                    &mut self.curr_record,
1185                    &mut self.filldown_record,
1186                    &mut self.records,
1187                    &self.parser.mandatory_values,
1188                    &self.parser.values,
1189                    transition.record_action,
1190                )?;
1191
1192                match transition.line_action {
1193                    LineAction::Next(x) => return Ok(ParseStatus::NextLine(x)),
1194                    LineAction::Continue(maybe_next_state) => {
1195                        if let Some(next_state) = maybe_next_state {
1196                            return Ok(ParseStatus::SameLine(Some(next_state)));
1197                        }
1198                    } // Do nothing
1199                }
1200            }
1201        } else {
1202            return Err(TextFsmError::StateError(format!(
1203                "State {} not found!",
1204                &self.curr_state
1205            )));
1206        }
1207        Ok(ParseStatus::NextLine(None))
1208    }
1209
1210    /// Returns a new vector of records with all field names converted to lowercase.
1211    pub fn lowercase_keys(src: &VecDeque<DataRecord>) -> Vec<DataRecord> {
1212        let mut out = vec![];
1213
1214        for irec in src {
1215            let mut hm = DataRecord::new();
1216            hm.record_key = irec.record_key.clone();
1217            for (k, v) in irec.iter() {
1218                let kl = k.to_lowercase();
1219                hm.fields.insert(kl, v.clone());
1220            }
1221            out.push(hm);
1222        }
1223        out
1224    }
1225
1226    /// Parses input from a reader line-by-line using an iterator.
1227    /// This is memory efficient for large files as it doesn't buffer all records.
1228    ///
1229    /// # Arguments
1230    /// * `reader` - A type implementing `BufRead` (e.g., `BufReader<File>`).
1231    pub fn parse_reader<R: std::io::BufRead>(self, reader: R) -> TextFsmIter<R> {
1232        TextFsmIter {
1233            fsm: self,
1234            lines: reader.lines(),
1235            eof_processed: false,
1236            current_line: None,
1237        }
1238    }
1239
1240    /// Parses input from a string.
1241    ///
1242    /// # Arguments
1243    /// * `input` - The input string to parse.
1244    /// * `conversion` - Optional transformation to apply to the results.
1245    pub fn parse_string(
1246        &mut self,
1247        input: &str,
1248        conversion: Option<DataRecordConversion>,
1249    ) -> Result<Vec<DataRecord>> {
1250        for (_lineno, aline) in input.lines().enumerate() {
1251            debug!("LINE:#{}: '{}'", _lineno + 1, &aline);
1252            loop {
1253                let status = self.parse_line(aline)?;
1254                match status {
1255                    ParseStatus::NextLine(maybe_next_state) => {
1256                        if let Some(next_state) = maybe_next_state {
1257                            match next_state {
1258                                NextState::Error(maybe_msg) => {
1259                                    return Err(TextFsmError::StateError(format!(
1260                                        "Error state reached! msg: {:?}",
1261                                        &maybe_msg
1262                                    )));
1263                                }
1264                                NextState::NamedState(name) => {
1265                                    self.set_curr_state(&name)?;
1266                                }
1267                            }
1268                        }
1269                        break;
1270                    }
1271                    ParseStatus::SameLine(maybe_next_state) => {
1272                        if let Some(next_state) = maybe_next_state {
1273                            match next_state {
1274                                NextState::Error(maybe_msg) => {
1275                                    return Err(TextFsmError::StateError(format!(
1276                                        "Error state reached! msg: {:?}",
1277                                        &maybe_msg
1278                                    )));
1279                                }
1280                                NextState::NamedState(name) => {
1281                                    self.set_curr_state(&name)?;
1282                                }
1283                            }
1284                        }
1285                    }
1286                }
1287            }
1288            if &self.curr_state == "EOF" || &self.curr_state == "End" {
1289                break;
1290            }
1291        }
1292        if &self.curr_state != "End" {
1293            self.set_curr_state("EOF")?;
1294            self.parse_line("")?;
1295            // FIXME: Can EOF state transition into something else ? Presumably not.
1296            self.set_curr_state("End")?;
1297        }
1298        match conversion {
1299            None => Ok(self.records.clone().into()),
1300            Some(DataRecordConversion::LowercaseKeys) => Ok(Self::lowercase_keys(&self.records)),
1301        }
1302    }
1303
1304    /// Parses an entire file and returns the extracted records.
1305    ///
1306    /// # Arguments
1307    /// * `fname` - Path to the data file to parse.
1308    /// * `conversion` - Optional transformation to apply to the results.
1309    pub fn parse_file<P: AsRef<std::path::Path>>(
1310        &mut self,
1311        fname: P,
1312        conversion: Option<DataRecordConversion>,
1313    ) -> Result<Vec<DataRecord>> {
1314        let input = std::fs::read_to_string(fname)?;
1315        self.parse_string(&input, conversion)
1316    }
1317}