1pub fn quote(s: &str) -> String {
7 let plain = !s.is_empty()
8 && !s.starts_with(['#', '~', '='])
9 && s.chars().all(|c| c.is_ascii_alphanumeric() || "-_./:#@=+,".contains(c));
10 if plain { s.to_string() } else { format!("'{}'", s.replace('\'', "'\\''")) }
11}
12
13pub fn word(v: impl std::fmt::Display) -> String {
15 quote(&v.to_string())
16}
17
18pub fn join<S: AsRef<str>>(items: &[S]) -> String {
20 items.iter().map(|s| quote(s.as_ref())).collect::<Vec<_>>().join(" ")
21}
22
23pub fn opt(flag: &str, v: &str) -> String {
26 if v.starts_with('-') { format!("{flag}={}", quote(v)) } else { format!("{flag} {}", quote(v)) }
27}
28
29pub fn positionals<S: AsRef<str>>(items: &[S]) -> String {
31 let words = join(items);
32 if items.iter().any(|s| s.as_ref().starts_with('-')) { format!("-- {words}") } else { words }
33}
34
35#[cfg(test)]
36mod tests {
37 use super::quote;
38
39 #[test]
40 fn quotes_only_what_the_shell_would_split_or_expand() {
41 assert_eq!(quote("src/new-dir"), "src/new-dir");
42 assert_eq!(quote("sot:shop/cart-totals"), "sot:shop/cart-totals");
43 assert_eq!(quote("src/new dir"), "'src/new dir'");
44 assert_eq!(quote("src/it's"), "'src/it'\\''s'");
45 assert_eq!(quote("src/한글"), "'src/한글'");
46 assert_eq!(quote(""), "''");
47 assert_eq!(quote("#x"), "'#x'");
48 assert_eq!(quote("a$b"), "'a$b'");
49 }
50
51 #[test]
52 fn dash_values_stay_values() {
53 assert_eq!(super::opt("--tag", "-q"), "--tag=-q");
54 assert_eq!(super::opt("--tag", "a b"), "--tag 'a b'");
55 assert_eq!(super::positionals(&["a", "-nope"]), "-- a -nope");
56 assert_eq!(super::positionals(&["a"]), "a");
57 }
58}