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:
Ian Manske
2024-09-06 23:44:26 -07:00
committed by GitHub
parent 6c1c7f9509
commit 3d008e2c4e
54 changed files with 566 additions and 650 deletions

View File

@ -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);