Skip to content

Commit 6063405

Browse files
committed
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`.
1 parent 34327f6 commit 6063405

File tree

1 file changed

+78
-0
lines changed

1 file changed

+78
-0
lines changed

library/std/src/process.rs

+78
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,84 @@ impl Command {
10291029
pub fn get_current_dir(&self) -> Option<&Path> {
10301030
self.inner.get_current_dir()
10311031
}
1032+
1033+
/// Convenience wrapper around [`Command::status`].
1034+
///
1035+
/// Returns an error if the command exited with non-zero status.
1036+
///
1037+
/// # Examples
1038+
///
1039+
/// ```no_run
1040+
/// # #![feature(command_easy_api)]
1041+
/// use std::process::Command;
1042+
///
1043+
/// let res = Command::new("cat")
1044+
/// .arg("no-such-file.txt")
1045+
/// .run();
1046+
/// assert!(res.is_err());
1047+
/// ```
1048+
#[unstable(feature = "command_easy_api", issue = "none")]
1049+
pub fn run(&mut self) -> io::Result<()> {
1050+
let status = self.status()?;
1051+
self.check_status(status)
1052+
}
1053+
1054+
/// Convenience wrapper around [`Command::output`] to get the contents of
1055+
/// standard output as [`String`].
1056+
///
1057+
/// The final newline (`\n`) is stripped from the output. Unlike
1058+
/// [`Command::output`], `stderr` is inherited by default.
1059+
///
1060+
/// Returns an error if the command exited with non-zero status or if the
1061+
/// output was not valid UTF-8.
1062+
///
1063+
/// # Examples
1064+
///
1065+
/// ```no_run
1066+
/// # #![feature(command_easy_api)]
1067+
/// use std::process::Command;
1068+
/// let output = Command::new("git")
1069+
/// .args(["rev-parse", "--short", "1.0.0"])
1070+
/// .read_stdout()?;
1071+
///
1072+
/// assert_eq!(output, "55bd4f8ff2b");
1073+
/// # Ok::<(), std::io::Error>(())
1074+
/// ```
1075+
#[unstable(feature = "command_easy_api", issue = "none")]
1076+
pub fn read_stdout(&mut self) -> io::Result<String> {
1077+
// FIXME: This shouldn't override the stderr to inherit, and merely use
1078+
// a default.
1079+
self.stderr(Stdio::inherit());
1080+
1081+
let output = self.output()?;
1082+
self.check_status(output.status)?;
1083+
let mut stdout = output.stdout;
1084+
if stdout.last() == Some(&b'\n') {
1085+
stdout.pop();
1086+
}
1087+
String::from_utf8(stdout).map_err(|_| {
1088+
io::Error::new_const(
1089+
io::ErrorKind::InvalidData,
1090+
format!("command {:?} produced non-UTF-8 output"),
1091+
)
1092+
})
1093+
}
1094+
1095+
fn check_status(&self, status: ExitStatus) -> io::Result<()> {
1096+
if status.success() {
1097+
Ok(())
1098+
} else {
1099+
Err(io::Error::new_const(
1100+
io::ErrorKind::Uncategorized,
1101+
match status.code() {
1102+
Some(code) => {
1103+
format!("command {:?} exited with non zero status ({})", self, code)
1104+
}
1105+
None => format!("command {:?} was terminated", self),
1106+
},
1107+
))
1108+
}
1109+
}
10321110
}
10331111

10341112
#[stable(feature = "rust1", since = "1.0.0")]

0 commit comments

Comments
 (0)