safe-chains 0.125.0

Auto-allow safe, read-only bash commands in agentic coding tools
Documentation
use super::{Word, WordPart};

pub fn eval_word(word: &Word) -> String {
    let mut out = String::new();
    for part in &word.0 {
        eval_part(part, &mut out);
    }
    out
}

fn eval_part(part: &WordPart, out: &mut String) {
    match part {
        WordPart::Lit(s) => out.push_str(s),
        WordPart::Escape(c) => out.push(*c),
        WordPart::SQuote(s) => out.push_str(s),
        WordPart::DQuote(inner) => {
            for p in &inner.0 {
                eval_part(p, out);
            }
        }
        WordPart::CmdSub(_) => out.push_str("__SAFE_CHAINS_SUB__"),
        WordPart::Backtick(_) => out.push_str("__SAFE_CHAINS_SUB__"),
    }
}

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

    #[test]
    fn literal() {
        let w = Word(vec![WordPart::Lit("hello".into())]);
        assert_eq!(w.eval(), "hello");
    }

    #[test]
    fn single_quoted() {
        let w = Word(vec![WordPart::SQuote("hello world".into())]);
        assert_eq!(w.eval(), "hello world");
    }

    #[test]
    fn double_quoted() {
        let w = Word(vec![WordPart::DQuote(Word(vec![
            WordPart::Lit("hello ".into()),
            WordPart::Lit("world".into()),
        ]))]);
        assert_eq!(w.eval(), "hello world");
    }

    #[test]
    fn mixed_parts() {
        let w = Word(vec![
            WordPart::Lit("foo".into()),
            WordPart::DQuote(Word(vec![WordPart::Lit("bar".into())])),
            WordPart::SQuote("baz".into()),
        ]);
        assert_eq!(w.eval(), "foobarbaz");
    }

    #[test]
    fn cmd_sub_placeholder() {
        let w = Word(vec![WordPart::CmdSub(super::super::Script(vec![]))]);
        assert_eq!(w.eval(), "__SAFE_CHAINS_SUB__");
    }

    #[test]
    fn dquote_with_escape() {
        let w = Word(vec![WordPart::DQuote(Word(vec![
            WordPart::Lit("hello".into()),
            WordPart::Escape('"'),
            WordPart::Lit("world".into()),
        ]))]);
        assert_eq!(w.eval(), "hello\"world");
    }
}