-
Notifications
You must be signed in to change notification settings - Fork 12.9k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add convenience API to std::process::Command #89004
Conversation
r? @yaahc (rust-highfive has picked a reviewer for you, use r? to override) |
Will file a tracking issue if this is something we'd want to have at all! |
This comment has been minimized.
This comment has been minimized.
6fc67db
to
07819fe
Compare
cc #84908 |
What about renaming |
Yeah, the name/API here is definitely not "orthogonal". For full generality, we'd need a matrix with binary/text stdour/stderr/both. But, as this is a convenience API, I figured it's better to make the common case short. We have the precedent in missing "_to_string": we have Another thing which is reasonable to have is Overall, I feel that there's certainly an unfortunately uncomfortably large design space here, and it's unclear how to best explore it. So, on the meta level, I propose reaching consensus in the PR on that:
After that, we can merge and iterate on the tracking issue. |
I think these methods would be better on |
I don't think I think I still prefer the API to be available on the command directly. While #84908 is a bit more general, we already have a fully general API for arbitrary custom error management. What we don't have is a sane-defaults one-liner. |
The it is quite ambiguous here, I had to look at the methods to get what you actually mean 😅 Ok, |
I agree that this is a good place to start building consensus. My opinion is that this is a problem worth solving. I've built similar abstractions on top of the Regarding the currently suggested API's shape: My first issue is that Second, I agree with @the8472, Overall On that note, I'm curious how other language's convenience APIs for capturing the output of a subprocess handle stderr. Does Julia's |
Yeah, as a designer of the API, I also see how two calls isn't really a big deal, and how this is cleaner and more orthogonal. However, from an API user point of view, the difference seem rather meaningful. As a user of the API, I want to "run the command", or "get the command's output". It's a single atomic operation. As a metaphor, when running commands in a terminal, I don't think whether I misstyped the command name or the argument name, both are just "command failed to run": $ cargo tst
error: no such subcommand: `tst`
Did you mean `test`?
$ carg test
carg: command not found For use-case I have in mind, one would always just write It's not that big deal (although the extra possibility of forgetting to check the exit status is a bit concerning), but, still, it would be odd if shortcut API included more than strictly necessary. Regarding the context, one interesting bit here is that, by letting the API hang off This is similar to I sort-of feel that we are conflating three use-cases here:
I feel that Alice is rather happy -- the current API already allows to do fine-grained error reporting. She only needs Bob has become considerably happier since the introduction of Carol feels that the language is a joke: seriously, upon failing to read a file you get neither the filename nor backtrace by default? The proposed API helps Bob a lot. If at some point we'll learn how to add more context to
Good call! It's interesting that Python's
|
I've realized that that's actually a bigger deal: use anyhow
// V1
let stdout = command.read_stdout()
.with_context(|| format!("failed to run {:?}", comand))?;
// V2
let stdout = command.output()
.with_context(|| format!("failed to run {:?}", comand))?
.utf8_stdout()
.with_context(|| format!("failed to run {:?}", comand))?; |
The V2 version isn't as bad if you use try blocks but yea I see what you're saying, particularly the bit about wanting errors to be maximally useful by default. I worry about being able to pick a format that actually works for everyone. My fear being that it will be too long or will be missing env information that is relevant, but I think it's worth a try as long as it doesn't affect reporting of existing usages of If you're going to go with this argument though I don't think this PR should be merged as a half measure. I'd want to see this additional reporting of the command itself added to the new |
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`.
07819fe
to
6063405
Compare
The job Click to see the possible cause of the failure (guessed by this bot)
|
let output = self.output()?; | ||
self.check_status(output.status)?; | ||
let mut stdout = output.stdout; | ||
if stdout.last() == Some(&b'\n') { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe use strip.suffix instead? let stdout = stdout.strip_suffix(b'\n').unwrap_or(stdout)
or something.
Ok(()) | ||
} else { | ||
Err(io::Error::new_const( | ||
io::ErrorKind::Uncategorized, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think Uncategorized
is right. IMO this should get its own ErrorKind
. Perhaps you'd like to steal io::ErrorKind::SubprocessFailed
from 88528a1 ?
}) | ||
} | ||
|
||
fn check_status(&self, status: ExitStatus) -> io::Result<()> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I wonder if this would be usefully pub
.
/// # Ok::<(), std::io::Error>(()) | ||
/// ``` | ||
#[unstable(feature = "command_easy_api", issue = "none")] | ||
pub fn read_stdout(&mut self) -> io::Result<String> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Bikeshed: I suggest get_stdout
for the name? To me, read_stdout
fails to suggest that it also waits for termination.
|
@matklad Thank you so much for this MR. IMO we absolutely definitely need both of the things you are doing here. See my tracking issue #73131. I wonder if it would be easier to do your two halves in two separate MRs? I'm hoping I've made a few comments on details. See also #73126 |
I think I found @matklad's use case personae in #89004 (comment) very convincing. I have been (roughly speaking) all of those people. Alice is well-served apart from the fact that Bob and Carol are very ill-served. We should provide facilities for them that DTRT conveniently. I think being able to conveniently bundle a subprocess failure as an
See my complaints in #73131 about
I can't speak to Julia. I have done a lot of this kind of thing in Perl and quite a bit in Tcl, and also some in Python. Frankly, this area is rather subtle and most languages have made a mess of it. Rust's current Perl's backquote operator has the command inherit the script's stderr. If you want to do something else you need to do a lot of hand-coding. It reports errors in a funky and not particularly convenient way (but, frankly, that is typical for Perl). It doesn't trim a final newline but Perl has a Tcl's Python3's What I would like
|
Yeah, evidently I've run out of steam on this one, so closing :) |
@matklad thanks for your hard work, and sorry if I contributed to bogging it down. Please comment on my rust-lang/rfcs#3362 |
This is similar in spirit to
fs::read_to_string
-- not strictlynecessary, 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 otherauxiliary 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, whileI 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 likestatus
, except that it check thatstatus
is zeroread_stdout
is likeoutput
, except that it checks status anddecodes to
String
. It also chomps the last newline, which is modeledafter shell substitution behavior (
echo -n "$(git rev-parse HEAD)"
)and Julia's
readchomp
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
.