printable_shell_command/
lib.rs1use 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 fn printable_invocation_string_lossy(&mut self) -> String;
9
10 fn print_invocation(&mut self) -> Result<&mut Self, Utf8Error> {
12 println!("{}", self.printable_invocation_string_lossy());
13 Ok(self)
14 }
15 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}