1use std::io::Write;
2use std::path::{Path, PathBuf};
3use std::process::{Command, Output, Stdio};
4
5#[derive(Debug)]
7pub struct CmdOutput {
8 pub code: i32,
9 pub stdout: Vec<u8>,
10 pub stderr: Vec<u8>,
11}
12
13impl CmdOutput {
14 pub fn success(&self) -> bool {
15 self.code == 0
16 }
17
18 pub fn stdout_text(&self) -> String {
19 String::from_utf8_lossy(&self.stdout).to_string()
20 }
21
22 pub fn stderr_text(&self) -> String {
23 String::from_utf8_lossy(&self.stderr).to_string()
24 }
25
26 pub fn into_output(self) -> Output {
29 Output {
30 status: exit_status_from_code(self.code),
31 stdout: self.stdout,
32 stderr: self.stderr,
33 }
34 }
35}
36
37#[cfg(unix)]
38fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
39 use std::os::unix::process::ExitStatusExt;
40 let raw = if code >= 0 { code << 8 } else { 1 << 8 };
41 std::process::ExitStatus::from_raw(raw)
42}
43
44#[cfg(windows)]
45fn exit_status_from_code(code: i32) -> std::process::ExitStatus {
46 use std::os::windows::process::ExitStatusExt;
47 let raw = if code >= 0 { code as u32 } else { 1 };
48 std::process::ExitStatus::from_raw(raw)
49}
50
51#[derive(Debug, Clone)]
52pub struct CmdOptions {
53 pub cwd: Option<PathBuf>,
54 pub envs: Vec<(String, String)>,
55 pub env_remove: Vec<String>,
56 pub stdin: Option<Vec<u8>>,
57 pub stdin_null: bool,
58}
59
60impl Default for CmdOptions {
61 fn default() -> Self {
62 Self {
63 cwd: None,
64 envs: Vec::new(),
65 env_remove: Vec::new(),
66 stdin: None,
67 stdin_null: true,
68 }
69 }
70}
71
72impl CmdOptions {
73 pub fn new() -> Self {
74 Self::default()
75 }
76
77 pub fn with_envs(mut self, envs: &[(&str, &str)]) -> Self {
78 for (key, value) in envs {
79 self = self.with_env(key, value);
80 }
81 self
82 }
83
84 pub fn with_env_remove_prefix(mut self, prefix: &str) -> Self {
85 for (key, _) in std::env::vars_os() {
86 let key = key.to_string_lossy();
87 if key.starts_with(prefix) {
88 self = self.with_env_remove(&key);
89 }
90 }
91 self
92 }
93
94 pub fn with_path_prepend(self, dir: &Path) -> Self {
95 let base = self
96 .envs
97 .iter()
98 .rev()
99 .find(|(key, _)| key == "PATH")
100 .map(|(_, value)| value.clone())
101 .or_else(|| std::env::var_os("PATH").map(|value| value.to_string_lossy().to_string()))
102 .unwrap_or_default();
103
104 let mut paths: Vec<PathBuf> = std::env::split_paths(std::ffi::OsStr::new(&base)).collect();
105 paths.insert(0, dir.to_path_buf());
106 let joined = std::env::join_paths(paths).expect("join paths");
107 let joined = joined.to_string_lossy().to_string();
108 self.with_env("PATH", &joined)
109 }
110
111 pub fn with_cwd(mut self, dir: &Path) -> Self {
112 self.cwd = Some(dir.to_path_buf());
113 self
114 }
115
116 pub fn with_env(mut self, key: &str, value: &str) -> Self {
117 self.envs.push((key.to_string(), value.to_string()));
118 self
119 }
120
121 pub fn with_env_remove(mut self, key: &str) -> Self {
122 self.env_remove.push(key.to_string());
123 self
124 }
125
126 pub fn with_stdin_bytes(mut self, bytes: &[u8]) -> Self {
127 self.stdin = Some(bytes.to_vec());
128 self
129 }
130
131 pub fn with_stdin_str(mut self, input: &str) -> Self {
132 self.stdin = Some(input.as_bytes().to_vec());
133 self
134 }
135
136 pub fn inherit_stdin(mut self) -> Self {
137 self.stdin_null = false;
138 self
139 }
140}
141
142pub fn run(bin: &Path, args: &[&str], envs: &[(&str, &str)], stdin: Option<&[u8]>) -> CmdOutput {
147 let mut options = CmdOptions::default().with_envs(envs);
148 if let Some(input) = stdin {
149 options = options.with_stdin_bytes(input);
150 }
151 run_with(bin, args, &options)
152}
153
154pub fn run_in_dir(
156 dir: &Path,
157 bin: &Path,
158 args: &[&str],
159 envs: &[(&str, &str)],
160 stdin: Option<&[u8]>,
161) -> CmdOutput {
162 let mut options = CmdOptions::default().with_cwd(dir).with_envs(envs);
163 if let Some(input) = stdin {
164 options = options.with_stdin_bytes(input);
165 }
166 run_with(bin, args, &options)
167}
168
169pub fn options_in_dir_with_envs(dir: &Path, envs: &[(&str, &str)]) -> CmdOptions {
171 CmdOptions::default().with_cwd(dir).with_envs(envs)
172}
173
174pub fn run_resolved(bin_name: &str, args: &[&str], options: &CmdOptions) -> CmdOutput {
176 let bin = crate::bin::resolve(bin_name);
177 run_with(&bin, args, options)
178}
179
180pub fn run_resolved_in_dir(
182 bin_name: &str,
183 dir: &Path,
184 args: &[&str],
185 envs: &[(&str, &str)],
186 stdin: Option<&[u8]>,
187) -> CmdOutput {
188 let mut options = options_in_dir_with_envs(dir, envs);
189 if let Some(input) = stdin {
190 options = options.with_stdin_bytes(input);
191 }
192 run_resolved(bin_name, args, &options)
193}
194
195pub fn run_with(bin: &Path, args: &[&str], options: &CmdOptions) -> CmdOutput {
196 run_impl(bin, args, options, None)
197}
198
199pub fn run_in_dir_with(dir: &Path, bin: &Path, args: &[&str], options: &CmdOptions) -> CmdOutput {
200 run_impl(bin, args, options, Some(dir))
201}
202
203fn run_impl(bin: &Path, args: &[&str], options: &CmdOptions, dir: Option<&Path>) -> CmdOutput {
204 let mut cmd = Command::new(bin);
205 if let Some(dir) = dir {
206 cmd.current_dir(dir);
207 } else if let Some(cwd) = options.cwd.as_deref() {
208 cmd.current_dir(cwd);
209 }
210
211 cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
212
213 for key in &options.env_remove {
214 cmd.env_remove(key);
215 }
216 for (key, value) in &options.envs {
217 cmd.env(key, value);
218 }
219
220 let output = match options.stdin.as_ref() {
221 Some(input) => {
222 cmd.stdin(Stdio::piped());
223 let mut child = cmd.spawn().expect("spawn command");
224 if let Some(mut writer) = child.stdin.take() {
225 writer.write_all(input).expect("write stdin");
226 }
227 child.wait_with_output().expect("wait command")
228 }
229 None => {
230 if options.stdin_null {
231 cmd.stdin(Stdio::null());
232 }
233 cmd.output().expect("run command")
234 }
235 };
236
237 CmdOutput {
238 code: output.status.code().unwrap_or(-1),
239 stdout: output.stdout,
240 stderr: output.stderr,
241 }
242}