1use std::{
5 borrow::Cow,
6 ffi::OsStr,
7 fmt::{Debug, Display},
8 ops::Deref,
9 path::Path,
10 process::{Child, Command, ExitStatus, Output, Stdio},
11};
12
13use anyhow::{anyhow, bail, Context, Result};
14
15fn lossy_string(v: &[u8]) -> Cow<str> {
16 String::from_utf8_lossy(v)
18}
19
20fn cmd_args<P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
21 cmd: P,
22 arguments: &[A],
23) -> Vec<String> {
24 let cmd_osstr: &OsStr = cmd.as_ref();
25 let mut vec = vec![cmd_osstr.to_string_lossy().to_string()];
26 for v in arguments {
27 let v_osstr: &OsStr = v.as_ref();
28 vec.push(v_osstr.to_string_lossy().to_string());
29 }
30 vec
31}
32
33fn check_exitstatus(exitstatus: &ExitStatus, acceptable_status_codes: &[i32]) -> Result<bool> {
34 if let Some(code) = exitstatus.code() {
35 if acceptable_status_codes.contains(&code) {
36 Ok(code == 0)
37 } else {
38 bail!("command exited with code {code}",)
39 }
40 } else {
41 bail!("command exited via signal, or other problem",)
42 }
43}
44
45#[derive(Debug)]
50pub struct Capturing {
51 stdout: Option<Stdio>,
52 stderr: Option<Stdio>,
53}
54
55impl Capturing {
56 pub fn none() -> Self {
57 Self {
58 stdout: None,
59 stderr: None,
60 }
61 }
62 pub fn stdout() -> Self {
63 Self {
64 stdout: Some(Stdio::piped()),
65 stderr: None,
66 }
67 }
68 pub fn stderr() -> Self {
69 Self {
70 stdout: None,
71 stderr: Some(Stdio::piped()),
72 }
73 }
74 pub fn both() -> Self {
75 Self {
76 stdout: Some(Stdio::piped()),
77 stderr: Some(Stdio::piped()),
78 }
79 }
80 pub fn available(&self) -> AvailableCaptures {
81 match self {
82 Self {
83 stdout: None,
84 stderr: None,
85 } => AvailableCaptures::None,
86 Self {
87 stdout: Some(_),
88 stderr: None,
89 } => AvailableCaptures::Stdout,
90 Self {
91 stdout: None,
92 stderr: Some(_),
93 } => AvailableCaptures::Stderr,
94 Self {
95 stdout: Some(_),
96 stderr: Some(_),
97 } => AvailableCaptures::Both,
98 }
99 }
100}
101
102#[derive(Debug, Clone, Copy)]
103pub enum AvailableCaptures {
104 Stdout,
105 Stderr,
106 Both,
107 None,
108}
109
110impl AvailableCaptures {
111 pub fn from_output(output: &Output) -> Self {
112 match (output.stdout.is_empty(), output.stderr.is_empty()) {
113 (true, true) => Self::None,
114 (true, false) => Self::Stderr,
115 (false, true) => Self::Stdout,
116 (false, false) => Self::Both,
117 }
118 }
119}
120
121#[derive(Debug)]
125pub struct Outputs<'t> {
126 pub truthy: bool,
129 pub available_captures: AvailableCaptures,
130 pub output: Output,
131 pub indent: &'t str,
133}
134
135impl<'t> Deref for Outputs<'t> {
136 type Target = Output;
137
138 fn deref(&self) -> &Self::Target {
139 &self.output
140 }
141}
142
143const ONLY_SHOW_NON_EMPTY_CAPTURES: bool = true;
144
145fn display_output(
146 output: &Output,
147 available_captures: AvailableCaptures,
148 f: &mut std::fmt::Formatter<'_>,
149 indent: &str,
150) -> std::fmt::Result {
151 let captures = if ONLY_SHOW_NON_EMPTY_CAPTURES {
152 AvailableCaptures::from_output(output)
153 } else {
154 available_captures
155 };
156 match captures {
157 AvailableCaptures::Stdout => f.write_fmt(format_args!(
158 "{},\n{indent}stdout: {}",
159 output.status,
160 lossy_string(&output.stdout),
161 )),
162 AvailableCaptures::Stderr => f.write_fmt(format_args!(
163 "{},\n{indent}stderr: {}",
164 output.status,
165 lossy_string(&output.stderr)
166 )),
167 AvailableCaptures::Both => f.write_fmt(format_args!(
168 "{},\n{indent}stdout: {}\n{indent}stderr: {}",
169 output.status,
170 lossy_string(&output.stdout),
171 lossy_string(&output.stderr)
172 )),
173 AvailableCaptures::None => f.write_fmt(format_args!("{}", output.status,)),
174 }
175}
176
177impl<'t> Display for Outputs<'t> {
178 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
179 display_output(&self.output, self.available_captures, f, self.indent)
180 }
181}
182
183struct DisplayOutput<'t> {
186 available_captures: AvailableCaptures,
187 output: &'t Output,
188 indent: &'t str,
189}
190
191impl<'t> Display for DisplayOutput<'t> {
192 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
193 display_output(&self.output, self.available_captures, f, self.indent)
194 }
195}
196
197fn check_exitstatus_context<'t, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
198 in_directory: &'t Path,
199 cmd: &'t P,
200 arguments: &'t [A],
201 available_captures: AvailableCaptures,
202 output: &'t Output,
203) -> impl Fn() -> anyhow::Error + 't {
204 move || {
205 let (cmd_args, in_dir, output) = (
206 cmd_args(cmd, arguments),
207 in_directory.to_string_lossy().to_string(),
208 DisplayOutput {
209 available_captures,
210 output,
211 indent: "\t",
212 },
213 );
214 anyhow!("running {cmd_args:?} in directory {in_dir:?}, {output}")
215 }
216}
217
218pub fn command_with_settings<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
219 in_directory: D,
220 cmd: P,
221 arguments: &[A],
222 set_env: &[(&str, &str)],
223 captures: Capturing,
224) -> Command {
225 let mut c = Command::new(&cmd);
226 c.args(arguments).current_dir(in_directory);
227 for (k, v) in set_env {
228 c.env(k, v);
229 }
230 c.stdout(if let Some(cap) = captures.stdout {
231 cap
232 } else {
233 Stdio::inherit()
234 });
235 c.stderr(if let Some(cap) = captures.stderr {
236 cap
237 } else {
238 Stdio::inherit()
239 });
240 c
241}
242
243pub fn spawn<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
253 in_directory: D,
254 cmd: P,
255 arguments: &[A],
256 set_env: &[(&str, &str)],
257 captures: Capturing,
258) -> Result<Child> {
259 let mut c = command_with_settings(in_directory.as_ref(), &cmd, arguments, set_env, captures);
260 c.spawn().with_context(|| {
261 let (cmd_args, in_dir) = (
262 cmd_args(&cmd, arguments),
263 in_directory.as_ref().to_string_lossy().to_string(),
264 );
265 anyhow!("running {cmd_args:?} in directory {in_dir:?}",)
266 })
267}
268
269pub fn run<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
277 in_directory: D,
278 cmd: P,
279 arguments: &[A],
280 set_env: &[(&str, &str)],
281 acceptable_status_codes: &[i32],
282 silencing: Capturing,
283) -> Result<bool> {
284 let get_cmd_args_dir = || {
285 (
286 cmd_args(&cmd, arguments),
287 in_directory.as_ref().to_string_lossy().to_string(),
288 )
289 };
290 let available_captures = silencing.available();
291 let output = run_output(in_directory.as_ref(), &cmd, arguments, set_env, silencing)?;
292 let exitstatus = output.status;
293 check_exitstatus(&exitstatus, acceptable_status_codes).with_context(|| {
294 let (cmd_args, in_dir) = get_cmd_args_dir();
295 let outputs = Outputs {
296 truthy: false,
297 available_captures,
298 output,
299 indent: "", };
301 anyhow!("running {cmd_args:?} in directory {in_dir:?}: {outputs}")
302 })
303}
304
305pub fn run_output<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
306 in_directory: D,
307 cmd: P,
308 arguments: &[A],
309 set_env: &[(&str, &str)],
310 captures: Capturing,
311) -> Result<Output> {
312 let mut c = command_with_settings(in_directory.as_ref(), &cmd, arguments, set_env, captures);
313 c.output().with_context(|| {
314 let (cmd_args, in_dir) = (
315 cmd_args(&cmd, arguments),
316 in_directory.as_ref().to_string_lossy().to_string(),
317 );
318 anyhow!("running {cmd_args:?} in directory {in_dir:?}",)
319 })
320}
321
322pub fn run_outputs<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
325 in_directory: D,
326 cmd: P,
327 arguments: &[A],
328 set_env: &[(&str, &str)],
329 acceptable_status_codes: &[i32],
330) -> Result<Outputs<'static>> {
331 let captures = Capturing::both();
332 let available_captures = captures.available();
333 let output = run_output(in_directory.as_ref(), &cmd, arguments, set_env, captures)?;
334 let truthy = check_exitstatus(&output.status, acceptable_status_codes).with_context(|| {
335 let (cmd_args, in_dir, output) = (
336 cmd_args(&cmd, arguments),
337 in_directory.as_ref().to_string_lossy().to_string(),
338 DisplayOutput {
339 available_captures,
340 output: &output,
341 indent: "\t",
342 },
343 );
344 anyhow!("running {cmd_args:?} in directory {in_dir:?}, {output}")
345 })?;
346 Ok(Outputs {
347 output,
348 truthy,
349 available_captures,
350 indent: "\t\t",
351 })
352}
353
354pub fn run_stdout<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
356 in_directory: D,
357 cmd: P,
358 arguments: &[A],
359 set_env: &[(&str, &str)],
360 acceptable_status_codes: &[i32],
361) -> Result<Outputs<'static>> {
362 let captures = Capturing::stdout();
363 let available_captures = captures.available();
364 let output = run_output(in_directory.as_ref(), &cmd, arguments, set_env, captures)?;
365 let truthy = check_exitstatus(&output.status, acceptable_status_codes).with_context(|| {
366 let (cmd_args, in_dir, output) = (
367 cmd_args(&cmd, arguments),
368 in_directory.as_ref().to_string_lossy().to_string(),
369 DisplayOutput {
370 available_captures,
371 output: &output,
372 indent: "\n",
373 },
374 );
375 anyhow!("running {cmd_args:?} in directory {in_dir:?}, {output}")
376 })?;
377 Ok(Outputs {
378 output,
379 truthy,
380 available_captures,
381 indent: "\t\t",
382 })
383}
384
385pub fn run_stderr<P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
387 in_directory: &Path,
388 cmd: P,
389 arguments: &[A],
390 set_env: &[(&str, &str)],
391 acceptable_status_codes: &[i32],
392) -> Result<Outputs<'static>> {
393 let captures = Capturing::stderr();
394 let available_captures = captures.available();
395 let output = run_output(in_directory, &cmd, arguments, set_env, captures)?;
396 let truthy = check_exitstatus(&output.status, acceptable_status_codes).with_context(
397 check_exitstatus_context(in_directory, &cmd, arguments, available_captures, &output),
398 )?;
399 Ok(Outputs {
400 output,
401 truthy,
402 available_captures,
403 indent: "\t\t",
404 })
405}
406
407pub fn run_stdout_string<D: AsRef<Path>, P: AsRef<OsStr> + Debug, A: AsRef<OsStr> + Debug>(
409 in_directory: D,
410 cmd: P,
411 arguments: &[A],
412 set_env: &[(&str, &str)],
413 acceptable_status_codes: &[i32],
414 trim_ending_newline: bool,
415) -> Result<String> {
416 let outputs = run_stdout(
417 in_directory,
418 cmd,
419 arguments,
420 set_env,
421 acceptable_status_codes,
422 )?;
423 let mut stdout = String::from_utf8(outputs.output.stdout)?;
424 if trim_ending_newline {
425 let end = "\n";
426 if stdout.ends_with(end) {
427 stdout = stdout[0..stdout.len() - end.len()].into();
428 }
429 }
430 Ok(stdout)
431}