Skip to main content

pimalaya_cli/
shell.rs

1//! Commands printed for a person to paste into a shell.
2//!
3//! A binary sometimes answers with the next command to run: the first
4//! one after the welcome, the one resuming a flow. [`invocation`] starts
5//! such a command with the binary and the flags it ran with, and
6//! [`quote`] makes each value survive the paste.
7
8use std::path::PathBuf;
9
10/// Quotes `s` for a paste into a command line: single quotes for a POSIX
11/// shell, double quotes on Windows, which cmd and PowerShell both read,
12/// a Windows path never holding one.
13pub fn quote(s: &str) -> String {
14    if cfg!(windows) {
15        return format!("\"{s}\"");
16    }
17
18    let mut out = String::with_capacity(s.len() + 2);
19    out.push('\'');
20    for ch in s.chars() {
21        if ch == '\'' {
22            out.push_str("'\\''");
23        } else {
24            out.push(ch);
25        }
26    }
27    out.push('\'');
28    out
29}
30
31/// Builds the invocation a printed command starts with: `bin`, then one
32/// `-c` per configuration path and `-a` when an account is named, each
33/// value quoted.
34///
35/// It carries the `-c` and `-a` the binary ran with, so the command
36/// reaches the same configuration and account rather than the default
37/// ones.
38pub fn invocation(bin: &str, config_paths: &[PathBuf], account: Option<&str>) -> String {
39    let mut invocation = String::from(bin);
40
41    for path in config_paths {
42        invocation.push_str(" -c ");
43        invocation.push_str(&quote(&path.to_string_lossy()));
44    }
45
46    if let Some(name) = account.filter(|name| !name.is_empty()) {
47        invocation.push_str(" -a ");
48        invocation.push_str(&quote(name));
49    }
50
51    invocation
52}
53
54#[cfg(test)]
55mod tests {
56    use std::path::PathBuf;
57
58    use crate::shell;
59
60    #[test]
61    #[cfg(not(windows))]
62    fn quote_escapes_embedded_quotes() {
63        assert_eq!(shell::quote("plain"), "'plain'");
64        assert_eq!(shell::quote("a;b"), "'a;b'");
65        assert_eq!(shell::quote("it's"), "'it'\\''s'");
66    }
67
68    #[test]
69    #[cfg(not(windows))]
70    fn invocation_carries_the_account_and_config() {
71        assert_eq!(shell::invocation("app", &[], None), "app");
72        assert_eq!(shell::invocation("app", &[], Some("")), "app");
73        assert_eq!(shell::invocation("app", &[], Some("mail")), "app -a 'mail'");
74
75        let paths = [PathBuf::from("/a.toml"), PathBuf::from("/b c.toml")];
76        assert_eq!(
77            shell::invocation("app", &paths, Some("work's")),
78            "app -c '/a.toml' -c '/b c.toml' -a 'work'\\''s'"
79        );
80    }
81}