Skip to main content

textfsm_core/parser/
mod.rs

1//! FSM parser for processing input text.
2
3mod state;
4
5pub use state::ValueState;
6
7use std::collections::HashMap;
8
9use crate::error::ParseError;
10use crate::template::Template;
11use crate::types::{LineOp, RecordOp, Transition, Value};
12
13/// Parser for processing text with a compiled template.
14pub struct Parser<'t> {
15    /// Reference to the compiled template.
16    template: &'t Template,
17
18    /// Current state name.
19    current_state: String,
20
21    /// Runtime state for each value.
22    value_states: Vec<ValueState>,
23
24    /// Accumulated results.
25    results: Vec<Vec<Value>>,
26}
27
28impl<'t> Parser<'t> {
29    /// Create a new parser for the given template.
30    pub fn new(template: &'t Template) -> Self {
31        let value_states = template
32            .values()
33            .iter()
34            .enumerate()
35            .map(|(idx, def)| ValueState::new(def.clone(), idx))
36            .collect();
37
38        Self {
39            template,
40            current_state: "Start".to_string(),
41            value_states,
42            results: Vec::new(),
43        }
44    }
45
46    /// Reset the parser state for reuse.
47    pub fn reset(&mut self) {
48        self.current_state = "Start".to_string();
49        self.results.clear();
50        for vs in &mut self.value_states {
51            vs.clear_all();
52        }
53    }
54
55    /// Parse text and return list of records.
56    pub fn parse_text(&mut self, text: &str) -> Result<Vec<Vec<Value>>, ParseError> {
57        self.parse_text_with_eof(text, true)
58    }
59
60    /// Parse text with explicit EOF control.
61    pub fn parse_text_with_eof(
62        &mut self,
63        text: &str,
64        eof: bool,
65    ) -> Result<Vec<Vec<Value>>, ParseError> {
66        for line in text.lines() {
67            self.process_line(line)?;
68
69            if self.current_state == "End" || self.current_state == "EOF" {
70                break;
71            }
72        }
73
74        // Implicit EOF behavior
75        if self.current_state != "End"
76            && !self.template.get_state("EOF").is_some()
77            && eof
78        {
79            self.append_record();
80        }
81
82        Ok(std::mem::take(&mut self.results))
83    }
84
85    /// Parse text and return results as list of dicts.
86    pub fn parse_text_to_dicts(
87        &mut self,
88        text: &str,
89    ) -> Result<Vec<HashMap<String, String>>, ParseError> {
90        let results = self.parse_text(text)?;
91        let header = self.template.header();
92
93        Ok(results
94            .into_iter()
95            .map(|row| {
96                header
97                    .iter()
98                    .zip(row.into_iter())
99                    .map(|(k, v)| (k.to_lowercase(), v.as_string()))
100                    .collect()
101            })
102            .collect())
103    }
104
105    /// Process a single input line.
106    fn process_line(&mut self, line: &str) -> Result<(), ParseError> {
107        let state = match self.template.get_state(&self.current_state) {
108            Some(s) => s,
109            None => return Ok(()), // End/EOF state
110        };
111
112        for rule in &state.rules {
113            if let Ok(Some(captures)) = rule.regex.captures(line) {
114                // Extract matched values
115                for vs in &mut self.value_states {
116                    if let Some(matched) = captures.name(&vs.def.name) {
117                        vs.assign(matched.as_str().to_string(), &mut self.results);
118                    }
119                }
120
121                // Apply record operation
122                match rule.record_op {
123                    RecordOp::Record => self.append_record(),
124                    RecordOp::Clear => self.clear_values(),
125                    RecordOp::ClearAll => self.clear_all_values(),
126                    RecordOp::NoRecord => {}
127                }
128
129                // Apply line operation
130                match rule.line_op {
131                    LineOp::Error => {
132                        let message = match &rule.transition {
133                            Transition::State(msg) => msg.clone(),
134                            _ => "state error".into(),
135                        };
136                        return Err(ParseError::RuleError {
137                            rule_line: rule.line_num,
138                            message,
139                        });
140                    }
141                    LineOp::Continue => {
142                        // Don't break, continue checking rules
143                        continue;
144                    }
145                    LineOp::Next => {
146                        // Apply state transition and break
147                        self.apply_transition(&rule.transition);
148                        break;
149                    }
150                }
151            }
152        }
153
154        Ok(())
155    }
156
157    /// Append current record to results.
158    fn append_record(&mut self) {
159        // Check Required constraints
160        for vs in &self.value_states {
161            if !vs.satisfies_required() {
162                // Skip record
163                self.clear_values();
164                return;
165            }
166        }
167
168        // Build record
169        let record: Vec<Value> = self
170            .value_states
171            .iter_mut()
172            .map(|vs| vs.take_for_record())
173            .collect();
174
175        // Don't record if all empty
176        if record.iter().all(|v| v.is_empty()) {
177            return;
178        }
179
180        self.results.push(record);
181        self.clear_values();
182    }
183
184    /// Clear non-Filldown values.
185    fn clear_values(&mut self) {
186        for vs in &mut self.value_states {
187            vs.clear();
188        }
189    }
190
191    /// Clear all values including Filldown.
192    fn clear_all_values(&mut self) {
193        for vs in &mut self.value_states {
194            vs.clear_all();
195        }
196    }
197
198    /// Apply a state transition.
199    fn apply_transition(&mut self, transition: &Transition) {
200        match transition {
201            Transition::Stay => {}
202            Transition::State(name) => {
203                self.current_state = name.clone();
204            }
205            Transition::End => {
206                self.current_state = "End".to_string();
207            }
208            Transition::Eof => {
209                self.current_state = "EOF".to_string();
210            }
211        }
212    }
213}
214
215#[cfg(test)]
216mod tests {
217    use super::*;
218    use crate::template::Template;
219
220    #[test]
221    fn test_simple_parse() {
222        let template_str = r#"
223Value Interface (\S+)
224Value Status (up|down)
225
226Start
227  ^Interface: ${Interface} is ${Status} -> Record
228"#;
229
230        let template = Template::parse_str(template_str).unwrap();
231        let mut parser = template.parser();
232
233        let input = "Interface: eth0 is up\nInterface: eth1 is down\n";
234        let results = parser.parse_text(input).unwrap();
235
236        assert_eq!(results.len(), 2);
237        assert_eq!(results[0][0], Value::Single("eth0".into()));
238        assert_eq!(results[0][1], Value::Single("up".into()));
239        assert_eq!(results[1][0], Value::Single("eth1".into()));
240        assert_eq!(results[1][1], Value::Single("down".into()));
241    }
242
243    #[test]
244    fn test_parse_to_dicts() {
245        let template_str = r#"
246Value Name (\S+)
247Value Age (\d+)
248
249Start
250  ^Name: ${Name}, Age: ${Age} -> Record
251"#;
252
253        let template = Template::parse_str(template_str).unwrap();
254        let mut parser = template.parser();
255
256        let input = "Name: Alice, Age: 30\nName: Bob, Age: 25\n";
257        let results = parser.parse_text_to_dicts(input).unwrap();
258
259        assert_eq!(results.len(), 2);
260        assert_eq!(results[0].get("name"), Some(&"Alice".to_string()));
261        assert_eq!(results[0].get("age"), Some(&"30".to_string()));
262    }
263
264    #[test]
265    fn test_required_skips_empty() {
266        let template_str = r#"
267Value Required Name (\S+)
268Value Optional (\S+)
269
270Start
271  ^Name: ${Name}
272  ^Optional: ${Optional}
273  ^--- -> Record
274"#;
275
276        let template = Template::parse_str(template_str).unwrap();
277        let mut parser = template.parser();
278
279        // Record with no Name should be skipped
280        let input = "Optional: foo\n---\nName: bar\n---\n";
281        let results = parser.parse_text(input).unwrap();
282
283        assert_eq!(results.len(), 1);
284        assert_eq!(results[0][0], Value::Single("bar".into()));
285    }
286}