Files
nushell/crates/nu-protocol/src/process/exit_status.rs
Ian Manske 3d008e2c4e Error on non-zero exit statuses (#13515)
# Description
This PR makes it so that non-zero exit codes and termination by signal
are treated as a normal `ShellError`. Currently, these are silent
errors. That is, if an external command fails, then it's code block is
aborted, but the parent block can sometimes continue execution. E.g.,
see #8569 and this example:
```nushell
[1 2] | each { ^false }
```

Before this would give:
```
╭───┬──╮
│ 0 │  │
│ 1 │  │
╰───┴──╯
```

Now, this shows an error:
```
Error: nu:🐚:eval_block_with_input

  × Eval block failed with pipeline input
   ╭─[entry #1:1:2]
 1 │ [1 2] | each { ^false }
   ·  ┬
   ·  ╰── source value
   ╰────

Error: nu:🐚:non_zero_exit_code

  × External command had a non-zero exit code
   ╭─[entry #1:1:17]
 1 │ [1 2] | each { ^false }
   ·                 ──┬──
   ·                   ╰── exited with code 1
   ╰────
```

This PR fixes #12874, fixes #5960, fixes #10856, and fixes #5347. This
PR also partially addresses #10633 and #10624 (only the last command of
a pipeline is currently checked). It looks like #8569 is already fixed,
but this PR will make sure it is definitely fixed (fixes #8569).

# User-Facing Changes
- Non-zero exit codes and termination by signal now cause an error to be
thrown.
- The error record value passed to a `catch` block may now have an
`exit_code` column containing the integer exit code if the error was due
to an external command.
- Adds new config values, `display_errors.exit_code` and
`display_errors.termination_signal`, which determine whether an error
message should be printed in the respective error cases. For
non-interactive sessions, these are set to `true`, and for interactive
sessions `display_errors.exit_code` is false (via the default config).

# Tests
Added a few tests.

# After Submitting
- Update docs and book.
- Future work:
- Error if other external commands besides the last in a pipeline exit
with a non-zero exit code. Then, deprecate `do -c` since this will be
the default behavior everywhere.
- Add a better mechanism for exit codes and deprecate
`$env.LAST_EXIT_CODE` (it's buggy).
2024-09-07 06:44:26 +00:00

107 lines
3.2 KiB
Rust

use crate::{ShellError, Span};
use std::process;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExitStatus {
Exited(i32),
#[cfg(unix)]
Signaled {
signal: i32,
core_dumped: bool,
},
}
impl ExitStatus {
pub fn code(self) -> i32 {
match self {
ExitStatus::Exited(code) => code,
#[cfg(unix)]
ExitStatus::Signaled { signal, .. } => -signal,
}
}
pub fn check_ok(self, span: Span) -> Result<(), ShellError> {
match self {
ExitStatus::Exited(exit_code) => {
if let Ok(exit_code) = exit_code.try_into() {
Err(ShellError::NonZeroExitCode { exit_code, span })
} else {
Ok(())
}
}
#[cfg(unix)]
ExitStatus::Signaled {
signal,
core_dumped,
} => {
use nix::sys::signal::Signal;
let sig = Signal::try_from(signal);
if sig == Ok(Signal::SIGPIPE) {
// Processes often exit with SIGPIPE, but this is not an error condition.
Ok(())
} else {
let signal_name = sig.map(Signal::as_str).unwrap_or("unknown signal").into();
Err(if core_dumped {
ShellError::CoreDumped {
signal_name,
signal,
span,
}
} else {
ShellError::TerminatedBySignal {
signal_name,
signal,
span,
}
})
}
}
}
}
}
#[cfg(unix)]
impl From<process::ExitStatus> for ExitStatus {
fn from(status: process::ExitStatus) -> Self {
use std::os::unix::process::ExitStatusExt;
match (status.code(), status.signal()) {
(Some(code), None) => Self::Exited(code),
(None, Some(signal)) => Self::Signaled {
signal,
core_dumped: status.core_dumped(),
},
(None, None) => {
debug_assert!(false, "ExitStatus should have either a code or a signal");
Self::Exited(-1)
}
(Some(code), Some(signal)) => {
// Should be unreachable, as `code()` will be `None` if `signal()` is `Some`
// according to the docs for `ExitStatus::code`.
debug_assert!(
false,
"ExitStatus cannot have both a code ({code}) and a signal ({signal})"
);
Self::Signaled {
signal,
core_dumped: status.core_dumped(),
}
}
}
}
}
#[cfg(not(unix))]
impl From<process::ExitStatus> for ExitStatus {
fn from(status: process::ExitStatus) -> Self {
let code = status.code();
debug_assert!(
code.is_some(),
"`ExitStatus::code` cannot return `None` on windows"
);
Self::Exited(code.unwrap_or(-1))
}
}