run_git/
command.rs

1//! A more convenient interface than std::process::* for running
2//! external programs.
3
4use 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    // XX different on Windows?
17    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/// Which outputs and how they should be captured. Can't clone, the
46/// contained handles are not clonable; use `available` before
47/// consuming this if you need to retain the knowledge about available
48/// captures.
49#[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/// Wrapper around `Output` that dereferences to it, but also offers a
122/// `truthy` field, and implements `Display` to show a multi-line message
123/// with both process status and its outputs.
124#[derive(Debug)]
125pub struct Outputs<'t> {
126    /// Whether process exited with status 0 (or maybe in the future a
127    /// set of status codes that represent success)
128    pub truthy: bool,
129    pub available_captures: AvailableCaptures,
130    pub output: Output,
131    /// Prefixed to `output` lines, "\t" by default
132    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
183// And a temporary helper to re-use `display_output` when truthy is
184// not available
185struct 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
243/// Run `cmd` with `arguments` and the env overridden with the
244/// key-value pairs in `set_env`, return a `Child` handle (does *not*
245/// wait for its completion). If you don't want to capture outputs,
246/// pass `Captures::none()` to `captures`. Note: if you just want to
247/// run a process in the background and are not actually reading from
248/// the filehandles in `Child`, then you definitely want to set this
249/// to `Captures::none()`, because otherwise the outputs will go
250/// nowhere, or even block the child process after filling the pipe
251/// buffer.
252pub 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
269/// Run `cmd` with `arguments` and the env overridden with the
270/// key-value pairs in `set_env`, wait for its completion. Returns an
271/// error if cmd exited with a code that is not in
272/// `acceptable_status_codes`.  Returns true when 0 is in
273/// acceptable_status_codes and cmd exited with status 0, false for
274/// other accepted status codes. `silencing` specifies captures that
275/// should be done, which are dropped unless there's an error.
276pub 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: "", // XX
300        };
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
322/// Same as `run` but captures outputs, returning (exited_0, stdout,
323/// stderr)
324pub 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
354/// Same as `run` but captures and returns stdout.
355pub 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
385/// Same as `run` but captures and returns stderr.
386pub 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
407/// Same as `run_stdout` but returns stdout as a utf-8 decoded string.
408pub 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}