sandbox_runtime/utils/
shell.rs

1//! Shell quoting utilities.
2
3/// Quote a string for use in a shell command.
4/// This wraps the string in single quotes and escapes any existing single quotes.
5pub fn quote(s: &str) -> String {
6    // If the string is empty, return empty quoted string
7    if s.is_empty() {
8        return "''".to_string();
9    }
10
11    // If the string contains no special characters, return it as-is
12    if !needs_quoting(s) {
13        return s.to_string();
14    }
15
16    // Use single quotes and escape any existing single quotes
17    // 'abc' -> 'abc'
18    // abc's -> 'abc'"'"'s'
19    format!("'{}'", s.replace('\'', "'\"'\"'"))
20}
21
22/// Quote a string for use as a shell argument, always using quotes.
23pub fn quote_always(s: &str) -> String {
24    format!("'{}'", s.replace('\'', "'\"'\"'"))
25}
26
27/// Check if a string needs quoting.
28fn needs_quoting(s: &str) -> bool {
29    s.chars().any(|c| {
30        matches!(
31            c,
32            ' ' | '\t'
33                | '\n'
34                | '\r'
35                | '"'
36                | '\''
37                | '\\'
38                | '$'
39                | '`'
40                | '!'
41                | '*'
42                | '?'
43                | '['
44                | ']'
45                | '{'
46                | '}'
47                | '('
48                | ')'
49                | '<'
50                | '>'
51                | '|'
52                | '&'
53                | ';'
54                | '#'
55                | '~'
56        )
57    })
58}
59
60/// Join arguments with proper quoting for shell execution.
61pub fn join_args<I, S>(args: I) -> String
62where
63    I: IntoIterator<Item = S>,
64    S: AsRef<str>,
65{
66    args.into_iter()
67        .map(|s| quote(s.as_ref()))
68        .collect::<Vec<_>>()
69        .join(" ")
70}
71
72/// Parse a shell command string into arguments.
73/// Uses shell-words crate for proper handling.
74pub fn split_args(s: &str) -> Result<Vec<String>, shell_words::ParseError> {
75    shell_words::split(s)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::*;
81
82    #[test]
83    fn test_quote() {
84        assert_eq!(quote("simple"), "simple");
85        assert_eq!(quote("with space"), "'with space'");
86        assert_eq!(quote("it's"), "'it'\"'\"'s'");
87        assert_eq!(quote(""), "''");
88        assert_eq!(quote("$var"), "'$var'");
89    }
90
91    #[test]
92    fn test_join_args() {
93        let args = vec!["echo", "hello world", "it's"];
94        assert_eq!(join_args(args), "echo 'hello world' 'it'\"'\"'s'");
95    }
96
97    #[test]
98    fn test_split_args() {
99        let args = split_args("echo 'hello world' test").unwrap();
100        assert_eq!(args, vec!["echo", "hello world", "test"]);
101    }
102}