osp_cli/core/
shell_words.rs1#[derive(Debug, Clone, Copy, PartialEq, Eq)]
3pub enum QuoteStyle {
4 Single,
6 Double,
8}
9
10pub fn quote_for_shell(value: &str, style: QuoteStyle) -> String {
25 match style {
26 QuoteStyle::Single => quote_single(value),
27 QuoteStyle::Double => quote_double(value),
28 }
29}
30
31pub fn escape_for_shell(value: &str) -> String {
47 let mut out = String::with_capacity(value.len());
48 for ch in value.chars() {
49 if is_unquoted_safe(ch) {
50 out.push(ch);
51 } else {
52 out.push('\\');
53 out.push(ch);
54 }
55 }
56 out
57}
58
59fn quote_single(value: &str) -> String {
60 let mut out = String::from("'");
61 for ch in value.chars() {
62 if ch == '\'' {
63 out.push_str("'\\''");
64 } else {
65 out.push(ch);
66 }
67 }
68 out.push('\'');
69 out
70}
71
72fn quote_double(value: &str) -> String {
73 let mut out = String::from("\"");
74 for ch in value.chars() {
75 match ch {
76 '"' | '\\' | '$' | '`' => {
77 out.push('\\');
78 out.push(ch);
79 }
80 _ => out.push(ch),
81 }
82 }
83 out.push('"');
84 out
85}
86
87fn is_unquoted_safe(ch: char) -> bool {
88 ch.is_alphanumeric() || matches!(ch, '/' | '.' | '_' | '-' | '~' | ':' | '+')
89}
90
91#[cfg(test)]
92mod tests {
93 use super::{QuoteStyle, escape_for_shell, quote_for_shell};
94
95 #[test]
96 fn escape_for_shell_backslashes_unsafe_characters() {
97 assert_eq!(
98 escape_for_shell("team docs/file name.txt"),
99 "team\\ docs/file\\ name.txt"
100 );
101 assert_eq!(escape_for_shell("rød"), "rød");
102 }
103
104 #[test]
105 fn quote_for_shell_handles_single_quotes() {
106 assert_eq!(
107 quote_for_shell("O'Brien", QuoteStyle::Single),
108 "'O'\\''Brien'"
109 );
110 }
111
112 #[test]
113 fn quote_for_shell_handles_double_quotes() {
114 assert_eq!(
115 quote_for_shell("a\"b$`c\\d", QuoteStyle::Double),
116 "\"a\\\"b\\$\\`c\\\\d\""
117 );
118 }
119}