Skip to content

Commit 25a277f

Browse files
committed
Auto merge of #82973 - ijackson:exitstatuserror, r=yaahc
Provide ExitStatusError Closes #73125 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)
2 parents 5f10d31 + 26c782b commit 25a277f

File tree

8 files changed

+281
-20
lines changed

8 files changed

+281
-20
lines changed

library/std/src/os/unix/process.rs

+46-7
Original file line numberDiff line numberDiff line change
@@ -201,22 +201,32 @@ impl CommandExt for process::Command {
201201
}
202202
}
203203

204-
/// Unix-specific extensions to [`process::ExitStatus`].
204+
/// Unix-specific extensions to [`process::ExitStatus`] and
205+
/// [`ExitStatusError`](process::ExitStatusError).
205206
///
206-
/// On Unix, `ExitStatus` **does not necessarily represent an exit status**, as passed to the
207-
/// `exit` system call or returned by [`ExitStatus::code()`](crate::process::ExitStatus::code).
208-
/// It represents **any wait status**, as returned by one of the `wait` family of system calls.
207+
/// On Unix, `ExitStatus` **does not necessarily represent an exit status**, as
208+
/// passed to the `exit` system call or returned by
209+
/// [`ExitStatus::code()`](crate::process::ExitStatus::code). It represents **any wait status**
210+
/// as returned by one of the `wait` family of system
211+
/// calls.
209212
///
210-
/// This is because a Unix wait status (a Rust `ExitStatus`) can represent a Unix exit status, but
211-
/// can also represent other kinds of process event.
213+
/// A Unix wait status (a Rust `ExitStatus`) can represent a Unix exit status, but can also
214+
/// represent other kinds of process event.
212215
///
213216
/// This trait is sealed: it cannot be implemented outside the standard library.
214217
/// This is so that future additional methods are not breaking changes.
215218
#[stable(feature = "rust1", since = "1.0.0")]
216219
pub trait ExitStatusExt: Sealed {
217-
/// Creates a new `ExitStatus` from the raw underlying integer status value from `wait`
220+
/// Creates a new `ExitStatus` or `ExitStatusError` from the raw underlying integer status
221+
/// value from `wait`
218222
///
219223
/// The value should be a **wait status, not an exit status**.
224+
///
225+
/// # Panics
226+
///
227+
/// Panics on an attempt to make an `ExitStatusError` from a wait status of `0`.
228+
///
229+
/// Making an `ExitStatus` always succeds and never panics.
220230
#[stable(feature = "exit_status_from", since = "1.12.0")]
221231
fn from_raw(raw: i32) -> Self;
222232

@@ -278,6 +288,35 @@ impl ExitStatusExt for process::ExitStatus {
278288
}
279289
}
280290

