oxdock_process/
shell.rs

1use anyhow::{Context, Result, bail};
2use std::ffi::OsStr;
3use std::fs::File;
4#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
5use std::process::{Command, ExitStatus, Stdio};
6
7pub fn shell_program() -> String {
8    #[cfg(windows)]
9    {
10        std::env::var("COMSPEC").unwrap_or_else(|_| "cmd".to_string())
11    }
12
13    #[cfg(not(windows))]
14    {
15        std::env::var("SHELL").unwrap_or_else(|_| "sh".to_string())
16    }
17}
18
19#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
20pub(crate) fn shell_cmd(cmd: &str) -> Command {
21    let program = shell_program();
22    let mut c = Command::new(program);
23    #[allow(clippy::disallowed_macros)]
24    if cfg!(windows) {
25        c.arg("/C").arg(cmd);
26    } else {
27        c.arg("-c").arg(cmd);
28    }
29    c
30}
31
32#[derive(Default)]
33pub struct ShellLauncher;
34
35#[allow(clippy::disallowed_types, clippy::disallowed_methods)]
36impl ShellLauncher {
37    pub fn run(&self, cmd: &mut Command) -> Result<()> {
38        let status = cmd
39            .status()
40            .with_context(|| format!("failed to run {:?}", cmd))?;
41        if !status.success() {
42            bail!("command {:?} failed with status {}", cmd, status);
43        }
44        Ok(())
45    }
46
47    pub fn run_with_output(&self, cmd: &mut Command) -> Result<(ExitStatus, Vec<u8>, Vec<u8>)> {
48        cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
49        let output = cmd
50            .output()
51            .with_context(|| format!("failed to run {:?}", cmd))?;
52        Ok((output.status, output.stdout, output.stderr))
53    }
54
55    pub fn spawn(&self, cmd: &mut Command) -> Result<()> {
56        cmd.spawn()
57            .with_context(|| format!("failed to spawn {:?}", cmd))?;
58        Ok(())
59    }
60
61    pub fn with_stdins<'a>(&self, cmd: &'a mut Command, stdin: Option<File>) -> &'a mut Command {
62        if let Some(file) = stdin {
63            cmd.stdin(file);
64        }
65        cmd
66    }
67
68    pub fn program_arg(&self, program: impl AsRef<OsStr>) -> Command {
69        Command::new(program)
70    }
71}
72
73#[cfg(test)]
74mod tests {
75    use super::{ShellLauncher, shell_cmd, shell_program};
76    use crate::TestEnvGuard;
77
78    use std::ffi::OsStr;
79    use std::sync::Mutex;
80
81    // For Windows, fixing COMSPEC override test race condition
82    static ENV_LOCK: Mutex<()> = Mutex::new(());
83
84    #[test]
85    fn shell_program_prefers_env_override() {
86        let _lock = ENV_LOCK.lock().expect("env lock");
87        #[cfg(windows)]
88        let _guard = TestEnvGuard::set("COMSPEC", "custom-cmd");
89        #[cfg(not(windows))]
90        let _guard = TestEnvGuard::set("SHELL", "custom-sh");
91        let program = shell_program();
92        #[cfg(windows)]
93        assert_eq!(program, "custom-cmd");
94        #[cfg(not(windows))]
95        assert_eq!(program, "custom-sh");
96    }
97
98    #[cfg_attr(
99        miri,
100        ignore = "spawns shell command; Miri does not support process execution"
101    )]
102    #[test]
103    fn shell_launcher_run_with_output_captures_stdout() {
104        let _lock = ENV_LOCK.lock().expect("env lock");
105        let launcher = ShellLauncher;
106        let mut cmd = shell_cmd("echo hello");
107        let (status, stdout, _stderr) = launcher.run_with_output(&mut cmd).expect("run output");
108        assert!(status.success());
109        let out = String::from_utf8_lossy(&stdout);
110        assert!(out.contains("hello"));
111    }
112
113    #[test]
114    fn shell_launcher_program_arg_tracks_program() {
115        let launcher = ShellLauncher;
116        let cmd = launcher.program_arg("echo");
117        assert_eq!(cmd.get_program(), OsStr::new("echo"));
118    }
119}