cargo_quickinstall/
command_ext.rs1use 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 f.write_str("\"")?;
51
52 for ch in s.chars() {
53 if ch == '"' {
54 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}