jabba_lib/
jprocess.rs

1//! # process
2//!
3//! Working with external commands.
4//!
5//! - call an external command and get its exit code, stdout, and stderr
6//! - call an external command (and see its output on the stdout)
7
8use shlex;
9
10use std::process;
11
12/// Stores process information: exit code, stdout, stderr.
13#[allow(dead_code)]
14#[derive(Debug)]
15pub struct ProcStat {
16    pub exit_code: i32,
17    pub stdout: String,
18    pub stderr: String,
19}
20
21impl ProcStat {
22    /// Returns a copy of the output.
23    ///
24    /// Use it if you want to avoid value moving.
25    pub fn output(&self) -> String {
26        self.stdout.clone()
27    }
28
29    /// Trims the trailing whitespaces from the output.
30    pub fn trimmed_output(&self) -> String {
31        self.stdout.trim_end().to_string()
32    }
33}
34
35/// Executes an external command and gets its exit code, stdout and stderr.
36///
37/// It waits for the command to complete.
38///
39/// The three values are returned in a `ProcStat` structure.
40///
41/// The command must be a simple command with some optional arguments.
42/// Pipes, redirections are not allowed.
43///
44/// # Examples
45///
46/// ```
47/// let commands = vec![
48///     r#"python -c "print('Hello Rust!')""#,
49///     "python --version",
50/// ];
51///
52/// for cmd in commands.iter() {
53///     let stat = jabba_lib::jprocess::get_exitcode_stdout_stderr(cmd).unwrap();
54///     println!("{:?}", stat);
55/// }
56///
57/// let answer = jabba_lib::jprocess::get_exitcode_stdout_stderr("rustc --version")
58///     .unwrap()
59///     .trimmed_output(); // no trailing whitespaces
60/// println!("{:?}", answer);
61/// ```
62///
63/// # Sample Output
64///
65/// ```text
66/// ProcStat { exit_code: 0, stdout: "Hello Rust!\n", stderr: "" }
67/// ProcStat { exit_code: 0, stdout: "Python 3.10.5\n", stderr: "" }
68/// "rustc 1.62.1 (e092d0b6b 2022-07-16)"
69/// ```
70pub fn get_exitcode_stdout_stderr(cmd: &str) -> Option<ProcStat> {
71    let parts = shlex::split(cmd).unwrap_or_else(|| panic!("cannot parse command {:?}", cmd));
72    let head = &parts[0];
73    let tail = &parts[1..];
74
75    let mut p = process::Command::new(head);
76    p.args(tail);
77    let p = p
78        .output()
79        .unwrap_or_else(|_| panic!("failed to execute {:?}", cmd));
80
81    let result = ProcStat {
82        exit_code: p.status.code()?,
83        stdout: String::from_utf8_lossy(&p.stdout).to_string(),
84        stderr: String::from_utf8_lossy(&p.stderr).to_string(),
85    };
86
87    Some(result)
88}
89
90/// Executes an external command and waits for it to complete.
91///
92/// The command's output goes to stdout (i.e., not captured).
93/// Similar to Python's `os.system("something")`.
94///
95/// The command must be a simple command with some optional arguments.
96/// Pipes, redirections are not allowed.
97///
98/// # Examples
99///
100/// ```
101/// let cmd = "rustc --version";
102/// jabba_lib::jprocess::exec_cmd(cmd);
103/// ```
104///
105/// # Sample Output
106///
107/// ```text
108/// rustc 1.62.1 (e092d0b6b 2022-07-16)
109/// ```
110pub fn exec_cmd(cmd: &str) {
111    let parts = shlex::split(cmd).unwrap();
112    let head = &parts[0];
113    let tail = &parts[1..];
114
115    let mut p = process::Command::new(head);
116    p.args(tail);
117    let mut child = p
118        .spawn()
119        .unwrap_or_else(|_| panic!("command {:?} failed to start", cmd));
120    child.wait().expect("command wasn't running");
121}
122
123/// Executes an external command in the background (i.e., it doesn't wait for it to complete).
124///
125/// The command's output goes to stdout (i.e., not captured).
126/// Similar to Python's `os.system("something &")`.
127///
128/// The command must be a simple command with some optional arguments.
129/// Pipes, redirections are not allowed.
130///
131/// # Examples
132///
133/// ```
134/// let cmd = "rustc --version";
135/// jabba_lib::jprocess::exec_cmd_in_bg(cmd);
136/// ```
137///
138/// # Sample Output
139///
140/// ```text
141/// rustc 1.62.1 (e092d0b6b 2022-07-16)
142/// ```
143pub fn exec_cmd_in_bg(cmd: &str) {
144    let parts = shlex::split(cmd).unwrap();
145    let head = &parts[0];
146    let tail = &parts[1..];
147
148    process::Command::new(head)
149        .args(tail)
150        .spawn()
151        .unwrap_or_else(|_| panic!("command {:?} failed to start", cmd));
152}
153
154// ==========================================================================
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn get_exitcode_stdout_stderr_exit_code_test() {
162        use which::which;
163
164        let cmd = "rustc";
165        let result = which(cmd);
166        if result.is_err() {
167            return;
168        }
169        // else, if "rustc" is available (should be...)
170        let cmd = "rustc --version";
171        let stat = get_exitcode_stdout_stderr(cmd).unwrap();
172        assert_eq!(stat.exit_code, 0);
173    }
174
175    #[test]
176    fn get_exitcode_stdout_stderr_stdout_test() {
177        use which::which;
178
179        let cmd = "rustc";
180        let result = which(cmd);
181        if result.is_err() {
182            return;
183        }
184        // else, if "rustc" is available (should be...)
185        let cmd = "rustc --version";
186        let stat = get_exitcode_stdout_stderr(cmd).unwrap();
187        assert!(stat.stdout.starts_with("rustc"));
188    }
189
190    #[test]
191    fn get_exitcode_stdout_stderr_stderr_test() {
192        use which::which;
193
194        let cmd = "rustc";
195        let result = which(cmd);
196        if result.is_err() {
197            return;
198        }
199        // else, if "rustc" is available (should be...)
200        let cmd = "rustc --nothing20220731"; // this option doesn't exist
201        let stat = get_exitcode_stdout_stderr(cmd).unwrap();
202        assert!(stat.exit_code != 0);
203        assert!(stat.stderr.len() > 0);
204    }
205
206    #[test]
207    fn trimmed_output_test() {
208        use which::which;
209
210        let cmd = "rustc";
211        let result = which(cmd);
212        if result.is_err() {
213            return;
214        }
215        // else, if "rustc" is available (should be...)
216        let cmd = "rustc --version";
217        let stat = get_exitcode_stdout_stderr(cmd).unwrap();
218        assert!(stat.output() == stat.stdout.clone());
219        assert!(stat.trimmed_output().len() < stat.output().len());
220    }
221
222    #[test]
223    fn exec_cmd_test() {
224        let cmd = "rustc --version";
225        exec_cmd(cmd);
226    }
227
228    #[test]
229    fn exec_cmd_in_bg_test() {
230        let cmd = "rustc --version";
231        exec_cmd_in_bg(cmd);
232    }
233}