Skip to main content

specfold_core/
shell.rs

1//! Shell words for the commands specfold suggests (`next:`, fix commands, reruns).
2
3/// `s` as one POSIX shell word: plain words stay as they are, anything else is single-quoted
4/// with embedded `'` written as `'\''`. Every suggested command that embeds a path, id, or
5/// other user value goes through this one function.
6pub 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
13/// Any displayable value (an id, a path) as one shell word.
14pub fn word(v: impl std::fmt::Display) -> String {
15    quote(&v.to_string())
16}
17
18/// Every item quoted and joined with spaces.
19pub fn join<S: AsRef<str>>(items: &[S]) -> String {
20    items.iter().map(|s| quote(s.as_ref())).collect::<Vec<_>>().join(" ")
21}
22
23/// C229: `flag value` as shell words that parse back as that option: a value starting with `-`
24/// is written `--flag=value`, so it is not read as another option.
25pub fn opt(flag: &str, v: &str) -> String {
26    if v.starts_with('-') { format!("{flag}={}", quote(v)) } else { format!("{flag} {}", quote(v)) }
27}
28
29/// C229: positional words for the end of a command; `--` goes first when one starts with `-`.
30pub 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}