mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 00:44:57 +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:
@ -278,7 +278,6 @@ impl BlockBuilder {
|
||||
Instruction::OnError { index: _ } => Ok(()),
|
||||
Instruction::OnErrorInto { index: _, dst } => allocate(&[], &[*dst]),
|
||||
Instruction::PopErrorHandler => Ok(()),
|
||||
Instruction::CheckExternalFailed { dst, src } => allocate(&[*src], &[*dst, *src]),
|
||||
Instruction::ReturnEarly { src } => allocate(&[*src], &[]),
|
||||
Instruction::Return { src } => allocate(&[*src], &[]),
|
||||
};
|
||||
@ -499,6 +498,7 @@ impl BlockBuilder {
|
||||
}
|
||||
|
||||
/// Mark an unreachable code path. Produces an error at runtime if executed.
|
||||
#[allow(dead_code)] // currently unused, but might be used in the future.
|
||||
pub(crate) fn unreachable(&mut self, span: Span) -> Result<(), CompileError> {
|
||||
self.push(Instruction::Unreachable.into_spanned(span))
|
||||
}
|
||||
|
@ -362,12 +362,8 @@ pub(crate) fn compile_try(
|
||||
//
|
||||
// on-error-into ERR, %io_reg // or without
|
||||
// %io_reg <- <...block...> <- %io_reg
|
||||
// check-external-failed %failed_reg, %io_reg
|
||||
// branch-if %failed_reg, FAIL
|
||||
// pop-error-handler
|
||||
// jump END
|
||||
// FAIL: drain %io_reg
|
||||
// unreachable
|
||||
// ERR: clone %err_reg, %io_reg
|
||||
// store-variable $err_var, %err_reg // or without
|
||||
// %io_reg <- <...catch block...> <- %io_reg // set to empty if no catch block
|
||||
@ -378,12 +374,8 @@ pub(crate) fn compile_try(
|
||||
// %closure_reg <- <catch_expr>
|
||||
// on-error-into ERR, %io_reg
|
||||
// %io_reg <- <...block...> <- %io_reg
|
||||
// check-external-failed %failed_reg, %io_reg
|
||||
// branch-if %failed_reg, FAIL
|
||||
// pop-error-handler
|
||||
// jump END
|
||||
// FAIL: drain %io_reg
|
||||
// unreachable
|
||||
// ERR: clone %err_reg, %io_reg
|
||||
// push-positional %closure_reg
|
||||
// push-positional %err_reg
|
||||
@ -405,7 +397,6 @@ pub(crate) fn compile_try(
|
||||
let catch_span = catch_expr.map(|e| e.span).unwrap_or(call.head);
|
||||
|
||||
let err_label = builder.label(None);
|
||||
let failed_label = builder.label(None);
|
||||
let end_label = builder.label(None);
|
||||
|
||||
// We have two ways of executing `catch`: if it was provided as a literal, we can inline it.
|
||||
@ -470,32 +461,13 @@ pub(crate) fn compile_try(
|
||||
io_reg,
|
||||
)?;
|
||||
|
||||
// Check for external command exit code failure, and also redirect that to the catch handler
|
||||
let failed_reg = builder.next_register()?;
|
||||
builder.push(
|
||||
Instruction::CheckExternalFailed {
|
||||
dst: failed_reg,
|
||||
src: io_reg,
|
||||
}
|
||||
.into_spanned(catch_span),
|
||||
)?;
|
||||
builder.branch_if(failed_reg, failed_label, catch_span)?;
|
||||
|
||||
// Successful case: pop the error handler
|
||||
builder.push(Instruction::PopErrorHandler.into_spanned(call.head))?;
|
||||
|
||||
// Jump over the failure case
|
||||
builder.jump(end_label, catch_span)?;
|
||||
|
||||
// Set up an error handler preamble for failed external.
|
||||
// Draining the %io_reg results in the error handler being called with Empty, and sets
|
||||
// $env.LAST_EXIT_CODE
|
||||
builder.set_label(failed_label, builder.here())?;
|
||||
builder.drain(io_reg, catch_span)?;
|
||||
builder.add_comment("branches to err");
|
||||
builder.unreachable(catch_span)?;
|
||||
|
||||
// This is the real error handler
|
||||
// This is the error handler
|
||||
builder.set_label(err_label, builder.here())?;
|
||||
|
||||
// Mark out register as likely not clean - state in error handler is not well defined
|
||||
|
@ -250,7 +250,7 @@ pub fn eval_expression_with_input<D: DebugContext>(
|
||||
stack: &mut Stack,
|
||||
expr: &Expression,
|
||||
mut input: PipelineData,
|
||||
) -> Result<(PipelineData, bool), ShellError> {
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
match &expr.expr {
|
||||
Expr::Call(call) => {
|
||||
input = eval_call::<D>(engine_state, stack, call, input)?;
|
||||
@ -298,16 +298,7 @@ pub fn eval_expression_with_input<D: DebugContext>(
|
||||
}
|
||||
};
|
||||
|
||||
// If input an external command,
|
||||
// then `might_consume_external_result` will consume `stderr` if `stdout` is `None`.
|
||||
// This should not happen if the user wants to capture stderr.
|
||||
if !matches!(stack.stdout(), OutDest::Pipe | OutDest::Capture)
|
||||
&& matches!(stack.stderr(), OutDest::Capture)
|
||||
{
|
||||
Ok((input, false))
|
||||
} else {
|
||||
input.check_external_failed()
|
||||
}
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
fn eval_redirection<D: DebugContext>(
|
||||
@ -401,9 +392,8 @@ fn eval_element_with_input_inner<D: DebugContext>(
|
||||
stack: &mut Stack,
|
||||
element: &PipelineElement,
|
||||
input: PipelineData,
|
||||
) -> Result<(PipelineData, bool), ShellError> {
|
||||
let (data, failed) =
|
||||
eval_expression_with_input::<D>(engine_state, stack, &element.expr, input)?;
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let data = eval_expression_with_input::<D>(engine_state, stack, &element.expr, input)?;
|
||||
|
||||
if let Some(redirection) = element.redirection.as_ref() {
|
||||
let is_external = if let PipelineData::ByteStream(stream, ..) = &data {
|
||||
@ -473,7 +463,7 @@ fn eval_element_with_input_inner<D: DebugContext>(
|
||||
PipelineData::Empty => PipelineData::Empty,
|
||||
};
|
||||
|
||||
Ok((data, failed))
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
fn eval_element_with_input<D: DebugContext>(
|
||||
@ -481,20 +471,11 @@ fn eval_element_with_input<D: DebugContext>(
|
||||
stack: &mut Stack,
|
||||
element: &PipelineElement,
|
||||
input: PipelineData,
|
||||
) -> Result<(PipelineData, bool), ShellError> {
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
D::enter_element(engine_state, element);
|
||||
match eval_element_with_input_inner::<D>(engine_state, stack, element, input) {
|
||||
Ok((data, failed)) => {
|
||||
let res = Ok(data);
|
||||
D::leave_element(engine_state, element, &res);
|
||||
res.map(|data| (data, failed))
|
||||
}
|
||||
Err(err) => {
|
||||
let res = Err(err);
|
||||
D::leave_element(engine_state, element, &res);
|
||||
res.map(|data| (data, false))
|
||||
}
|
||||
}
|
||||
let result = eval_element_with_input_inner::<D>(engine_state, stack, element, input);
|
||||
D::leave_element(engine_state, element, &result);
|
||||
result
|
||||
}
|
||||
|
||||
pub fn eval_block_with_early_return<D: DebugContext>(
|
||||
@ -509,7 +490,7 @@ pub fn eval_block_with_early_return<D: DebugContext>(
|
||||
}
|
||||
}
|
||||
|
||||
pub fn eval_block<D: DebugContext>(
|
||||
fn eval_block_inner<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
block: &Block,
|
||||
@ -520,8 +501,6 @@ pub fn eval_block<D: DebugContext>(
|
||||
return eval_ir_block::<D>(engine_state, stack, block, input);
|
||||
}
|
||||
|
||||
D::enter_block(engine_state, block);
|
||||
|
||||
let num_pipelines = block.len();
|
||||
|
||||
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
|
||||
@ -542,14 +521,7 @@ pub fn eval_block<D: DebugContext>(
|
||||
(next_out.or(Some(OutDest::Pipe)), next_err),
|
||||
)?;
|
||||
let stack = &mut stack.push_redirection(stdout, stderr);
|
||||
let (output, failed) =
|
||||
eval_element_with_input::<D>(engine_state, stack, element, input)?;
|
||||
if failed {
|
||||
// External command failed.
|
||||
// Don't return `Err(ShellError)`, so nushell won't show an extra error message.
|
||||
return Ok(output);
|
||||
}
|
||||
input = output;
|
||||
input = eval_element_with_input::<D>(engine_state, stack, element, input)?;
|
||||
}
|
||||
|
||||
if last_pipeline {
|
||||
@ -560,13 +532,7 @@ pub fn eval_block<D: DebugContext>(
|
||||
(stack.pipe_stdout().cloned(), stack.pipe_stderr().cloned()),
|
||||
)?;
|
||||
let stack = &mut stack.push_redirection(stdout, stderr);
|
||||
let (output, failed) = eval_element_with_input::<D>(engine_state, stack, last, input)?;
|
||||
if failed {
|
||||
// External command failed.
|
||||
// Don't return `Err(ShellError)`, so nushell won't show an extra error message.
|
||||
return Ok(output);
|
||||
}
|
||||
input = output;
|
||||
input = eval_element_with_input::<D>(engine_state, stack, last, input)?;
|
||||
} else {
|
||||
let (stdout, stderr) = eval_element_redirection::<D>(
|
||||
engine_state,
|
||||
@ -575,40 +541,41 @@ pub fn eval_block<D: DebugContext>(
|
||||
(None, None),
|
||||
)?;
|
||||
let stack = &mut stack.push_redirection(stdout, stderr);
|
||||
let (output, failed) = eval_element_with_input::<D>(engine_state, stack, last, input)?;
|
||||
if failed {
|
||||
// External command failed.
|
||||
// Don't return `Err(ShellError)`, so nushell won't show an extra error message.
|
||||
return Ok(output);
|
||||
}
|
||||
input = PipelineData::Empty;
|
||||
match output {
|
||||
match eval_element_with_input::<D>(engine_state, stack, last, input)? {
|
||||
PipelineData::ByteStream(stream, ..) => {
|
||||
let span = stream.span();
|
||||
let status = stream.drain()?;
|
||||
if let Some(status) = status {
|
||||
stack.add_env_var(
|
||||
"LAST_EXIT_CODE".into(),
|
||||
Value::int(status.code().into(), span),
|
||||
);
|
||||
if status.code() != 0 {
|
||||
break;
|
||||
}
|
||||
if let Err(err) = stream.drain() {
|
||||
stack.set_last_error(&err);
|
||||
return Err(err);
|
||||
} else {
|
||||
stack.set_last_exit_code(0, span);
|
||||
}
|
||||
}
|
||||
PipelineData::ListStream(stream, ..) => {
|
||||
stream.drain()?;
|
||||
}
|
||||
PipelineData::ListStream(stream, ..) => stream.drain()?,
|
||||
PipelineData::Value(..) | PipelineData::Empty => {}
|
||||
}
|
||||
input = PipelineData::Empty;
|
||||
}
|
||||
}
|
||||
|
||||
D::leave_block(engine_state, block);
|
||||
|
||||
Ok(input)
|
||||
}
|
||||
|
||||
pub fn eval_block<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
block: &Block,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
D::enter_block(engine_state, block);
|
||||
let result = eval_block_inner::<D>(engine_state, stack, block, input);
|
||||
D::leave_block(engine_state, block);
|
||||
if let Err(err) = &result {
|
||||
stack.set_last_error(err);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
pub fn eval_collect<D: DebugContext>(
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
@ -639,8 +606,7 @@ pub fn eval_collect<D: DebugContext>(
|
||||
expr,
|
||||
// We still have to pass it as input
|
||||
input.into_pipeline_data_with_metadata(metadata),
|
||||
)
|
||||
.map(|(result, _failed)| result);
|
||||
);
|
||||
|
||||
stack.remove_var(var_id);
|
||||
|
||||
|
@ -25,12 +25,8 @@ pub type EvalBlockWithEarlyReturnFn =
|
||||
pub type EvalExpressionFn = fn(&EngineState, &mut Stack, &Expression) -> Result<Value, ShellError>;
|
||||
|
||||
/// Type of eval_expression_with_input() function
|
||||
pub type EvalExpressionWithInputFn = fn(
|
||||
&EngineState,
|
||||
&mut Stack,
|
||||
&Expression,
|
||||
PipelineData,
|
||||
) -> Result<(PipelineData, bool), ShellError>;
|
||||
pub type EvalExpressionWithInputFn =
|
||||
fn(&EngineState, &mut Stack, &Expression, PipelineData) -> Result<PipelineData, ShellError>;
|
||||
|
||||
/// Type of eval_subexpression() function
|
||||
pub type EvalSubexpressionFn =
|
||||
|
@ -6,9 +6,9 @@ use nu_protocol::{
|
||||
debugger::DebugContext,
|
||||
engine::{Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack},
|
||||
ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
|
||||
record, ByteStreamSource, DataSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned,
|
||||
ListStream, OutDest, PipelineData, PipelineMetadata, PositionalArg, Range, Record, RegId,
|
||||
ShellError, Signals, Signature, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
ByteStreamSource, DataSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned, ListStream,
|
||||
OutDest, PipelineData, PipelineMetadata, PositionalArg, Range, Record, RegId, ShellError,
|
||||
Signals, Signature, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
|
||||
};
|
||||
use nu_utils::IgnoreCaseExt;
|
||||
|
||||
@ -207,18 +207,6 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
Ok(InstructionResult::Return(reg_id)) => {
|
||||
return Ok(ctx.take_reg(reg_id));
|
||||
}
|
||||
Ok(InstructionResult::ExitCode(exit_code)) => {
|
||||
if let Some(error_handler) = ctx.stack.error_handlers.pop(ctx.error_handler_base) {
|
||||
// If an error handler is set, branch there
|
||||
prepare_error_handler(ctx, error_handler, None);
|
||||
pc = error_handler.handler_index;
|
||||
} else {
|
||||
// If not, exit the block with the exit code
|
||||
return Ok(PipelineData::new_external_stream_with_only_exit_code(
|
||||
exit_code,
|
||||
));
|
||||
}
|
||||
}
|
||||
Err(
|
||||
err @ (ShellError::Return { .. }
|
||||
| ShellError::Continue { .. }
|
||||
@ -259,15 +247,10 @@ fn prepare_error_handler(
|
||||
if let Some(reg_id) = error_handler.error_register {
|
||||
if let Some(error) = error {
|
||||
// Create the error value and put it in the register
|
||||
let value = Value::record(
|
||||
record! {
|
||||
"msg" => Value::string(format!("{}", error.item), error.span),
|
||||
"debug" => Value::string(format!("{:?}", error.item), error.span),
|
||||
"raw" => Value::error(error.item, error.span),
|
||||
},
|
||||
error.span,
|
||||
ctx.put_reg(
|
||||
reg_id,
|
||||
error.item.into_value(error.span).into_pipeline_data(),
|
||||
);
|
||||
ctx.put_reg(reg_id, PipelineData::Value(value, None));
|
||||
} else {
|
||||
// Set the register to empty
|
||||
ctx.put_reg(reg_id, PipelineData::Empty);
|
||||
@ -281,7 +264,6 @@ enum InstructionResult {
|
||||
Continue,
|
||||
Branch(usize),
|
||||
Return(RegId),
|
||||
ExitCode(i32),
|
||||
}
|
||||
|
||||
/// Perform an instruction
|
||||
@ -788,13 +770,6 @@ fn eval_instruction<D: DebugContext>(
|
||||
ctx.stack.error_handlers.pop(ctx.error_handler_base);
|
||||
Ok(Continue)
|
||||
}
|
||||
Instruction::CheckExternalFailed { dst, src } => {
|
||||
let data = ctx.take_reg(*src);
|
||||
let (data, failed) = data.check_external_failed()?;
|
||||
ctx.put_reg(*src, data);
|
||||
ctx.put_reg(*dst, Value::bool(failed, *span).into_pipeline_data());
|
||||
Ok(Continue)
|
||||
}
|
||||
Instruction::ReturnEarly { src } => {
|
||||
let val = ctx.collect_reg(*src, *span)?;
|
||||
Err(ShellError::Return {
|
||||
@ -1362,23 +1337,23 @@ fn collect(data: PipelineData, fallback_span: Span) -> Result<PipelineData, Shel
|
||||
Ok(PipelineData::Value(value, metadata))
|
||||
}
|
||||
|
||||
/// Helper for drain behavior. Returns `Ok(ExitCode)` on failed external.
|
||||
/// Helper for drain behavior.
|
||||
fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result<InstructionResult, ShellError> {
|
||||
use self::InstructionResult::*;
|
||||
let span = data.span().unwrap_or(Span::unknown());
|
||||
if let Some(exit_status) = data.drain()? {
|
||||
ctx.stack.add_env_var(
|
||||
"LAST_EXIT_CODE".into(),
|
||||
Value::int(exit_status.code() as i64, span),
|
||||
);
|
||||
if exit_status.code() == 0 {
|
||||
Ok(Continue)
|
||||
} else {
|
||||
Ok(ExitCode(exit_status.code()))
|
||||
match data {
|
||||
PipelineData::ByteStream(stream, ..) => {
|
||||
let span = stream.span();
|
||||
if let Err(err) = stream.drain() {
|
||||
ctx.stack.set_last_error(&err);
|
||||
return Err(err);
|
||||
} else {
|
||||
ctx.stack.set_last_exit_code(0, span);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(Continue)
|
||||
PipelineData::ListStream(stream, ..) => stream.drain()?,
|
||||
PipelineData::Value(..) | PipelineData::Empty => {}
|
||||
}
|
||||
Ok(Continue)
|
||||
}
|
||||
|
||||
enum RedirectionStream {
|
||||
|
Reference in New Issue
Block a user