291+
#[unstable(feature = "exit_status_error", issue = "84908")]
292+
impl ExitStatusExt for process::ExitStatusError {
293+
fn from_raw(raw: i32) -> Self {
294+
process::ExitStatus::from_raw(raw)
295+
.exit_ok()
296+
.expect_err("<ExitStatusError as ExitStatusExt>::from_raw(0) but zero is not an error")
297+
}
298+
299+
fn signal(&self) -> Option<i32> {
300+
self.into_status().signal()
301+
}
302+
303+
fn core_dumped(&self) -> bool {
304+
self.into_status().core_dumped()
305+
}
306+
307+
fn stopped_signal(&self) -> Option<i32> {
308+
self.into_status().stopped_signal()
309+
}
310+
311+
fn continued(&self) -> bool {
312+
self.into_status().continued()
313+
}
314+
315+
fn into_raw(self) -> i32 {
316+
self.into_status().into_raw()
317+
}
318+
}
319+
281320
#[stable(feature = "process_extensions", since = "1.2.0")]
282321
impl FromRawFd for process::Stdio {
283322
#[inline]

library/std/src/process.rs

+141-3
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,7 @@ use crate::ffi::OsStr;
110110
use crate::fmt;
111111
use crate::fs;
112112
use crate::io::{self, Initializer, IoSlice, IoSliceMut};
113+
use crate::num::NonZeroI32;
113114
use crate::path::Path;
114115
use crate::str;
115116
use crate::sys::pipe::{read2, AnonPipe};
@@ -1387,8 +1388,8 @@ impl From<fs::File> for Stdio {
13871388
/// An `ExitStatus` represents every possible disposition of a process. On Unix this
13881389
/// is the **wait status**. It is *not* simply an *exit status* (a value passed to `exit`).
13891390
///
1390-
/// For proper error reporting of failed processes, print the value of `ExitStatus` using its
1391-
/// implementation of [`Display`](crate::fmt::Display).
1391+
/// For proper error reporting of failed processes, print the value of `ExitStatus` or
1392+
/// `ExitStatusError` using their implementations of [`Display`](crate::fmt::Display).
13921393
///
13931394
/// [`status`]: Command::status
13941395
/// [`wait`]: Child::wait
@@ -1401,6 +1402,29 @@ pub struct ExitStatus(imp::ExitStatus);
14011402
impl crate::sealed::Sealed for ExitStatus {}
14021403

14031404
impl ExitStatus {
1405+
/// Was termination successful? Returns a `Result`.
1406+
///
1407+
/// # Examples
1408+
///
1409+
/// ```
1410+
/// #![feature(exit_status_error)]
1411+
/// # if cfg!(unix) {
1412+
/// use std::process::Command;
1413+
///
1414+
/// let status = Command::new("ls")
1415+
/// .arg("/dev/nonexistent")
1416+
/// .status()
1417+
/// .expect("ls could not be executed");
1418+
///
1419+
/// println!("ls: {}", status);
1420+
/// status.exit_ok().expect_err("/dev/nonexistent could be listed!");
1421+
/// # } // cfg!(unix)
1422+
/// ```
1423+
#[unstable(feature = "exit_status_error", issue = "84908")]
1424+
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
1425+
self.0.exit_ok().map_err(ExitStatusError)
1426+
}
1427+
14041428
/// Was termination successful? Signal termination is not considered a
14051429
/// success, and success is defined as a zero exit status.
14061430
///
@@ -1422,7 +1446,7 @@ impl ExitStatus {
14221446
/// ```
14231447
#[stable(feature = "process", since = "1.0.0")]
14241448
pub fn success(&self) -> bool {
1425-
self.0.success()
1449+
self.0.exit_ok().is_ok()
14261450
}
14271451

14281452
/// Returns the exit code of the process, if any.
@@ -1476,6 +1500,120 @@ impl fmt::Display for ExitStatus {
14761500
}
14771501
}
14781502

1503+
/// Allows extension traits within `std`.
1504+
#[unstable(feature = "sealed", issue = "none")]
1505+
impl crate::sealed::Sealed for ExitStatusError {}
1506+
1507+
/// Describes the result of a process after it has failed
1508+
///
1509+
/// Produced by the [`.exit_ok`](ExitStatus::exit_ok) method on [`ExitStatus`].
1510+
///
1511+
/// # Examples
1512+
///
1513+
/// ```
1514+
/// #![feature(exit_status_error)]
1515+
/// # if cfg!(unix) {
1516+
/// use std::process::{Command, ExitStatusError};
1517+
///
1518+
/// fn run(cmd: &str) -> Result<(),ExitStatusError> {
1519+
/// Command::new(cmd).status().unwrap().exit_ok()?;
1520+
/// Ok(())
1521+
/// }
1522+
///
1523+
/// run("true").unwrap();
1524+
/// run("false").unwrap_err();
1525+
/// # } // cfg!(unix)
1526+
/// ```
1527+
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
1528+
#[unstable(feature = "exit_status_error", issue = "84908")]
1529+
// The definition of imp::ExitStatusError should ideally be such that
1530+
// Result<(), imp::ExitStatusError> has an identical representation to imp::ExitStatus.
1531+
pub struct ExitStatusError(imp::ExitStatusError);
1532+
1533+
#[unstable(feature = "exit_status_error", issue = "84908")]
1534+
impl ExitStatusError {
1535+
/// Reports the exit code, if applicable, from an `ExitStatusError`.
1536+
///
1537+
/// In Unix terms the return value is the **exit status**: the value passed to `exit`, if the
1538+
/// process finished by calling `exit`. Note that on Unix the exit status is truncated to 8
1539+
/// bits, and that values that didn't come from a program's call to `exit` may be invented by the
1540+
/// runtime system (often, for example, 255, 254, 127 or 126).
1541+
///
1542+
/// On Unix, this will return `None` if the process was terminated by a signal. If you want to
1543+
/// handle such situations specially, consider using methods from
1544+
/// [`ExitStatusExt`](crate::os::unix::process::ExitStatusExt).
1545+
///
1546+
/// If the process finished by calling `exit` with a nonzero value, this will return
1547+
/// that exit status.
1548+
///
1549+
/// If the error was something else, it will return `None`.
1550+
///
1551+
/// If the process exited successfully (ie, by calling `exit(0)`), there is no
1552+
/// `ExitStatusError`. So the return value from `ExitStatusError::code()` is always nonzero.
1553+
///
1554+
/// # Examples
1555+
///
1556+
/// ```
1557+
/// #![feature(exit_status_error)]
1558+
/// # #[cfg(unix)] {
1559+
/// use std::process::Command;
1560+
///
1561+
/// let bad = Command::new("false").status().unwrap().exit_ok().unwrap_err();
1562+
/// assert_eq!(bad.code(), Some(1));
1563+
/// # } // #[cfg(unix)]
1564+
/// ```
1565+
pub fn code(&self) -> Option<i32> {
1566+
self.code_nonzero().map(Into::into)
1567+
}
1568+
1569+
/// Reports the exit code, if applicable, from an `ExitStatusError`, as a `NonZero`
1570+
///
1571+
/// This is exaclty like [`code()`](Self::code), except that it returns a `NonZeroI32`.
1572+
///
1573+
/// Plain `code`, returning a plain integer, is provided because is is often more convenient.
1574+
/// The returned value from `code()` is indeed also nonzero; use `code_nonzero()` when you want
1575+
/// a type-level guarantee of nonzeroness.
1576+
///
1577+
/// # Examples
1578+
///
1579+
/// ```
1580+
/// #![feature(exit_status_error)]
1581+
/// # if cfg!(unix) {
1582+
/// use std::convert::TryFrom;
1583+
/// use std::num::NonZeroI32;
1584+
/// use std::process::Command;
1585+
///
1586+
/// let bad = Command::new("false").status().unwrap().exit_ok().unwrap_err();
1587+
/// assert_eq!(bad.code_nonzero().unwrap(), NonZeroI32::try_from(1).unwrap());
1588+
/// # } // cfg!(unix)
1589+
/// ```
1590+
pub fn code_nonzero(&self) -> Option<NonZeroI32> {
1591+
self.0.code()
1592+
}
1593+
1594+
/// Converts an `ExitStatusError` (back) to an `ExitStatus`.
1595+
pub fn into_status(&self) -> ExitStatus {
1596+
ExitStatus(self.0.into())
1597+
}
1598+
}
1599+
1600+
#[unstable(feature = "exit_status_error", issue = "84908")]
1601+
impl Into<ExitStatus> for ExitStatusError {
1602+
fn into(self) -> ExitStatus {
1603+
ExitStatus(self.0.into())
1604+
}
1605+
}
1606+
1607+
#[unstable(feature = "exit_status_error", issue = "84908")]
1608+
impl fmt::Display for ExitStatusError {
1609+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1610+
write!(f, "process exited unsuccessfully: {}", self.into_status())
1611+
}
1612+
}
1613+
1614+
#[unstable(feature = "exit_status_error", issue = "84908")]
1615+
impl crate::error::Error for ExitStatusError {}
1616+
14791617
/// This type represents the status code a process can return to its
14801618
/// parent under normal termination.
14811619
///

library/std/src/sys/unix/process/mod.rs

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
pub use self::process_common::{Command, CommandArgs, ExitCode, Stdio, StdioPipes};
2-
pub use self::process_inner::{ExitStatus, Process};
2+
pub use self::process_inner::{ExitStatus, ExitStatusError, Process};
33
pub use crate::ffi::OsString as EnvKey;
44
pub use crate::sys_common::process::CommandEnvs;
55

library/std/src/sys/unix/process/process_fuchsia.rs

+23-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
use crate::convert::TryInto;
1+
use crate::convert::{TryFrom, TryInto};
22
use crate::fmt;
33
use crate::io;
44
use crate::mem;
5+
use crate::num::{NonZeroI32, NonZeroI64};
56
use crate::ptr;
67

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

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

243247
pub fn code(&self) -> Option<i32> {
@@ -306,3 +310,19 @@ impl fmt::Display for ExitStatus {
306310
write!(f, "exit code: {}", self.0)
307311
}
308312
}
313+
314+
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
315+
pub struct ExitStatusError(NonZeroI64);
316+
317+
impl Into<ExitStatus> for ExitStatusError {
318+
fn into(self) -> ExitStatus {
319+
ExitStatus(self.0.into())
320+
}
321+
}
322+
323+
impl ExitStatusError {
324+
pub fn code(self) -> Option<NonZeroI32> {
325+
// fixme: affected by the same bug as ExitStatus::code()
326+
ExitStatus(self.0.into()).code().map(|st| st.try_into().unwrap())
327+
}
328+
}

library/std/src/sys/unix/process/process_unix.rs

+28-3
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
1-
use crate::convert::TryInto;
1+
use crate::convert::{TryFrom, TryInto};
22
use crate::fmt;
33
use crate::io::{self, Error, ErrorKind};
44
use crate::mem;
5+
use crate::num::NonZeroI32;
6+
use crate::os::raw::NonZero_c_int;
57
use crate::ptr;
68
use crate::sys;
79
use crate::sys::cvt;
@@ -491,8 +493,16 @@ impl ExitStatus {
491493
libc::WIFEXITED(self.0)
492494
}
493495

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

498508
pub fn code(&self) -> Option<i32> {
@@ -547,6 +557,21 @@ impl fmt::Display for ExitStatus {
547557
}
548558
}
549559

560+
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
561+
pub struct ExitStatusError(NonZero_c_int);
562+
563+
impl Into<ExitStatus> for ExitStatusError {
564+
fn into(self) -> ExitStatus {
565+
ExitStatus(self.0.into())
566+
}
567+
}
568+
569+
impl ExitStatusError {
570+
pub fn code(self) -> Option<NonZeroI32> {
571+
ExitStatus(self.0.into()).code().map(|st| st.try_into().unwrap())
572+
}
573+
}
574+
550575
#[cfg(test)]
551576
#[path = "process_unix/tests.rs"]
552577
mod tests;

library/std/src/sys/unsupported/process.rs

+17-1
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ use crate::ffi::OsStr;
22
use crate::fmt;
33
use crate::io;
44
use crate::marker::PhantomData;
5+
use crate::num::NonZeroI32;
56
use crate::path::Path;
67
use crate::sys::fs::File;
78
use crate::sys::pipe::AnonPipe;
@@ -97,7 +98,7 @@ impl fmt::Debug for Command {
9798
pub struct ExitStatus(!);
9899

99100
impl ExitStatus {
100-
pub fn success(&self) -> bool {
101+
pub fn exit_ok(&self) -> Result<(), ExitStatusError> {
101102
self.0
102103
}
103104

@@ -134,6 +135,21 @@ impl fmt::Display for ExitStatus {
134135
}
135136
}
136137

138+
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
139+
pub struct ExitStatusError(ExitStatus);
140+
141+
impl Into<ExitStatus> for ExitStatusError {
142+
fn into(self) -> ExitStatus {
143+
self.0.0
144+
}
145+
}
146+
147+
impl ExitStatusError {
148+
pub fn code(self) -> Option<NonZeroI32> {
149+
self.0.0
150+
}
151+
}
152+
137153
#[derive(PartialEq, Eq, Clone, Copy, Debug)]
138154
pub struct ExitCode(bool);
139155

0 commit comments

Comments
 (0)