Skip to main content

cli/
shell_quote.rs

1//! Shared POSIX shell-quoting helpers for rendering commands back into copy-paste-safe or
2//! sourceable shell syntax. Display-only: `task::mod` and `apps::upgrade` never execute the
3//! quoted string itself, they always run the underlying argv directly via
4//! `std::process::Command`/`tokio::process::Command`, so this module has no injection surface
5//! of its own.
6
7/// Single-quotes `value` for POSIX shells. The only character a single-quoted string cannot
8/// contain is `'`, which is closed, escaped, and reopened as `'\''`.
9pub(crate) fn single_quote(value: &str) -> String {
10    format!("'{}'", value.replace('\'', "'\\''"))
11}
12
13/// Renders `value` back into a copy-paste-safe shell argument: left bare when every character
14/// is inert to POSIX shells, single-quoted otherwise.
15pub(crate) fn quote_if_needed(value: &str) -> String {
16    if !value.is_empty() && value.chars().all(is_shell_safe) {
17        value.to_string()
18    } else {
19        single_quote(value)
20    }
21}
22
23/// Characters that never need quoting: alphanumerics plus punctuation that is inert to POSIX
24/// shells and common in paths, URLs, and rsync targets (e.g. `host:/var/www/`).
25fn is_shell_safe(c: char) -> bool {
26    c.is_ascii_alphanumeric()
27        || matches!(c, '_' | '-' | '.' | '/' | ':' | ',' | '=' | '@' | '%' | '+')
28}
29
30#[cfg(test)]
31mod tests {
32    use super::*;
33
34    #[test]
35    fn quote_if_needed_leaves_plain_values_unquoted() {
36        assert_eq!(quote_if_needed("hello"), "hello");
37        assert_eq!(quote_if_needed("host:/var/www/"), "host:/var/www/");
38    }
39
40    #[test]
41    fn quote_if_needed_quotes_empty_and_unsafe_values() {
42        assert_eq!(quote_if_needed(""), "''");
43        assert_eq!(quote_if_needed("it's"), "'it'\\''s'");
44        assert_eq!(quote_if_needed("a b"), "'a b'");
45    }
46
47    #[test]
48    fn single_quote_escapes_embedded_quotes() {
49        assert_eq!(single_quote("it's"), "'it'\\''s'");
50        assert_eq!(single_quote(""), "''");
51    }
52}