cargo_quickinstall/
command_ext.rs

1use std::{
2    ffi::OsStr,
3    fmt::{self, Write},
4    process::{self, Child, Command, Output},
5};
6
7use crate::{utf8_to_string_lossy, CommandFailed, InstallError};
8
9pub trait CommandExt {
10    fn formattable(&self) -> CommandFormattable<'_>;
11
12    fn output_checked_status(&mut self) -> Result<Output, InstallError>;
13
14    fn spawn_with_cmd(self) -> Result<ChildWithCommand, InstallError>;
15}
16
17impl CommandExt for Command {
18    fn formattable(&self) -> CommandFormattable<'_> {
19        CommandFormattable(self)
20    }
21
22    fn output_checked_status(&mut self) -> Result<Output, InstallError> {
23        self.output()
24            .map_err(InstallError::from)
25            .and_then(|output| check_status(self, output))
26    }
27
28    fn spawn_with_cmd(mut self) -> Result<ChildWithCommand, InstallError> {
29        self.spawn()
30            .map_err(InstallError::from)
31            .map(move |child| ChildWithCommand { child, cmd: self })
32    }
33}
34
35pub struct CommandFormattable<'a>(&'a Command);
36
37fn needs_escape(s: &str) -> bool {
38    s.contains(|ch| !matches!(ch, 'a'..='z' | 'A'..='Z' | '0'..='9' | '-' | '_' | '=' | '/' | ',' | '.' | '+'))
39}
40
41fn write_shell_arg_escaped(f: &mut fmt::Formatter<'_>, os_str: &OsStr) -> fmt::Result {
42    let s = os_str.to_string_lossy();
43
44    if needs_escape(&s) {
45        // There is some ascii whitespace (' ', '\n', '\t'),
46        // or non-ascii characters need to quote them using `"`.
47        //
48        // But then, it is possible for the `s` to contains `"`,
49        // so they needs to be escaped.
50        f.write_str("\"")?;
51
52        for ch in s.chars() {
53            if ch == '"' {
54                // Escape it with `\`.
55                f.write_char('\\')?;
56            }
57
58            f.write_char(ch)?;
59        }
60
61        f.write_str("\"")
62    } else {
63        f.write_str(&s)
64    }
65}
66
67impl fmt::Display for CommandFormattable<'_> {
68    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
69        let cmd = self.0;
70
71        write_shell_arg_escaped(f, cmd.get_program())?;
72
73        for arg in cmd.get_args() {
74            f.write_str(" ")?;
75            write_shell_arg_escaped(f, arg)?;
76        }
77
78        Ok(())
79    }
80}
81
82pub struct ChildWithCommand {
83    cmd: Command,
84    child: Child,
85}
86
87fn check_status(cmd: &Command, output: Output) -> Result<Output, InstallError> {
88    if output.status.success() {
89        Ok(output)
90    } else {
91        Err(CommandFailed {
92            command: cmd.formattable().to_string(),
93            stdout: utf8_to_string_lossy(output.stdout),
94            stderr: utf8_to_string_lossy(output.stderr),
95        }
96        .into())
97    }
98}
99
100impl ChildWithCommand {
101    pub fn wait_with_output_checked_status(self) -> Result<Output, InstallError> {
102        let cmd = self.cmd;
103
104        self.child
105            .wait_with_output()
106            .map_err(InstallError::from)
107            .and_then(|output| check_status(&cmd, output))
108    }
109
110    pub fn stdin(&mut self) -> &mut Option<process::ChildStdin> {
111        &mut self.child.stdin
112    }
113
114    pub fn stdout(&mut self) -> &mut Option<process::ChildStdout> {
115        &mut self.child.stdout
116    }
117
118    pub fn stderr(&mut self) -> &mut Option<process::ChildStderr> {
119        &mut self.child.stderr
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use super::*;
126
127    #[test]
128    fn test_cmd_format() {
129        assert_eq!(
130            Command::new("cargo")
131                .args(["binstall", "-V"])
132                .formattable()
133                .to_string(),
134            "cargo binstall -V"
135        );
136    }
137}