mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 14:36:08 +02:00
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).
This commit is contained in:
29
crates/nu-protocol/src/config/display_errors.rs
Normal file
29
crates/nu-protocol/src/config/display_errors.rs
Normal file
@ -0,0 +1,29 @@
|
||||
use super::prelude::*;
|
||||
use crate as nu_protocol;
|
||||
use crate::ShellError;
|
||||
|
||||
#[derive(Clone, Copy, Debug, IntoValue, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct DisplayErrors {
|
||||
pub exit_code: bool,
|
||||
pub termination_signal: bool,
|
||||
}
|
||||
|
||||
impl DisplayErrors {
|
||||
pub fn should_show(&self, error: &ShellError) -> bool {
|
||||
match error {
|
||||
ShellError::NonZeroExitCode { .. } => self.exit_code,
|
||||
#[cfg(unix)]
|
||||
ShellError::TerminatedBySignal { .. } => self.termination_signal,
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for DisplayErrors {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
exit_code: true,
|
||||
termination_signal: true,
|
||||
}
|
||||
}
|
||||
}
|
@ -12,6 +12,7 @@ pub use self::completer::{
|
||||
CompleterConfig, CompletionAlgorithm, CompletionSort, ExternalCompleterConfig,
|
||||
};
|
||||
pub use self::datetime_format::DatetimeFormatConfig;
|
||||
pub use self::display_errors::DisplayErrors;
|
||||
pub use self::filesize::FilesizeConfig;
|
||||
pub use self::helper::extract_value;
|
||||
pub use self::history::{HistoryConfig, HistoryFileFormat};
|
||||
@ -28,6 +29,7 @@ pub use self::table::{FooterMode, TableConfig, TableIndexMode, TableMode, TrimSt
|
||||
|
||||
mod completer;
|
||||
mod datetime_format;
|
||||
mod display_errors;
|
||||
mod filesize;
|
||||
mod helper;
|
||||
mod history;
|
||||
@ -67,6 +69,7 @@ pub struct Config {
|
||||
pub cursor_shape: CursorShapeConfig,
|
||||
pub datetime_format: DatetimeFormatConfig,
|
||||
pub error_style: ErrorStyle,
|
||||
pub display_errors: DisplayErrors,
|
||||
pub use_kitty_protocol: bool,
|
||||
pub highlight_resolved_externals: bool,
|
||||
/// Configuration for plugins.
|
||||
@ -121,6 +124,7 @@ impl Default for Config {
|
||||
keybindings: Vec::new(),
|
||||
|
||||
error_style: ErrorStyle::Fancy,
|
||||
display_errors: DisplayErrors::default(),
|
||||
|
||||
use_kitty_protocol: false,
|
||||
highlight_resolved_externals: false,
|
||||
@ -609,6 +613,29 @@ impl Value {
|
||||
"show_banner" => {
|
||||
process_bool_config(value, &mut errors, &mut config.show_banner);
|
||||
}
|
||||
"display_errors" => {
|
||||
if let Value::Record { val, .. } = value {
|
||||
val.to_mut().retain_mut(|key2, value| {
|
||||
let span = value.span();
|
||||
match key2 {
|
||||
"exit_code" => {
|
||||
process_bool_config(value, &mut errors, &mut config.display_errors.exit_code);
|
||||
}
|
||||
"termination_signal" => {
|
||||
process_bool_config(value, &mut errors, &mut config.display_errors.termination_signal);
|
||||
}
|
||||
_ => {
|
||||
report_invalid_key(&[key, key2], span, &mut errors);
|
||||
return false;
|
||||
}
|
||||
};
|
||||
true
|
||||
});
|
||||
} else {
|
||||
report_invalid_value("should be a record", span, &mut errors);
|
||||
*value = config.display_errors.into_value(span);
|
||||
}
|
||||
}
|
||||
"render_right_prompt_on_last_line" => {
|
||||
process_bool_config(value, &mut errors, &mut config.render_right_prompt_on_last_line);
|
||||
}
|
||||
|
@ -279,6 +279,16 @@ impl Stack {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_last_exit_code(&mut self, code: i32, span: Span) {
|
||||
self.add_env_var("LAST_EXIT_CODE".into(), Value::int(code.into(), span));
|
||||
}
|
||||
|
||||
pub fn set_last_error(&mut self, error: &ShellError) {
|
||||
if let Some(code) = error.external_exit_code() {
|
||||
self.set_last_exit_code(code.item, code.span);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn last_overlay_name(&self) -> Result<String, ShellError> {
|
||||
self.active_overlays
|
||||
.last()
|
||||
|
@ -3,7 +3,7 @@
|
||||
//! Relies on the `miette` crate for pretty layout
|
||||
use crate::{
|
||||
engine::{EngineState, StateWorkingSet},
|
||||
ErrorStyle,
|
||||
CompileError, ErrorStyle, ParseError, ParseWarning, ShellError,
|
||||
};
|
||||
use miette::{
|
||||
LabeledSpan, MietteHandlerOpts, NarratableReportHandler, ReportHandler, RgbColors, Severity,
|
||||
@ -20,14 +20,35 @@ pub struct CliError<'src>(
|
||||
pub &'src StateWorkingSet<'src>,
|
||||
);
|
||||
|
||||
pub fn format_error(
|
||||
working_set: &StateWorkingSet,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) -> String {
|
||||
pub fn format_shell_error(working_set: &StateWorkingSet, error: &ShellError) -> String {
|
||||
format!("Error: {:?}", CliError(error, working_set))
|
||||
}
|
||||
|
||||
pub fn report_error(
|
||||
pub fn report_shell_error(engine_state: &EngineState, error: &ShellError) {
|
||||
if engine_state.config.display_errors.should_show(error) {
|
||||
report_error(&StateWorkingSet::new(engine_state), error)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_shell_warning(engine_state: &EngineState, error: &ShellError) {
|
||||
if engine_state.config.display_errors.should_show(error) {
|
||||
report_warning(&StateWorkingSet::new(engine_state), error)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_parse_error(working_set: &StateWorkingSet, error: &ParseError) {
|
||||
report_error(working_set, error);
|
||||
}
|
||||
|
||||
pub fn report_parse_warning(working_set: &StateWorkingSet, error: &ParseWarning) {
|
||||
report_warning(working_set, error);
|
||||
}
|
||||
|
||||
pub fn report_compile_error(working_set: &StateWorkingSet, error: &CompileError) {
|
||||
report_error(working_set, error);
|
||||
}
|
||||
|
||||
fn report_error(
|
||||
working_set: &StateWorkingSet,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
@ -39,15 +60,7 @@ pub fn report_error(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_error_new(
|
||||
engine_state: &EngineState,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, error);
|
||||
}
|
||||
|
||||
pub fn report_warning(
|
||||
fn report_warning(
|
||||
working_set: &StateWorkingSet,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
@ -59,14 +72,6 @@ pub fn report_warning(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn report_warning_new(
|
||||
engine_state: &EngineState,
|
||||
error: &(dyn miette::Diagnostic + Send + Sync + 'static),
|
||||
) {
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
report_error(&working_set, error);
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for CliError<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let config = self.1.get_config();
|
||||
|
@ -6,7 +6,8 @@ mod parse_warning;
|
||||
mod shell_error;
|
||||
|
||||
pub use cli_error::{
|
||||
format_error, report_error, report_error_new, report_warning, report_warning_new,
|
||||
format_shell_error, report_parse_error, report_parse_warning, report_shell_error,
|
||||
report_shell_warning,
|
||||
};
|
||||
pub use compile_error::CompileError;
|
||||
pub use labeled_error::{ErrorLabel, LabeledError};
|
||||
|
@ -1,11 +1,11 @@
|
||||
use miette::Diagnostic;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::io;
|
||||
use std::{io, num::NonZeroI32};
|
||||
use thiserror::Error;
|
||||
|
||||
use crate::{
|
||||
ast::Operator, engine::StateWorkingSet, format_error, LabeledError, ParseError, Span, Spanned,
|
||||
Value,
|
||||
ast::Operator, engine::StateWorkingSet, format_shell_error, record, LabeledError, ParseError,
|
||||
Span, Spanned, Value,
|
||||
};
|
||||
|
||||
/// The fundamental error type for the evaluation engine. These cases represent different kinds of errors
|
||||
@ -639,6 +639,49 @@ pub enum ShellError {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// An external command exited with a non-zero exit code.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// Check the external command's error message.
|
||||
#[error("External command had a non-zero exit code")]
|
||||
#[diagnostic(code(nu::shell::non_zero_exit_code))]
|
||||
NonZeroExitCode {
|
||||
exit_code: NonZeroI32,
|
||||
#[label("exited with code {exit_code}")]
|
||||
span: Span,
|
||||
},
|
||||
|
||||
#[cfg(unix)]
|
||||
/// An external command exited due to a signal.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// Check why the signal was sent or triggered.
|
||||
#[error("External command was terminated by a signal")]
|
||||
#[diagnostic(code(nu::shell::terminated_by_signal))]
|
||||
TerminatedBySignal {
|
||||
signal_name: String,
|
||||
signal: i32,
|
||||
#[label("terminated by {signal_name} ({signal})")]
|
||||
span: Span,
|
||||
},
|
||||
|
||||
#[cfg(unix)]
|
||||
/// An external command core dumped.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// Check why the core dumped was triggered.
|
||||
#[error("External command core dumped")]
|
||||
#[diagnostic(code(nu::shell::core_dumped))]
|
||||
CoreDumped {
|
||||
signal_name: String,
|
||||
signal: i32,
|
||||
#[label("core dumped with {signal_name} ({signal})")]
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// An unsupported body input was used for the respective application body type in 'http' command
|
||||
///
|
||||
/// ## Resolution
|
||||
@ -885,21 +928,6 @@ pub enum ShellError {
|
||||
span: Span,
|
||||
},
|
||||
|
||||
#[cfg(unix)]
|
||||
/// An I/O operation failed.
|
||||
///
|
||||
/// ## Resolution
|
||||
///
|
||||
/// This is a generic error. Refer to the specific error message for further details.
|
||||
#[error("program coredump error")]
|
||||
#[diagnostic(code(nu::shell::coredump_error))]
|
||||
CoredumpErrorSpanned {
|
||||
msg: String,
|
||||
signal: i32,
|
||||
#[label("{msg}")]
|
||||
span: Span,
|
||||
},
|
||||
|
||||
/// Tried to `cd` to a path that isn't a directory.
|
||||
///
|
||||
/// ## Resolution
|
||||
@ -1285,6 +1313,16 @@ This is an internal Nushell error, please file an issue https://github.com/nushe
|
||||
span: Span,
|
||||
},
|
||||
|
||||
#[error("Deprecated: {old_command}")]
|
||||
#[diagnostic(help("for more info see {url}"))]
|
||||
Deprecated {
|
||||
old_command: String,
|
||||
new_suggestion: String,
|
||||
#[label("`{old_command}` is deprecated and will be removed in a future release. Please {new_suggestion} instead.")]
|
||||
span: Span,
|
||||
url: String,
|
||||
},
|
||||
|
||||
/// Invalid glob pattern
|
||||
///
|
||||
/// ## Resolution
|
||||
@ -1404,10 +1442,41 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
|
||||
},
|
||||
}
|
||||
|
||||
// TODO: Implement as From trait
|
||||
impl ShellError {
|
||||
pub fn external_exit_code(&self) -> Option<Spanned<i32>> {
|
||||
let (item, span) = match *self {
|
||||
Self::NonZeroExitCode { exit_code, span } => (exit_code.into(), span),
|
||||
#[cfg(unix)]
|
||||
Self::TerminatedBySignal { signal, span, .. }
|
||||
| Self::CoreDumped { signal, span, .. } => (-signal, span),
|
||||
_ => return None,
|
||||
};
|
||||
Some(Spanned { item, span })
|
||||
}
|
||||
|
||||
pub fn exit_code(&self) -> i32 {
|
||||
self.external_exit_code().map(|e| e.item).unwrap_or(1)
|
||||
}
|
||||
|
||||
pub fn into_value(self, span: Span) -> Value {
|
||||
let exit_code = self.external_exit_code();
|
||||
|
||||
let mut record = record! {
|
||||
"msg" => Value::string(self.to_string(), span),
|
||||
"debug" => Value::string(format!("{self:?}"), span),
|
||||
"raw" => Value::error(self, span),
|
||||
};
|
||||
|
||||
if let Some(code) = exit_code {
|
||||
record.push("exit_code", Value::int(code.item.into(), code.span));
|
||||
}
|
||||
|
||||
Value::record(record, span)
|
||||
}
|
||||
|
||||
// TODO: Implement as From trait
|
||||
pub fn wrap(self, working_set: &StateWorkingSet, span: Span) -> ParseError {
|
||||
let msg = format_error(working_set, &self);
|
||||
let msg = format_shell_error(working_set, &self);
|
||||
ParseError::LabeledError(
|
||||
msg,
|
||||
"Encountered error during parse-time evaluation".into(),
|
||||
|
@ -255,9 +255,6 @@ impl<'a> fmt::Display for FmtInstruction<'a> {
|
||||
Instruction::PopErrorHandler => {
|
||||
write!(f, "{:WIDTH$}", "pop-error-handler")
|
||||
}
|
||||
Instruction::CheckExternalFailed { dst, src } => {
|
||||
write!(f, "{:WIDTH$} {dst}, {src}", "check-external-failed")
|
||||
}
|
||||
Instruction::ReturnEarly { src } => {
|
||||
write!(f, "{:WIDTH$} {src}", "return-early")
|
||||
}
|
||||
|
@ -252,9 +252,6 @@ pub enum Instruction {
|
||||
/// Pop an error handler. This is not necessary when control flow is directed to the error
|
||||
/// handler due to an error.
|
||||
PopErrorHandler,
|
||||
/// Check if an external command failed. Boolean value into `dst`. `src` is preserved, but it
|
||||
/// does require waiting for the command to exit.
|
||||
CheckExternalFailed { dst: RegId, src: RegId },
|
||||
/// Return early from the block, raising a `ShellError::Return` instead.
|
||||
///
|
||||
/// Collecting the value is unavoidable.
|
||||
@ -330,7 +327,6 @@ impl Instruction {
|
||||
Instruction::OnError { .. } => None,
|
||||
Instruction::OnErrorInto { .. } => None,
|
||||
Instruction::PopErrorHandler => None,
|
||||
Instruction::CheckExternalFailed { dst, .. } => Some(dst),
|
||||
Instruction::ReturnEarly { .. } => None,
|
||||
Instruction::Return { .. } => None,
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
//! Module managing the streaming of raw bytes between pipeline elements
|
||||
use crate::{
|
||||
process::{ChildPipe, ChildProcess, ExitStatus},
|
||||
process::{ChildPipe, ChildProcess},
|
||||
ErrSpan, IntoSpanned, OutDest, PipelineData, ShellError, Signals, Span, Type, Value,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@ -548,25 +548,19 @@ impl ByteStream {
|
||||
}
|
||||
|
||||
/// Consume and drop all bytes of the [`ByteStream`].
|
||||
///
|
||||
/// If the source of the [`ByteStream`] is [`ByteStreamSource::Child`],
|
||||
/// then the [`ExitStatus`] of the [`ChildProcess`] is returned.
|
||||
pub fn drain(self) -> Result<Option<ExitStatus>, ShellError> {
|
||||
pub fn drain(self) -> Result<(), ShellError> {
|
||||
match self.stream {
|
||||
ByteStreamSource::Read(read) => {
|
||||
copy_with_signals(read, io::sink(), self.span, &self.signals)?;
|
||||
Ok(None)
|
||||
Ok(())
|
||||
}
|
||||
ByteStreamSource::File(_) => Ok(None),
|
||||
ByteStreamSource::Child(child) => Ok(Some(child.wait()?)),
|
||||
ByteStreamSource::File(_) => Ok(()),
|
||||
ByteStreamSource::Child(child) => child.wait(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Print all bytes of the [`ByteStream`] to stdout or stderr.
|
||||
///
|
||||
/// If the source of the [`ByteStream`] is [`ByteStreamSource::Child`],
|
||||
/// then the [`ExitStatus`] of the [`ChildProcess`] is returned.
|
||||
pub fn print(self, to_stderr: bool) -> Result<Option<ExitStatus>, ShellError> {
|
||||
pub fn print(self, to_stderr: bool) -> Result<(), ShellError> {
|
||||
if to_stderr {
|
||||
self.write_to(&mut io::stderr())
|
||||
} else {
|
||||
@ -575,20 +569,15 @@ impl ByteStream {
|
||||
}
|
||||
|
||||
/// Write all bytes of the [`ByteStream`] to `dest`.
|
||||
///
|
||||
/// If the source of the [`ByteStream`] is [`ByteStreamSource::Child`],
|
||||
/// then the [`ExitStatus`] of the [`ChildProcess`] is returned.
|
||||
pub fn write_to(self, dest: impl Write) -> Result<Option<ExitStatus>, ShellError> {
|
||||
pub fn write_to(self, dest: impl Write) -> Result<(), ShellError> {
|
||||
let span = self.span;
|
||||
let signals = &self.signals;
|
||||
match self.stream {
|
||||
ByteStreamSource::Read(read) => {
|
||||
copy_with_signals(read, dest, span, signals)?;
|
||||
Ok(None)
|
||||
}
|
||||
ByteStreamSource::File(file) => {
|
||||
copy_with_signals(file, dest, span, signals)?;
|
||||
Ok(None)
|
||||
}
|
||||
ByteStreamSource::Child(mut child) => {
|
||||
// All `OutDest`s except `OutDest::Capture` will cause `stderr` to be `None`.
|
||||
@ -606,36 +595,33 @@ impl ByteStream {
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(Some(child.wait()?))
|
||||
child.wait()?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) fn write_to_out_dests(
|
||||
self,
|
||||
stdout: &OutDest,
|
||||
stderr: &OutDest,
|
||||
) -> Result<Option<ExitStatus>, ShellError> {
|
||||
) -> Result<(), ShellError> {
|
||||
let span = self.span;
|
||||
let signals = &self.signals;
|
||||
|
||||
match self.stream {
|
||||
ByteStreamSource::Read(read) => {
|
||||
write_to_out_dest(read, stdout, true, span, signals)?;
|
||||
Ok(None)
|
||||
}
|
||||
ByteStreamSource::File(file) => {
|
||||
match stdout {
|
||||
OutDest::Pipe | OutDest::Capture | OutDest::Null => {}
|
||||
OutDest::Inherit => {
|
||||
copy_with_signals(file, io::stdout(), span, signals)?;
|
||||
}
|
||||
OutDest::File(f) => {
|
||||
copy_with_signals(file, f.as_ref(), span, signals)?;
|
||||
}
|
||||
ByteStreamSource::File(file) => match stdout {
|
||||
OutDest::Pipe | OutDest::Capture | OutDest::Null => {}
|
||||
OutDest::Inherit => {
|
||||
copy_with_signals(file, io::stdout(), span, signals)?;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
OutDest::File(f) => {
|
||||
copy_with_signals(file, f.as_ref(), span, signals)?;
|
||||
}
|
||||
},
|
||||
ByteStreamSource::Child(mut child) => {
|
||||
match (child.stdout.take(), child.stderr.take()) {
|
||||
(Some(out), Some(err)) => {
|
||||
@ -682,9 +668,11 @@ impl ByteStream {
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
Ok(Some(child.wait()?))
|
||||
child.wait()?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,12 +1,11 @@
|
||||
use crate::{
|
||||
ast::{Call, PathMember},
|
||||
engine::{EngineState, Stack},
|
||||
process::{ChildPipe, ChildProcess, ExitStatus},
|
||||
ByteStream, ByteStreamType, Config, ErrSpan, ListStream, OutDest, PipelineMetadata, Range,
|
||||
ShellError, Signals, Span, Type, Value,
|
||||
ByteStream, ByteStreamType, Config, ListStream, OutDest, PipelineMetadata, Range, ShellError,
|
||||
Signals, Span, Type, Value,
|
||||
};
|
||||
use nu_utils::{stderr_write_all_and_flush, stdout_write_all_and_flush};
|
||||
use std::io::{Cursor, Read, Write};
|
||||
use std::io::Write;
|
||||
|
||||
const LINE_ENDING_PATTERN: &[char] = &['\r', '\n'];
|
||||
|
||||
@ -52,16 +51,6 @@ impl PipelineData {
|
||||
PipelineData::Empty
|
||||
}
|
||||
|
||||
/// create a `PipelineData::ByteStream` with proper exit_code
|
||||
///
|
||||
/// It's useful to break running without raising error at user level.
|
||||
pub fn new_external_stream_with_only_exit_code(exit_code: i32) -> PipelineData {
|
||||
let span = Span::unknown();
|
||||
let mut child = ChildProcess::from_raw(None, None, None, span);
|
||||
child.set_exit_code(exit_code);
|
||||
PipelineData::ByteStream(ByteStream::child(child, span), None)
|
||||
}
|
||||
|
||||
pub fn metadata(&self) -> Option<PipelineMetadata> {
|
||||
match self {
|
||||
PipelineData::Empty => None,
|
||||
@ -182,22 +171,17 @@ impl PipelineData {
|
||||
/// without consuming input and without writing anything.
|
||||
///
|
||||
/// For the other [`OutDest`]s, the given `PipelineData` will be completely consumed
|
||||
/// and `PipelineData::Empty` will be returned, unless the data is from an external stream,
|
||||
/// in which case an external stream containing only that exit code will be returned.
|
||||
/// and `PipelineData::Empty` will be returned (assuming no errors).
|
||||
pub fn write_to_out_dests(
|
||||
self,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
match (self, stack.stdout()) {
|
||||
(PipelineData::ByteStream(stream, ..), stdout) => {
|
||||
if let Some(exit_status) = stream.write_to_out_dests(stdout, stack.stderr())? {
|
||||
return Ok(PipelineData::new_external_stream_with_only_exit_code(
|
||||
exit_status.code(),
|
||||
));
|
||||
}
|
||||
}
|
||||
(data, OutDest::Pipe | OutDest::Capture) => return Ok(data),
|
||||
(PipelineData::ByteStream(stream, ..), stdout) => {
|
||||
stream.write_to_out_dests(stdout, stack.stderr())?;
|
||||
}
|
||||
(PipelineData::Empty, ..) => {}
|
||||
(PipelineData::Value(..), OutDest::Null) => {}
|
||||
(PipelineData::ListStream(stream, ..), OutDest::Null) => {
|
||||
@ -227,15 +211,12 @@ impl PipelineData {
|
||||
Ok(PipelineData::Empty)
|
||||
}
|
||||
|
||||
pub fn drain(self) -> Result<Option<ExitStatus>, ShellError> {
|
||||
pub fn drain(self) -> Result<(), ShellError> {
|
||||
match self {
|
||||
PipelineData::Empty => Ok(None),
|
||||
PipelineData::Empty => Ok(()),
|
||||
PipelineData::Value(Value::Error { error, .. }, ..) => Err(*error),
|
||||
PipelineData::Value(..) => Ok(None),
|
||||
PipelineData::ListStream(stream, ..) => {
|
||||
stream.drain()?;
|
||||
Ok(None)
|
||||
}
|
||||
PipelineData::Value(..) => Ok(()),
|
||||
PipelineData::ListStream(stream, ..) => stream.drain(),
|
||||
PipelineData::ByteStream(stream, ..) => stream.drain(),
|
||||
}
|
||||
}
|
||||
@ -496,68 +477,6 @@ impl PipelineData {
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to catch the external command exit status and detect if it failed.
|
||||
///
|
||||
/// This is useful for external commands with semicolon, we can detect errors early to avoid
|
||||
/// commands after the semicolon running.
|
||||
///
|
||||
/// Returns `self` and a flag that indicates if the external command run failed. If `self` is
|
||||
/// not [`PipelineData::ByteStream`], the flag will be `false`.
|
||||
///
|
||||
/// Currently this will consume an external command to completion.
|
||||
pub fn check_external_failed(self) -> Result<(Self, bool), ShellError> {
|
||||
if let PipelineData::ByteStream(stream, metadata) = self {
|
||||
// Preserve stream attributes
|
||||
let span = stream.span();
|
||||
let type_ = stream.type_();
|
||||
let known_size = stream.known_size();
|
||||
match stream.into_child() {
|
||||
Ok(mut child) => {
|
||||
// Only check children without stdout. This means that nothing
|
||||
// later in the pipeline can possibly consume output from this external command.
|
||||
if child.stdout.is_none() {
|
||||
// Note:
|
||||
// In run-external's implementation detail, the result sender thread
|
||||
// send out stderr message first, then stdout message, then exit_code.
|
||||
//
|
||||
// In this clause, we already make sure that `stdout` is None
|
||||
// But not the case of `stderr`, so if `stderr` is not None
|
||||
// We need to consume stderr message before reading external commands' exit code.
|
||||
//
|
||||
// Or we'll never have a chance to read exit_code if stderr producer produce too much stderr message.
|
||||
// So we consume stderr stream and rebuild it.
|
||||
let stderr = child
|
||||
.stderr
|
||||
.take()
|
||||
.map(|mut stderr| {
|
||||
let mut buf = Vec::new();
|
||||
stderr.read_to_end(&mut buf).err_span(span)?;
|
||||
Ok::<_, ShellError>(buf)
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
let code = child.wait()?.code();
|
||||
let mut child = ChildProcess::from_raw(None, None, None, span);
|
||||
if let Some(stderr) = stderr {
|
||||
child.stderr = Some(ChildPipe::Tee(Box::new(Cursor::new(stderr))));
|
||||
}
|
||||
child.set_exit_code(code);
|
||||
let stream = ByteStream::child(child, span).with_type(type_);
|
||||
Ok((PipelineData::ByteStream(stream, metadata), code != 0))
|
||||
} else {
|
||||
let stream = ByteStream::child(child, span)
|
||||
.with_type(type_)
|
||||
.with_known_size(known_size);
|
||||
Ok((PipelineData::ByteStream(stream, metadata), false))
|
||||
}
|
||||
}
|
||||
Err(stream) => Ok((PipelineData::ByteStream(stream, metadata), false)),
|
||||
}
|
||||
} else {
|
||||
Ok((self, false))
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to convert Value from Value::Range to Value::List.
|
||||
/// This is useful to expand Value::Range into array notation, specifically when
|
||||
/// converting `to json` or `to nuon`.
|
||||
@ -613,7 +532,7 @@ impl PipelineData {
|
||||
stack: &mut Stack,
|
||||
no_newline: bool,
|
||||
to_stderr: bool,
|
||||
) -> Result<Option<ExitStatus>, ShellError> {
|
||||
) -> Result<(), ShellError> {
|
||||
match self {
|
||||
// Print byte streams directly as long as they aren't binary.
|
||||
PipelineData::ByteStream(stream, ..) if stream.type_() != ByteStreamType::Binary => {
|
||||
@ -650,14 +569,14 @@ impl PipelineData {
|
||||
engine_state: &EngineState,
|
||||
no_newline: bool,
|
||||
to_stderr: bool,
|
||||
) -> Result<Option<ExitStatus>, ShellError> {
|
||||
) -> Result<(), ShellError> {
|
||||
if let PipelineData::Value(Value::Binary { val: bytes, .. }, _) = self {
|
||||
if to_stderr {
|
||||
stderr_write_all_and_flush(bytes)?;
|
||||
} else {
|
||||
stdout_write_all_and_flush(bytes)?;
|
||||
}
|
||||
Ok(None)
|
||||
Ok(())
|
||||
} else {
|
||||
self.write_all_and_flush(engine_state, no_newline, to_stderr)
|
||||
}
|
||||
@ -668,7 +587,7 @@ impl PipelineData {
|
||||
engine_state: &EngineState,
|
||||
no_newline: bool,
|
||||
to_stderr: bool,
|
||||
) -> Result<Option<ExitStatus>, ShellError> {
|
||||
) -> Result<(), ShellError> {
|
||||
if let PipelineData::ByteStream(stream, ..) = self {
|
||||
// Copy ByteStreams directly
|
||||
stream.print(to_stderr)
|
||||
@ -692,7 +611,7 @@ impl PipelineData {
|
||||
}
|
||||
}
|
||||
|
||||
Ok(None)
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -23,21 +23,16 @@ impl ExitStatusFuture {
|
||||
ExitStatusFuture::Finished(Err(err)) => Err(err.as_ref().clone()),
|
||||
ExitStatusFuture::Running(receiver) => {
|
||||
let code = match receiver.recv() {
|
||||
Ok(Ok(status)) => {
|
||||
#[cfg(unix)]
|
||||
if let ExitStatus::Signaled {
|
||||
signal,
|
||||
core_dumped: true,
|
||||
} = status
|
||||
{
|
||||
return Err(ShellError::CoredumpErrorSpanned {
|
||||
msg: format!("coredump detected. received signal: {signal}"),
|
||||
signal,
|
||||
span,
|
||||
});
|
||||
}
|
||||
#[cfg(unix)]
|
||||
Ok(Ok(
|
||||
status @ ExitStatus::Signaled {
|
||||
core_dumped: true, ..
|
||||
},
|
||||
)) => {
|
||||
status.check_ok(span)?;
|
||||
Ok(status)
|
||||
}
|
||||
Ok(Ok(status)) => Ok(status),
|
||||
Ok(Err(err)) => Err(ShellError::IOErrorSpanned {
|
||||
msg: format!("failed to get exit code: {err:?}"),
|
||||
span,
|
||||
@ -187,13 +182,12 @@ impl ChildProcess {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
// TODO: check exit_status
|
||||
self.exit_status.wait(self.span)?;
|
||||
self.exit_status.wait(self.span)?.check_ok(self.span)?;
|
||||
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
pub fn wait(mut self) -> Result<ExitStatus, ShellError> {
|
||||
pub fn wait(mut self) -> Result<(), ShellError> {
|
||||
if let Some(stdout) = self.stdout.take() {
|
||||
let stderr = self
|
||||
.stderr
|
||||
@ -229,7 +223,7 @@ impl ChildProcess {
|
||||
consume_pipe(stderr).err_span(self.span)?;
|
||||
}
|
||||
|
||||
self.exit_status.wait(self.span)
|
||||
self.exit_status.wait(self.span)?.check_ok(self.span)
|
||||
}
|
||||
|
||||
pub fn try_wait(&mut self) -> Result<Option<ExitStatus>, ShellError> {
|
||||
|
@ -1,3 +1,4 @@
|
||||
use crate::{ShellError, Span};
|
||||
use std::process;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
@ -18,6 +19,47 @@ impl ExitStatus {
|
||||
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)]
|
||||
|
Reference in New Issue
Block a user