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_env_remove_prefix(mut self, prefix: &str) -> Self {
78 for (key, _) in std::env::vars_os() {
79 let key = key.to_string_lossy();
80 if key.starts_with(prefix) {
81 self = self.with_env_remove(&key);
82 }
83 }
84 self
85 }
86
87 pub fn with_path_prepend(self, dir: &Path) -> Self {
88 let base = self
89 .envs
90 .iter()
91 .rev()
92 .find(|(key, _)| key == "PATH")
93 .map(|(_, value)| value.clone())
94 .or_else(|| std::env::var_os("PATH").map(|value| value.to_string_lossy().to_string()))
95 .unwrap_or_default();
96
97 let mut paths: Vec<PathBuf> = std::env::split_paths(std::ffi::OsStr::new(&base)).collect();
98 paths.insert(0, dir.to_path_buf());
99 let joined = std::env::join_paths(paths).expect("join paths");
100 let joined = joined.to_string_lossy().to_string();
101 self.with_env("PATH", &joined)
102 }
103
104 pub fn with_cwd(mut self, dir: &Path) -> Self {
105 self.cwd = Some(dir.to_path_buf());
106 self
107 }
108
109 pub fn with_env(mut self, key: &str, value: &str) -> Self {
110 self.envs.push((key.to_string(), value.to_string()));
111 self
112 }
113
114 pub fn with_env_remove(mut self, key: &str) -> Self {
115 self.env_remove.push(key.to_string());
116 self
117 }
118
119 pub fn with_stdin_bytes(mut self, bytes: &[u8]) -> Self {
120 self.stdin = Some(bytes.to_vec());
121 self
122 }
123
124 pub fn with_stdin_str(mut self, input: &str) -> Self {
125 self.stdin = Some(input.as_bytes().to_vec());
126 self
127 }
128
129 pub fn inherit_stdin(mut self) -> Self {
130 self.stdin_null = false;
131 self
132 }
133}
134
135pub fn run(bin: &Path, args: &[&str], envs: &[(&str, &str)], stdin: Option<&[u8]>) -> CmdOutput {
140 let mut options = CmdOptions::default();
141 for (key, value) in envs {
142 options = options.with_env(key, value);
143 }
144 if let Some(input) = stdin {
145 options = options.with_stdin_bytes(input);
146 }
147 run_with(bin, args, &options)
148}
149
150pub fn run_in_dir(
152 dir: &Path,
153 bin: &Path,
154 args: &[&str],
155 envs: &[(&str, &str)],
156 stdin: Option<&[u8]>,
157) -> CmdOutput {
158 let mut options = CmdOptions::default().with_cwd(dir);
159 for (key, value) in envs {
160 options = options.with_env(key, value);
161 }
162 if let Some(input) = stdin {
163 options = options.with_stdin_bytes(input);
164 }
165 run_with(bin, args, &options)
166}
167
168pub fn options_in_dir_with_envs(dir: &Path, envs: &[(&str, &str)]) -> CmdOptions {
170 let mut options = CmdOptions::default().with_cwd(dir);
171 for (key, value) in envs {
172 options = options.with_env(key, value);
173 }
174 options
175}
176
177pub fn run_resolved(bin_name: &str, args: &[&str], options: &CmdOptions) -> CmdOutput {
179 let bin = crate::bin::resolve(bin_name);
180 run_with(&bin, args, options)
181}
182
183pub fn run_resolved_in_dir(
185 bin_name: &str,
186 dir: &Path,
187 args: &[&str],
188 envs: &[(&str, &str)],
189 stdin: Option<&[u8]>,
190) -> CmdOutput {
191 let mut options = options_in_dir_with_envs(dir, envs);
192 if let Some(input) = stdin {
193 options = options.with_stdin_bytes(input);
194 }
195 run_resolved(bin_name, args, &options)
196}
197
198pub fn run_with(bin: &Path, args: &[&str], options: &CmdOptions) -> CmdOutput {
199 run_impl(bin, args, options, None)
200}
201
202pub fn run_in_dir_with(dir: &Path, bin: &Path, args: &[&str], options: &CmdOptions) -> CmdOutput {
203 run_impl(bin, args, options, Some(dir))
204}
205
206fn run_impl(bin: &Path, args: &[&str], options: &CmdOptions, dir: Option<&Path>) -> CmdOutput {
207 let mut cmd = Command::new(bin);
208 if let Some(dir) = dir {
209 cmd.current_dir(dir);
210 } else if let Some(cwd) = options.cwd.as_deref() {
211 cmd.current_dir(cwd);
212 }
213
214 cmd.args(args).stdout(Stdio::piped()).stderr(Stdio::piped());
215
216 for key in &options.env_remove {
217 cmd.env_remove(key);
218 }
219 for (key, value) in &options.envs {
220 cmd.env(key, value);
221 }
222
223 let output = match options.stdin.as_ref() {
224 Some(input) => {
225 cmd.stdin(Stdio::piped());
226 let mut child = cmd.spawn().expect("spawn command");
227 if let Some(mut writer) = child.stdin.take() {
228 writer.write_all(input).expect("write stdin");
229 }
230 child.wait_with_output().expect("wait command")
231 }
232 None => {
233 if options.stdin_null {
234 cmd.stdin(Stdio::null());
235 }
236 cmd.output().expect("run command")
237 }
238 };
239
240 CmdOutput {
241 code: output.status.code().unwrap_or(-1),
242 stdout: output.stdout,
243 stderr: output.stderr,
244 }
245}