Skip to content

Commit

Permalink
Provide ExitStatusError
Browse files Browse the repository at this point in the history
Closes #73125

This is in pursuance of
  Issue #73127 Consider adding #[must_use] to std::process::ExitStatus

In
  MR #81452 Add #[must_use] to [...] process::ExitStatus
we concluded that the existing arrangements in are too awkward
so adding that #[must_use] is blocked on improving the ergonomics.

I wrote a mini-RFC-style discusion of the approach in
  #73125 (comment)

Signed-off-by: Ian Jackson <ijackson@chiark.greenend.org.uk>
  • Loading branch information
ijackson committed May 12, 2021
1 parent b50c1bb commit e893089
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 13 deletions.
138 changes: 135 additions & 3 deletions library/std/src/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ use crate::ffi::OsStr;
use crate::fmt;
use crate::fs;
use crate::io::{self, Initializer, IoSlice, IoSliceMut};
use crate::num::NonZeroI32;
use crate::path::Path;
use crate::str;
use crate::sys::pipe::{read2, AnonPipe};
Expand Down Expand Up @@ -1387,8 +1388,8 @@ impl From<fs::File> for Stdio {
/// An `ExitStatus` represents every possible disposition of a process. On Unix this
/// is the **wait status**. It is *not* simply an *exit status* (a value passed to `exit`).
///
/// For proper error reporting of failed processes, print the value of `ExitStatus` using its
/// implementation of [`Display`](crate::fmt::Display).
/// For proper error reporting of failed processes, print the value of `ExitStatus` or
/// `ExitStatusError` using their implementations of [`Display`](crate::fmt::Display).
///
/// [`status`]: Command::status
/// [`wait`]: Child::wait
Expand All @@ -1401,6 +1402,29 @@ pub struct ExitStatus(imp::ExitStatus);
impl crate::sealed::Sealed for ExitStatus {}

impl ExitStatus {
/// Was termination successful? Returns a `Result`.
///
/// # Examples
///
/// ```
/// #![feature(exit_status_error)]
/// # if cfg!(unix) {
/// use std::process::Command;
///
/// let status = Command::new("ls")
/// .arg("/dev/nonexistent")
/// .status()
/// .expect("ls could not be executed");
///
/// println!("ls: {}", status);
/// status.exit_ok().expect_err("/dev/nonexistent could be listed!");
/// # } // cfg!(unix)
/// ```
#[unstable(feature = "exit_status_error", issue = "84908")]
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
self.0.exit_ok().map_err(ExitStatusError)
}

/// Was termination successful? Signal termination is not considered a
/// success, and success is defined as a zero exit status.
///
Expand All @@ -1422,7 +1446,7 @@ impl ExitStatus {
/// ```
#[stable(feature = "process", since = "1.0.0")]
pub fn success(&self) -> bool {
self.0.success()
self.0.exit_ok().is_ok()
}

/// Returns the exit code of the process, if any.
Expand Down Expand Up @@ -1476,6 +1500,114 @@ impl fmt::Display for ExitStatus {
}
}

/// Describes the result of a process after it has failed
///
/// Produced by the [`.exit_ok`](ExitStatus::exit_ok) method on [`ExitStatus`].
///
/// # Examples
///
/// ```
/// #![feature(exit_status_error)]
/// # if cfg!(unix) {
/// use std::process::{Command, ExitStatusError};
///
/// fn run(cmd: &str) -> Result<(),ExitStatusError> {
/// Command::new(cmd).status().unwrap().exit_ok()?;
/// Ok(())
/// }
///
/// run("true").unwrap();
/// run("false").unwrap_err();
/// # } // cfg!(unix)
/// ```
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
#[unstable(feature = "exit_status_error", issue = "84908")]
// The definition of imp::ExitStatusError should ideally be such that
// Result<(), imp::ExitStatusError> has an identical representation to imp::ExitStatus.
pub struct ExitStatusError(imp::ExitStatusError);

#[unstable(feature = "exit_status_error", issue = "84908")]
impl ExitStatusError {
/// Reports the exit code, if applicable, from an `ExitStatusError`.
///
/// In Unix terms the return value is the **exit status**: the value passed to `exit`, if the
/// process finished by calling `exit`. Note that on Unix the exit status is truncated to 8
/// bits, and that values that didn't come from a program's call to `exit` may be invented the
/// runtime system (often, for example, 255, 254, 127 or 126).
///
/// On Unix, this will return `None` if the process was terminated by a signal. If you want to
/// handle such situations specially, consider using
/// [`ExitStatusExt`](crate::os::unix::process::ExitStatusExt) (possibly after getting the
/// general `ExitStatus` by using [`status()`](ExitStatusError::status).
///
/// If the process finished by calling `exit` with a nonzero value, this will return
/// that exit status.
///
/// If the error was something else, it will return `None`.
///
/// If the process exited successfully (ie, by calling `exit(0)`), there is no
/// `ExitStatusError`. So the return value from `ExitStatusError::code()` is always nonzero.
///
/// # Examples
///
/// ```
/// #![feature(exit_status_error)]
/// # #[cfg(unix)] {
/// use std::process::Command;
///
/// let bad = Command::new("false").status().unwrap().exit_ok().unwrap_err();
/// assert_eq!(bad.code(), Some(1));
/// # } // #[cfg(unix)]
/// ```
pub fn code(&self) -> Option<i32> {
self.code_nonzero().map(Into::into)
}

/// Reports the exit code, if applicable, from an `ExitStatusError`, as a `NonZero`
///
/// This is exaclty like [`code()`](Self::code), except that it returns a `NonZeroI32`.
///
/// Plain `code`, returning a plain integer, is provided because is is often more convenient.
/// The returned value from `code()` is indeed also nonzero; use `code_nonzero()` when you want
/// a type-level guarantee of nonzeroness.
///
/// # Examples
///
/// ```
/// #![feature(exit_status_error)]
/// # if cfg!(unix) {
/// use std::convert::TryFrom;
/// use std::num::NonZeroI32;
/// use std::process::Command;
///
/// let bad = Command::new("false").status().unwrap().exit_ok().unwrap_err();
/// assert_eq!(bad.code_nonzero().unwrap(), NonZeroI32::try_from(1).unwrap());
/// # } // cfg!(unix)
/// ```
pub fn code_nonzero(&self) -> Option<NonZeroI32> {
self.0.code()
}

/// Converts an `ExitStatusError` (back) to an `ExitStatus`.
pub fn into_status(&self) -> ExitStatus {
ExitStatus(self.0.into())
}
}

#[unstable(feature = "exit_status_error", issue = "84908")]
impl Into<ExitStatus> for ExitStatusError {
fn into(self) -> ExitStatus {
ExitStatus(self.0.into())
}
}

#[unstable(feature = "exit_status_error", issue = "84908")]
impl fmt::Display for ExitStatusError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.into_status().fmt(f)
}
}

