printable_shell_command/
lib.rs

1use std::{process::Command, str::Utf8Error, sync::LazyLock};
2
3use regex::Regex;
4
5pub trait ShellPrintable {
6    fn printable_invocation_string(&mut self) -> Result<String, Utf8Error>;
7    // Calls `.to_string_lossy()` on the program name and args.
8    fn printable_invocation_string_lossy(&mut self) -> String;
9
10    // Print the invocation to `stdout`.`
11    fn print_invocation(&mut self) -> Result<&mut Self, Utf8Error> {
12        println!("{}", self.printable_invocation_string_lossy());
13        Ok(self)
14    }
15    // Print the invocation to `stdout`.`
16    fn print_invocation_lossy(&mut self) -> &mut Self {
17        println!("{}", self.printable_invocation_string_lossy());
18        self
19    }
20}
21struct SimpleEscapeOptions {
22    is_command: bool,
23}
24
25static PROGRAM_NAME_REGEX: LazyLock<Regex> =
26    LazyLock::new(|| Regex::new(r#"[ "'`|$*?><()\[\]{}&\\;#=]"#).unwrap());
27static ARG_REGEX: LazyLock<Regex> =
28    LazyLock::new(|| Regex::new(r#"[ "'`|$*?><()\[\]{}&\\;#]"#).unwrap());
29
30fn simple_escape(s: &str, options: SimpleEscapeOptions) -> String {
31    let regex = if options.is_command {
32        &PROGRAM_NAME_REGEX
33    } else {
34        &ARG_REGEX
35    };
36
37    if regex.is_match(s) {
38        format!("'{}'", s.replace("\\", "\\\\").replace("'", "\\'"))
39    } else {
40        s.to_owned()
41    }
42}
43
44impl ShellPrintable for Command {
45    fn printable_invocation_string_lossy(&mut self) -> String {
46        let mut lines: Vec<String> = vec![simple_escape(
47            &self.get_program().to_string_lossy(),
48            SimpleEscapeOptions { is_command: true },
49        )];
50        for arg in self.get_args() {
51            lines.push(simple_escape(
52                &arg.to_string_lossy(),
53                SimpleEscapeOptions { is_command: false },
54            ))
55        }
56        lines.join(
57            " \\
58  ",
59        )
60    }
61    fn printable_invocation_string(&mut self) -> Result<String, Utf8Error> {
62        let mut lines: Vec<String> = vec![simple_escape(
63            TryInto::<&str>::try_into(self.get_program())?,
64            SimpleEscapeOptions { is_command: true },
65        )];
66        for arg in self.get_args() {
67            lines.push(simple_escape(
68                TryInto::<&str>::try_into(arg)?,
69                SimpleEscapeOptions { is_command: false },
70            ))
71        }
72        Ok(lines.join(
73            " \\
74  ",
75        ))
76    }
77}
78
79#[cfg(test)]
80mod tests {
81    use crate::ShellPrintable;
82    use std::process::Command;
83
84    #[test]
85    fn my_test() -> Result<(), String> {
86        let _ = Command::new("echo").args(["#hi"]).print_invocation();
87        Ok(())
88    }
89}