mirror of
https://github.com/nushell/nushell.git
synced 2025-07-21 16:07:24 +02:00
# 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).
107 lines
3.2 KiB
Rust
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))
|
|
}
|
|
}
|