From 6063405c1b502f1c72262278e3b710854c10be25 Mon Sep 17 00:00:00 2001 From: Aleksey Kladov Date: Thu, 16 Sep 2021 10:25:55 +0300 Subject: [PATCH] Add convenience API to std::process::Command This is similar in spirit to `fs::read_to_string` -- not strictly necessary, but very convenient for many use-cases. Often you need to write essentially a "bash script" in Rust which spawns other processes. This usually happens in `build.rs`, tests, or other auxiliary code. Using std's Command API for this is inconvenient, primarily because checking for exit code (and utf8-validity, if you need output) turns one-liner into a paragraph of boilerplate. While there are crates.io crates to help with this, using them often is not convenient. In fact, I maintain one such crate (`xshell`) and, while I do use it when I am writing something substantial, for smaller things I tend to copy-paste this *std-only* snippet: https://github.com/rust-analyzer/rust-analyzer/blob/ae36af2bd43906ddb1eeff96d76754f012c0a2c7/crates/rust-analyzer/build.rs#L61-L73 So, this PR adds two convenience functions to cover two most common use-cases: * `run` is like `status`, except that it check that `status` is zero * `read_stdout` is like `output`, except that it checks status and decodes to `String`. It also chomps the last newline, which is modeled after shell substitution behavior (`echo -n "$(git rev-parse HEAD)"`) and Julia's [`readchomp`](https://docs.julialang.org/en/v1/manual/running-external-programs/) Note that this is not the ideal API. In particular, error messages do not include the command line or stderr. So, for user-facing commands, you'd have to write you own code or use a process-spawning library like `duct`. --- library/std/src/process.rs | 78 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 78 insertions(+) diff --git a/library/std/src/process.rs b/library/std/src/process.rs index c9b21fcf9c6d2..e57b339fa3ddd 100644 --- a/library/std/src/process.rs +++ b/library/std/src/process.rs @@ -1029,6 +1029,84 @@ impl Command { pub fn get_current_dir(&self) -> Option<&Path> { self.inner.get_current_dir() } + + /// Convenience wrapper around [`Command::status`]. + /// + /// Returns an error if the command exited with non-zero status. + /// + /// # Examples + /// + /// ```no_run + /// # #![feature(command_easy_api)] + /// use std::process::Command; + /// + /// let res = Command::new("cat") + /// .arg("no-such-file.txt") + /// .run(); + /// assert!(res.is_err()); + /// ``` + #[unstable(feature = "command_easy_api", issue = "none")] + pub fn run(&mut self) -> io::Result<()> { + let status = self.status()?; + self.check_status(status) + } + + /// Convenience wrapper around [`Command::output`] to get the contents of + /// standard output as [`String`]. + /// + /// The final newline (`\n`) is stripped from the output. Unlike + /// [`Command::output`], `stderr` is inherited by default. + /// + /// Returns an error if the command exited with non-zero status or if the + /// output was not valid UTF-8. + /// + /// # Examples + /// + /// ```no_run + /// # #![feature(command_easy_api)] + /// use std::process::Command; + /// let output = Command::new("git") + /// .args(["rev-parse", "--short", "1.0.0"]) + /// .read_stdout()?; + /// + /// assert_eq!(output, "55bd4f8ff2b"); + /// # Ok::<(), std::io::Error>(()) + /// ``` + #[unstable(feature = "command_easy_api", issue = "none")] + pub fn read_stdout(&mut self) -> io::Result { + // FIXME: This shouldn't override the stderr to inherit, and merely use + // a default. + self.stderr(Stdio::inherit()); + + let output = self.output()?; + self.check_status(output.status)?; + let mut stdout = output.stdout; + if stdout.last() == Some(&b'\n') { + stdout.pop(); + } + String::from_utf8(stdout).map_err(|_| { + io::Error::new_const( + io::ErrorKind::InvalidData, + format!("command {:?} produced non-UTF-8 output"), + ) + }) + } + + fn check_status(&self, status: ExitStatus) -> io::Result<()> { + if status.success() { + Ok(()) + } else { + Err(io::Error::new_const( + io::ErrorKind::Uncategorized, + match status.code() { + Some(code) => { + format!("command {:?} exited with non zero status ({})", self, code) + } + None => format!("command {:?} was terminated", self), + }, + )) + } + } } #[stable(feature = "rust1", since = "1.0.0")]