mirror of
https://github.com/nushell/nushell.git
synced 2025-08-12 10:20:21 +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:
@ -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);
|
||||
|
||||
|
Reference in New Issue
Block a user