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 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}