/// This type represents the status code a process can return to its
/// parent under normal termination.
///
Expand Down
2 changes: 1 addition & 1 deletion library/std/src/sys/unix/process/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
pub use self::process_common::{Command, CommandArgs, ExitCode, Stdio, StdioPipes};
pub use self::process_inner::{ExitStatus, Process};
pub use self::process_inner::{ExitStatus, ExitStatusError, Process};
pub use crate::ffi::OsString as EnvKey;
pub use crate::sys_common::process::CommandEnvs;

Expand Down
26 changes: 23 additions & 3 deletions library/std/src/sys/unix/process/process_fuchsia.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use crate::convert::TryInto;
use crate::convert::{TryFrom, TryInto};
use crate::fmt;
use crate::io;
use crate::mem;
use crate::num::{NonZeroI32, NonZeroI64};
use crate::ptr;

use crate::sys::process::process_common::*;
Expand Down Expand Up @@ -236,8 +237,11 @@ impl Process {
pub struct ExitStatus(i64);

impl ExitStatus {
pub fn success(&self) -> bool {
self.code() == Some(0)
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
match NonZeroI64::try_from(self.0) {
/* was nonzero */ Ok(failure) => Err(ExitStatusError(failure)),
/* was zero, couldn't convert */ Err(_) => Ok(()),
}
}

pub fn code(&self) -> Option<i32> {
Expand Down Expand Up @@ -306,3 +310,19 @@ impl fmt::Display for ExitStatus {
write!(f, "exit code: {}", self.0)
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ExitStatusError(NonZeroI64);

impl Into<ExitStatus> for ExitStatusError {
fn into(self) -> ExitStatus {
ExitStatus(self.0.into())
}
}

impl ExitStatusError {
pub fn code(self) -> Option<NonZeroI32> {
// fixme: affected by the same bug as ExitStatus::code()
ExitStatus(self.0.into()).code().map(|st| st.try_into().unwrap())
}
}
31 changes: 28 additions & 3 deletions library/std/src/sys/unix/process/process_unix.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use crate::convert::TryInto;
use crate::convert::{TryFrom, TryInto};
use crate::fmt;
use crate::io::{self, Error, ErrorKind};
use crate::mem;
use crate::num::NonZeroI32;
use crate::os::raw::NonZero_c_int;
use crate::ptr;
use crate::sys;
use crate::sys::cvt;
Expand Down Expand Up @@ -490,8 +492,16 @@ impl ExitStatus {
libc::WIFEXITED(self.0)
}

pub fn success(&self) -> bool {
self.code() == Some(0)
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
// This assumes that WIFEXITED(status) && WEXITSTATUS==0 corresponds to status==0. This is
// true on all actual versios of Unix, is widely assumed, and is specified in SuS
// https://pubs.opengroup.org/onlinepubs/9699919799/functions/wait.html . If it is not
// true for a platform pretending to be Unix, the tests (our doctests, and also
// procsss_unix/tests.rs) will spot it. `ExitStatusError::code` assumes this too.
match NonZero_c_int::try_from(self.0) {
/* was nonzero */ Ok(failure) => Err(ExitStatusError(failure)),
/* was zero, couldn't convert */ Err(_) => Ok(()),
}
}

pub fn code(&self) -> Option<i32> {
Expand Down Expand Up @@ -546,6 +556,21 @@ impl fmt::Display for ExitStatus {
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ExitStatusError(NonZero_c_int);

impl Into<ExitStatus> for ExitStatusError {
fn into(self) -> ExitStatus {
ExitStatus(self.0.into())
}
}

impl ExitStatusError {
pub fn code(self) -> Option<NonZeroI32> {
ExitStatus(self.0.into()).code().map(|st| st.try_into().unwrap())
}
}

#[cfg(test)]
#[path = "process_unix/tests.rs"]
mod tests;
18 changes: 17 additions & 1 deletion library/std/src/sys/unsupported/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use crate::ffi::OsStr;
use crate::fmt;
use crate::io;
use crate::marker::PhantomData;
use crate::num::NonZeroI32;
use crate::path::Path;
use crate::sys::fs::File;
use crate::sys::pipe::AnonPipe;
Expand Down Expand Up @@ -97,7 +98,7 @@ impl fmt::Debug for Command {
pub struct ExitStatus(!);

impl ExitStatus {
pub fn success(&self) -> bool {
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
self.0
}

Expand Down Expand Up @@ -134,6 +135,21 @@ impl fmt::Display for ExitStatus {
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ExitStatusError(ExitStatus);

impl Into<ExitStatus> for ExitStatusError {
fn into(self) -> ExitStatus {
self.0.0
}
}

impl ExitStatusError {
pub fn code(self) -> Option<NonZeroI32> {
self.0.0
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ExitCode(bool);

Expand Down
25 changes: 23 additions & 2 deletions library/std/src/sys/windows/process.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,17 +5,20 @@ mod tests;

use crate::borrow::Borrow;
use crate::collections::BTreeMap;
use crate::convert::{TryFrom, TryInto};
use crate::env;
use crate::env::split_paths;
use crate::ffi::{OsStr, OsString};
use crate::fmt;
use crate::fs;
use crate::io::{self, Error, ErrorKind};
use crate::mem;
use crate::num::NonZeroI32;
use crate::os::windows::ffi::OsStrExt;
use crate::path::Path;
use crate::ptr;
use crate::sys::c;
use crate::sys::c::NonZeroDWORD;
use crate::sys::cvt;
use crate::sys::fs::{File, OpenOptions};
use crate::sys::handle::Handle;
Expand Down Expand Up @@ -376,8 +379,11 @@ impl Process {
pub struct ExitStatus(c::DWORD);

impl ExitStatus {
pub fn success(&self) -> bool {
self.0 == 0
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
match NonZeroDWORD::try_from(self.0) {
/* was nonzero */ Ok(failure) => Err(ExitStatusError(failure)),
/* was zero, couldn't convert */ Err(_) => Ok(()),
}
}
pub fn code(&self) -> Option<i32> {
Some(self.0 as i32)
Expand Down Expand Up @@ -406,6 +412,21 @@ impl fmt::Display for ExitStatus {
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ExitStatusError(c::NonZeroDWORD);

impl Into<ExitStatus> for ExitStatusError {
fn into(self) -> ExitStatus {
ExitStatus(self.0.into())
}
}

impl ExitStatusError {
pub fn code(self) -> Option<NonZeroI32> {
Some((u32::from(self.0) as i32).try_into().unwrap())
}
}

#[derive(PartialEq, Eq, Clone, Copy, Debug)]
pub struct ExitCode(c::DWORD);

Expand Down

0 comments on commit e893089

Please sign in to comment.