1use std::process::{Command, ExitStatus, Output};
2use thiserror::Error;
3
4pub trait ExecuteCommand {
5 fn parse(command_string: impl AsRef<str>) -> Result<Command>;
6 fn execute_status(&mut self) -> Result<ExitStatus>;
7 fn execute_output(&mut self) -> Result<Output>;
8 fn execute_string(&mut self) -> Result<String>;
9}
10
11impl ExecuteCommand for Command {
12 fn parse(command_string: impl AsRef<str>) -> Result<Command> {
13 parse(command_string)
14 }
15
16 fn execute_status(&mut self) -> Result<ExitStatus> {
17 execute_status(self)
18 }
19
20 fn execute_output(&mut self) -> Result<Output> {
21 execute_output(self)
22 }
23
24 fn execute_string(&mut self) -> Result<String> {
25 execute_string(self)
26 }
27}
28
29pub fn parse(command_string: impl AsRef<str>) -> Result<Command> {
30 let [program, args @ ..] = &shell_words::split(command_string.as_ref())?[..] else {
31 return Ok(Command::new(""));
32 };
33
34 let mut command = Command::new(program);
35 command.args(args);
36 Ok(command)
37}
38
39pub fn status(command_string: impl AsRef<str>) -> Result<ExitStatus> {
40 execute_status(&mut parse(command_string)?)
41}
42
43pub fn output(command_string: impl AsRef<str>) -> Result<Output> {
44 execute_output(&mut parse(command_string)?)
45}
46
47pub fn string(command_string: impl AsRef<str>) -> Result<String> {
48 execute_string(&mut parse(command_string)?)
49}
50
51fn execute_status(command: &mut Command) -> Result<ExitStatus> {
52 let status = command.status()?;
53
54 match status.success() {
55 true => Ok(status),
56 false => Err(Error::ExitStatus(status)),
57 }
58}
59
60fn execute_output(command: &mut Command) -> Result<Output> {
61 let output = command.output()?;
62
63 match output.status.success() {
64 true => Ok(output),
65 false => Err(Error::Output(output)),
66 }
67}
68
69fn execute_string(command: &mut Command) -> Result<String> {
70 let output = execute_output(command)?;
71
72 Ok(String::from_utf8(output.stdout)?)
73}
74
75pub type Result<T, E = Error> = std::result::Result<T, E>;
76
77#[derive(Error, Debug)]
78pub enum Error {
79 #[error("process exited unsuccessfully: {0}")]
80 ExitStatus(ExitStatus),
81
82 #[error("process exited unsuccessfully: {}", .0.status)]
83 Output(Output),
84
85 #[error(transparent)]
86 ParseError(#[from] shell_words::ParseError),
87
88 #[error(transparent)]
89 IoError(#[from] std::io::Error),
90
91 #[error(transparent)]
92 FromUtf8Error(#[from] std::string::FromUtf8Error),
93}
94
95#[cfg(test)]
96mod tests {
97 use super::*;
98
99 const BASE_COMMAND: &str = if cfg!(windows) { "cmd /C" } else { "sh -c" };
100
101 #[test]
102 fn test_parse() {
103 let command = Command::parse(r#"p a b"#).unwrap();
104 assert_eq!(command.get_program(), "p");
105 assert_eq!(command.get_args().collect::<Vec<_>>(), ["a", "b"]);
106
107 let command = Command::parse(r#"p a "b c""#).unwrap();
108 assert_eq!(command.get_program(), "p");
109 assert_eq!(command.get_args().collect::<Vec<_>>(), ["a", "b c"]);
110
111 let command = Command::parse(r#"p "a\"b""#).unwrap();
112 assert_eq!(command.get_program(), "p");
113 assert_eq!(command.get_args().collect::<Vec<_>>(), ["a\"b"]);
114
115 let command = Command::parse(r#"p "a"#);
116 assert!(command.is_err());
117 }
118
119 #[test]
120 fn test_execute_status() {
121 assert!(Command::parse(format!("{BASE_COMMAND} 'exit 0'"))
122 .unwrap()
123 .execute_status()
124 .is_ok());
125
126 assert!(Command::parse(format!("{BASE_COMMAND} 'exit 1'"))
127 .unwrap()
128 .execute_status()
129 .is_err());
130 }
131
132 #[test]
133 fn test_execute_output() {
134 assert!(Command::parse(format!("{BASE_COMMAND} 'exit 0'"))
135 .unwrap()
136 .execute_output()
137 .is_ok());
138
139 assert!(Command::parse(format!("{BASE_COMMAND} 'exit 1'"))
140 .unwrap()
141 .execute_output()
142 .is_err());
143 }
144
145 #[test]
146 fn test_execute_string() {
147 assert_eq!(
148 Command::parse(format!("{BASE_COMMAND} 'echo 1'"))
149 .unwrap()
150 .execute_string()
151 .unwrap(),
152 if cfg!(windows) { "1\r\n" } else { "1\n" }
153 );
154
155 assert!(Command::parse(format!("{BASE_COMMAND} 'exit 1'"))
156 .unwrap()
157 .execute_string()
158 .is_err());
159 }
160}