1mod completion;
21mod error;
22mod index;
23mod table;
24
25pub use completion::expand_completion;
26pub use error::CliTableError;
27pub use index::{Index, IndexEntry};
28pub use table::{Row, TextTable};
29
30use std::collections::HashMap;
31use std::path::{Path, PathBuf};
32use std::sync::{Arc, RwLock};
33
34use crate::Template;
35
36pub struct CliTable {
40 index: Arc<Index>,
42
43 template_dir: PathBuf,
45
46 template_cache: RwLock<HashMap<PathBuf, Arc<Template>>>,
48}
49
50impl CliTable {
51 pub fn new<P1: AsRef<Path>, P2: AsRef<Path>>(
58 index_path: P1,
59 template_dir: P2,
60 ) -> Result<Self, CliTableError> {
61 let index = Index::from_file(index_path)?;
62 Ok(Self {
63 index: Arc::new(index),
64 template_dir: template_dir.as_ref().to_path_buf(),
65 template_cache: RwLock::new(HashMap::new()),
66 })
67 }
68
69 pub fn from_index(index: Arc<Index>, template_dir: PathBuf) -> Self {
71 Self {
72 index,
73 template_dir,
74 template_cache: RwLock::new(HashMap::new()),
75 }
76 }
77
78 pub fn parse_cmd(
85 &self,
86 text: &str,
87 attributes: &HashMap<String, String>,
88 ) -> Result<TextTable, CliTableError> {
89 let entry = self
90 .index
91 .find_match(attributes)
92 .ok_or_else(|| CliTableError::NoMatch(attributes.clone()))?;
93
94 self.parse_with_templates(text, entry.templates())
95 }
96
97 pub fn parse_with_templates(
99 &self,
100 text: &str,
101 template_names: &[String],
102 ) -> Result<TextTable, CliTableError> {
103 if template_names.len() == 1 {
104 let template = self.load_template(&template_names[0])?;
106 let mut parser = template.parser();
107 let results = parser.parse_text(text)?;
108 let header: Vec<String> = template.header().iter().map(|s| s.to_string()).collect();
109 Ok(TextTable::from_values(header, results))
110 } else {
111 self.parse_and_merge(text, template_names)
113 }
114 }
115
116 pub fn find_templates(
118 &self,
119 attributes: &HashMap<String, String>,
120 ) -> Result<Vec<PathBuf>, CliTableError> {
121 let entry = self
122 .index
123 .find_match(attributes)
124 .ok_or_else(|| CliTableError::NoMatch(attributes.clone()))?;
125
126 Ok(entry
127 .templates()
128 .iter()
129 .map(|name| self.template_dir.join(name))
130 .collect())
131 }
132
133 pub fn index(&self) -> &Index {
135 &self.index
136 }
137
138 pub fn clear_cache(&self) {
140 let mut cache = self.template_cache.write().unwrap();
141 cache.clear();
142 }
143
144 fn load_template(&self, name: &str) -> Result<Arc<Template>, CliTableError> {
146 let path = self.template_dir.join(name);
147
148 {
150 let cache = self.template_cache.read().unwrap();
151 if let Some(template) = cache.get(&path) {
152 return Ok(Arc::clone(template));
153 }
154 }
155
156 let content = std::fs::read_to_string(&path)
158 .map_err(|_| CliTableError::TemplateNotFound(path.clone()))?;
159 let template = Template::parse_str(&content)?;
160 let template = Arc::new(template);
161
162 {
164 let mut cache = self.template_cache.write().unwrap();
165 cache.insert(path, Arc::clone(&template));
166 }
167
168 Ok(template)
169 }
170
171 fn parse_and_merge(
183 &self,
184 text: &str,
185 template_names: &[String],
186 ) -> Result<TextTable, CliTableError> {
187 use crate::types::ValueOption;
188 use indexmap::IndexSet;
189
190 type TemplateResults = (Arc<Template>, Vec<Vec<crate::Value>>, IndexSet<String>);
192
193 let mut all_results: Vec<TemplateResults> = Vec::new();
195
196 for name in template_names {
197 let template = self.load_template(name)?;
198 let mut parser = template.parser();
199 let results = parser.parse_text(text)?;
200
201 let template_keys: IndexSet<String> = template
203 .values()
204 .iter()
205 .filter(|v| v.has_option(ValueOption::Key))
206 .map(|v| v.name.clone())
207 .collect();
208
209 all_results.push((template, results, template_keys));
210 }
211
212 let shared_keys: IndexSet<String> = if all_results.is_empty() {
215 IndexSet::new()
216 } else {
217 let first_keys = &all_results[0].2;
218 all_results
219 .iter()
220 .skip(1)
221 .fold(first_keys.clone(), |acc, (_, _, keys)| {
222 acc.intersection(keys).cloned().collect()
223 })
224 };
225
226 if all_results.len() == 1 || shared_keys.is_empty() {
228 let (template, results, _) = all_results.into_iter().next().unwrap();
229 let header: Vec<String> = template.header().iter().map(|s| s.to_string()).collect();
230 return Ok(TextTable::from_values(header, results));
231 }
232
233 let mut unified_header: Vec<String> = Vec::new();
235 let mut header_index: HashMap<String, usize> = HashMap::new();
236
237 for (template, _, _) in &all_results {
238 for name in template.header() {
239 if let std::collections::hash_map::Entry::Vacant(e) = header_index.entry(name.to_string()) {
240 e.insert(unified_header.len());
241 unified_header.push(name.to_string());
242 }
243 }
244 }
245
246 let mut all_results_iter = all_results.into_iter();
248 let (first_template, first_results, _) = all_results_iter.next().unwrap();
249 let first_header: Vec<&str> = first_template.header();
250
251 let mut merged: indexmap::IndexMap<Vec<String>, Vec<crate::Value>> = indexmap::IndexMap::new();
254
255 for row in first_results {
256 let key = extract_key(&row, &first_header, &shared_keys);
257 let mut unified_row = vec![crate::Value::Empty; unified_header.len()];
258
259 for (i, value) in row.into_iter().enumerate() {
261 if i < first_header.len()
262 && let Some(&unified_idx) = header_index.get(first_header[i])
263 {
264 unified_row[unified_idx] = value;
265 }
266 }
267
268 merged.insert(key, unified_row);
269 }
270
271 for (template, results, _) in all_results_iter {
273 let template_header: Vec<&str> = template.header();
274
275 for row in results {
276 let key = extract_key(&row, &template_header, &shared_keys);
277
278 if let Some(merged_row) = merged.get_mut(&key) {
280 for (i, value) in row.into_iter().enumerate() {
282 if !value.is_empty()
283 && i < template_header.len()
284 && let Some(&unified_idx) = header_index.get(template_header[i])
285 && merged_row[unified_idx].is_empty()
286 {
287 merged_row[unified_idx] = value;
288 }
289 }
290 }
291 }
293 }
294
295 let rows: Vec<Vec<crate::Value>> = merged.into_values().collect();
297 let mut table = TextTable::from_values(unified_header, rows);
298
299 let superkey: Vec<String> = shared_keys.into_iter().collect();
301 table.set_superkey(superkey);
302 table.sort();
303
304 Ok(table)
305 }
306}
307
308fn extract_key(
311 row: &[crate::Value],
312 header: &[&str],
313 shared_keys: &indexmap::IndexSet<String>,
314) -> Vec<String> {
315 shared_keys
316 .iter()
317 .map(|key_col| {
318 header
319 .iter()
320 .position(|h| *h == key_col)
321 .and_then(|idx| row.get(idx))
322 .map(|v| normalize_key_value(&v.as_string()))
323 .unwrap_or_default()
324 })
325 .collect()
326}
327
328fn normalize_key_value(s: &str) -> String {
332 let trimmed = s.trim();
333
334 if let Ok(n) = trimmed.parse::<i64>() {
337 return n.to_string();
338 }
339
340 trimmed.to_string()
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use std::sync::Arc;
347 use std::thread;
348
349 fn _assert_send_sync() {
351 fn assert_send<T: Send>() {}
352 fn assert_sync<T: Sync>() {}
353 assert_send::<CliTable>();
354 assert_sync::<CliTable>();
355 }
356
357 #[test]
358 fn test_concurrent_parsing() {
359 let temp_dir = std::env::temp_dir().join("textfsm_concurrency_test");
361 let _ = std::fs::remove_dir_all(&temp_dir);
362 std::fs::create_dir_all(&temp_dir).unwrap();
363
364 let template_content = r#"Value Name (\S+)
366Value Age (\d+)
367
368Start
369 ^Name: ${Name}, Age: ${Age} -> Record
370"#;
371 std::fs::write(temp_dir.join("test_template.textfsm"), template_content).unwrap();
372
373 let index_content = "Template, Platform, Command\ntest_template.textfsm, .*, show users\n";
375 std::fs::write(temp_dir.join("index"), index_content).unwrap();
376
377 let cli_table = Arc::new(
379 CliTable::new(temp_dir.join("index"), &temp_dir).expect("failed to create CliTable"),
380 );
381
382 let input = "Name: Alice, Age: 30\nName: Bob, Age: 25\nName: Charlie, Age: 35\n";
384
385 let num_threads = 8;
387 let iterations_per_thread = 100;
388
389 let handles: Vec<_> = (0..num_threads)
390 .map(|thread_id| {
391 let cli_table = Arc::clone(&cli_table);
392 let input = input.to_string();
393
394 thread::spawn(move || {
395 let mut attrs = HashMap::new();
396 attrs.insert("Platform".to_string(), "test".to_string());
397 attrs.insert("Command".to_string(), "show users".to_string());
398
399 for i in 0..iterations_per_thread {
400 let result = cli_table.parse_cmd(&input, &attrs);
401 match result {
402 Ok(table) => {
403 assert_eq!(table.len(), 3, "thread {} iter {}: wrong row count", thread_id, i);
404 }
405 Err(e) => {
406 panic!("thread {} iter {}: parse failed: {}", thread_id, i, e);
407 }
408 }
409 }
410 thread_id
411 })
412 })
413 .collect();
414
415 let mut completed = Vec::new();
417 for handle in handles {
418 let thread_id = handle.join().expect("thread panicked");
419 completed.push(thread_id);
420 }
421
422 assert_eq!(completed.len(), num_threads);
423
424 let _ = std::fs::remove_dir_all(&temp_dir);
426 }
427
428 #[test]
429 fn test_concurrent_parsing_different_templates() {
430 let temp_dir = std::env::temp_dir().join("textfsm_concurrency_test_multi");
432 let _ = std::fs::remove_dir_all(&temp_dir);
433 std::fs::create_dir_all(&temp_dir).unwrap();
434
435 let template_a = r#"Value Interface (\S+)
437Value Status (up|down)
438
439Start
440 ^${Interface} is ${Status} -> Record
441"#;
442 std::fs::write(temp_dir.join("template_a.textfsm"), template_a).unwrap();
443
444 let template_b = r#"Value Version (\S+)
446Value Uptime (\d+)
447
448Start
449 ^Version: ${Version}, Uptime: ${Uptime} -> Record
450"#;
451 std::fs::write(temp_dir.join("template_b.textfsm"), template_b).unwrap();
452
453 let index_content = r#"Template, Platform, Command
455template_a.textfsm, .*, show interfaces
456template_b.textfsm, .*, show version
457"#;
458 std::fs::write(temp_dir.join("index"), index_content).unwrap();
459
460 let cli_table = Arc::new(
461 CliTable::new(temp_dir.join("index"), &temp_dir).expect("failed to create CliTable"),
462 );
463
464 let input_a = "eth0 is up\neth1 is down\n";
465 let input_b = "Version: 1.2.3, Uptime: 3600\n";
466
467 let num_threads = 4;
468
469 let handles: Vec<_> = (0..num_threads)
470 .map(|thread_id| {
471 let cli_table = Arc::clone(&cli_table);
472 let input_a = input_a.to_string();
473 let input_b = input_b.to_string();
474
475 thread::spawn(move || {
476 for i in 0..50 {
478 if (thread_id + i) % 2 == 0 {
479 let mut attrs = HashMap::new();
480 attrs.insert("Platform".to_string(), "test".to_string());
481 attrs.insert("Command".to_string(), "show interfaces".to_string());
482
483 let table = cli_table.parse_cmd(&input_a, &attrs).unwrap();
484 assert_eq!(table.len(), 2);
485 } else {
486 let mut attrs = HashMap::new();
487 attrs.insert("Platform".to_string(), "test".to_string());
488 attrs.insert("Command".to_string(), "show version".to_string());
489
490 let table = cli_table.parse_cmd(&input_b, &attrs).unwrap();
491 assert_eq!(table.len(), 1);
492 }
493 }
494 thread_id
495 })
496 })
497 .collect();
498
499 for handle in handles {
500 handle.join().expect("thread panicked");
501 }
502
503 let _ = std::fs::remove_dir_all(&temp_dir);
505 }
506}
507
508#[cfg(feature = "serde")]
509impl CliTable {
510 pub fn parse_cmd_into<T>(
512 &self,
513 text: &str,
514 attributes: &HashMap<String, String>,
515 ) -> Result<Vec<T>, CliTableError>
516 where
517 T: serde::de::DeserializeOwned,
518 {
519 let table = self.parse_cmd(text, attributes)?;
520 table.into_deserialize()
521 }
522}