execute_command/
lib.rs

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}