textfsm-core 0.3.1

Core parsing library for TextFSM template-based state machine
Documentation
//! Completion syntax expansion for index file patterns.
//!
//! Supports the `[[...]]` variable-length completion syntax:
//! - `sh[[ow]]` expands to `sh(o(w)?)?`
//! - `sh[[ow]] ver[[sion]]` expands to `sh(o(w)?)? ver(s(i(o(n)?)?)?)?`

use super::CliTableError;

/// Expand `[[...]]` completion syntax to regex pattern.
///
/// Each character inside `[[...]]` becomes optionally matched,
/// building from left to right.
///
/// # Examples
///
/// ```rust,ignore
/// assert_eq!(expand_completion("sh[[ow]]").unwrap(), "sh(o(w)?)?");
/// assert_eq!(expand_completion("sh[[ow]] ver[[sion]]").unwrap(), "sh(o(w)?)? ver(s(i(o(n)?)?)?)?");
/// assert_eq!(expand_completion("no completion").unwrap(), "no completion");
/// ```
pub fn expand_completion(pattern: &str) -> Result<String, CliTableError> {
    let mut result = String::with_capacity(pattern.len() * 2);
    let mut chars = pattern.chars().peekable();

    while let Some(c) = chars.next() {
        if c == '[' && chars.peek() == Some(&'[') {
            // Start of [[...]] completion
            chars.next(); // consume second '['

            let mut completion_chars = Vec::new();

            // Collect characters until ]]
            loop {
                match chars.next() {
                    Some(']') if chars.peek() == Some(&']') => {
                        chars.next(); // consume second ']'
                        break;
                    }
                    Some(ch) => completion_chars.push(ch),
                    None => {
                        return Err(CliTableError::InvalidCompletion(
                            "unclosed [[...]] completion".into(),
                        ));
                    }
                }
            }

            // Expand the completion
            // "ow" becomes "(o(w)?)?"
            if completion_chars.is_empty() {
                // Empty completion [[]] - just skip
                continue;
            }

            result.push('(');
            for (i, ch) in completion_chars.iter().enumerate() {
                // Escape regex metacharacters
                if is_regex_meta(*ch) {
                    result.push('\\');
                }
                result.push(*ch);
                if i < completion_chars.len() - 1 {
                    result.push('(');
                }
            }
            // Add closing )?
            for _ in 0..completion_chars.len() {
                result.push_str(")?");
            }
        } else {
            result.push(c);
        }
    }

    Ok(result)
}

/// Check if a character is a regex metacharacter that needs escaping.
fn is_regex_meta(c: char) -> bool {
    matches!(
        c,
        '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|' | '^' | '$' | '\\'
    )
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_simple_completion() {
        assert_eq!(expand_completion("sh[[ow]]").unwrap(), "sh(o(w)?)?");
    }

    #[test]
    fn test_multiple_completions() {
        assert_eq!(
            expand_completion("sh[[ow]] ver[[sion]]").unwrap(),
            "sh(o(w)?)? ver(s(i(o(n)?)?)?)?",
        );
    }

    #[test]
    fn test_no_completion() {
        assert_eq!(expand_completion("show version").unwrap(), "show version");
    }

    #[test]
    fn test_empty_completion() {
        assert_eq!(expand_completion("sh[[]]ow").unwrap(), "show");
    }

    #[test]
    fn test_single_char_completion() {
        assert_eq!(expand_completion("sh[[o]]w").unwrap(), "sh(o)?w");
    }

    #[test]
    fn test_completion_at_end() {
        assert_eq!(
            expand_completion("sh[[ow]] int[[erfaces]]").unwrap(),
            "sh(o(w)?)? int(e(r(f(a(c(e(s)?)?)?)?)?)?)?",
        );
    }

    #[test]
    fn test_unclosed_completion() {
        let result = expand_completion("sh[[ow");
        assert!(matches!(result, Err(CliTableError::InvalidCompletion(_))));
    }

    #[test]
    fn test_regex_metachar_in_completion() {
        // A dot inside completion should be escaped
        assert_eq!(expand_completion("test[[.x]]").unwrap(), "test(\\.(x)?)?");
    }

    #[test]
    fn test_real_ntc_pattern() {
        // Real pattern from ntc-templates
        let result = expand_completion("sh[[ow]] (in[[terfaces]] e[[thernet]]|in[[terfaces]])$");
        assert!(result.is_ok());
        let expanded = result.unwrap();
        assert!(expanded.contains("sh(o(w)?)?"));
        assert!(expanded.contains("in(t(e(r(f(a(c(e(s)?)?)?)?)?)?)?)?"));
    }
}