mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 22:37:45 +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:
@ -1,7 +1,7 @@
|
||||
use nu_engine::{command_prelude::*, get_eval_block_with_early_return, redirect_env};
|
||||
use nu_protocol::{
|
||||
engine::Closure,
|
||||
process::{ChildPipe, ChildProcess, ExitStatus},
|
||||
process::{ChildPipe, ChildProcess},
|
||||
ByteStream, ByteStreamSource, OutDest,
|
||||
};
|
||||
use std::{
|
||||
@ -147,13 +147,7 @@ impl Command for Do {
|
||||
None
|
||||
};
|
||||
|
||||
if child.wait()? != ExitStatus::Exited(0) {
|
||||
return Err(ShellError::ExternalCommand {
|
||||
label: "External command failed".to_string(),
|
||||
help: stderr_msg,
|
||||
span,
|
||||
});
|
||||
}
|
||||
child.wait()?;
|
||||
|
||||
let mut child = ChildProcess::from_raw(None, None, None, span);
|
||||
if let Some(stdout) = stdout {
|
||||
|
@ -93,25 +93,10 @@ impl Command for For {
|
||||
stack.add_var(var_id, x);
|
||||
|
||||
match eval_block(&engine_state, stack, block, PipelineData::empty()) {
|
||||
Err(ShellError::Break { .. }) => {
|
||||
break;
|
||||
}
|
||||
Err(ShellError::Continue { .. }) => {
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
Ok(data) => {
|
||||
if let Some(status) = data.drain()? {
|
||||
let code = status.code();
|
||||
if code != 0 {
|
||||
return Ok(
|
||||
PipelineData::new_external_stream_with_only_exit_code(code),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ShellError::Break { .. }) => break,
|
||||
Err(ShellError::Continue { .. }) => continue,
|
||||
Err(err) => return Err(err),
|
||||
Ok(data) => data.drain()?,
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -121,25 +106,10 @@ impl Command for For {
|
||||
stack.add_var(var_id, x);
|
||||
|
||||
match eval_block(&engine_state, stack, block, PipelineData::empty()) {
|
||||
Err(ShellError::Break { .. }) => {
|
||||
break;
|
||||
}
|
||||
Err(ShellError::Continue { .. }) => {
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
Ok(data) => {
|
||||
if let Some(status) = data.drain()? {
|
||||
let code = status.code();
|
||||
if code != 0 {
|
||||
return Ok(
|
||||
PipelineData::new_external_stream_with_only_exit_code(code),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ShellError::Break { .. }) => break,
|
||||
Err(ShellError::Continue { .. }) => continue,
|
||||
Err(err) => return Err(err),
|
||||
Ok(data) => data.drain()?,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -127,10 +127,9 @@ impl Command for If {
|
||||
eval_block(engine_state, stack, block, input)
|
||||
} else {
|
||||
eval_expression_with_input(engine_state, stack, else_expr, input)
|
||||
.map(|res| res.0)
|
||||
}
|
||||
} else {
|
||||
eval_expression_with_input(engine_state, stack, else_case, input).map(|res| res.0)
|
||||
eval_expression_with_input(engine_state, stack, else_case, input)
|
||||
}
|
||||
} else {
|
||||
Ok(PipelineData::empty())
|
||||
|
@ -56,23 +56,10 @@ impl Command for Loop {
|
||||
engine_state.signals().check(head)?;
|
||||
|
||||
match eval_block(engine_state, stack, block, PipelineData::empty()) {
|
||||
Err(ShellError::Break { .. }) => {
|
||||
break;
|
||||
}
|
||||
Err(ShellError::Continue { .. }) => {
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
Ok(data) => {
|
||||
if let Some(status) = data.drain()? {
|
||||
let code = status.code();
|
||||
if code != 0 {
|
||||
return Ok(PipelineData::new_external_stream_with_only_exit_code(code));
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ShellError::Break { .. }) => break,
|
||||
Err(ShellError::Continue { .. }) => continue,
|
||||
Err(err) => return Err(err),
|
||||
Ok(data) => data.drain()?,
|
||||
}
|
||||
}
|
||||
Ok(PipelineData::empty())
|
||||
|
@ -81,7 +81,7 @@ impl Command for Match {
|
||||
let block = engine_state.get_block(block_id);
|
||||
eval_block(engine_state, stack, block, input)
|
||||
} else {
|
||||
eval_expression_with_input(engine_state, stack, expr, input).map(|x| x.0)
|
||||
eval_expression_with_input(engine_state, stack, expr, input)
|
||||
};
|
||||
}
|
||||
} else {
|
||||
|
@ -47,6 +47,7 @@ impl Command for Try {
|
||||
call: &Call,
|
||||
input: PipelineData,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
let head = call.head;
|
||||
// This is compiled specially by the IR compiler. The code here is never used when
|
||||
// running in IR mode.
|
||||
let call = call.assert_ast_call()?;
|
||||
@ -61,30 +62,15 @@ impl Command for Try {
|
||||
let try_block = engine_state.get_block(try_block);
|
||||
let eval_block = get_eval_block(engine_state);
|
||||
|
||||
match eval_block(engine_state, stack, try_block, input) {
|
||||
Err(error) => {
|
||||
let error = intercept_block_control(error)?;
|
||||
let err_record = err_to_record(error, call.head);
|
||||
handle_catch(err_record, catch_block, engine_state, stack, eval_block)
|
||||
}
|
||||
let result = eval_block(engine_state, stack, try_block, input)
|
||||
.and_then(|pipeline| pipeline.write_to_out_dests(engine_state, stack));
|
||||
|
||||
match result {
|
||||
Err(err) => run_catch(err, head, catch_block, engine_state, stack, eval_block),
|
||||
Ok(PipelineData::Value(Value::Error { error, .. }, ..)) => {
|
||||
let error = intercept_block_control(*error)?;
|
||||
let err_record = err_to_record(error, call.head);
|
||||
handle_catch(err_record, catch_block, engine_state, stack, eval_block)
|
||||
}
|
||||
// external command may fail to run
|
||||
Ok(pipeline) => {
|
||||
let (pipeline, external_failed) = pipeline.check_external_failed()?;
|
||||
if external_failed {
|
||||
let status = pipeline.drain()?;
|
||||
let code = status.map(|status| status.code()).unwrap_or(0);
|
||||
stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(code.into(), call.head));
|
||||
let err_value = Value::nothing(call.head);
|
||||
handle_catch(err_value, catch_block, engine_state, stack, eval_block)
|
||||
} else {
|
||||
Ok(pipeline)
|
||||
}
|
||||
run_catch(*error, head, catch_block, engine_state, stack, eval_block)
|
||||
}
|
||||
Ok(pipeline) => Ok(pipeline),
|
||||
}
|
||||
}
|
||||
|
||||
@ -109,28 +95,33 @@ impl Command for Try {
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_catch(
|
||||
err_value: Value,
|
||||
catch_block: Option<Closure>,
|
||||
fn run_catch(
|
||||
error: ShellError,
|
||||
span: Span,
|
||||
catch: Option<Closure>,
|
||||
engine_state: &EngineState,
|
||||
stack: &mut Stack,
|
||||
eval_block_fn: EvalBlockFn,
|
||||
eval_block: EvalBlockFn,
|
||||
) -> Result<PipelineData, ShellError> {
|
||||
if let Some(catch_block) = catch_block {
|
||||
let catch_block = engine_state.get_block(catch_block.block_id);
|
||||
let error = intercept_block_control(error)?;
|
||||
|
||||
if let Some(catch) = catch {
|
||||
stack.set_last_error(&error);
|
||||
let error = error.into_value(span);
|
||||
let block = engine_state.get_block(catch.block_id);
|
||||
// Put the error value in the positional closure var
|
||||
if let Some(var) = catch_block.signature.get_positional(0) {
|
||||
if let Some(var) = block.signature.get_positional(0) {
|
||||
if let Some(var_id) = &var.var_id {
|
||||
stack.add_var(*var_id, err_value.clone());
|
||||
stack.add_var(*var_id, error.clone());
|
||||
}
|
||||
}
|
||||
|
||||
eval_block_fn(
|
||||
eval_block(
|
||||
engine_state,
|
||||
stack,
|
||||
catch_block,
|
||||
block,
|
||||
// Make the error accessible with $in, too
|
||||
err_value.into_pipeline_data(),
|
||||
error.into_pipeline_data(),
|
||||
)
|
||||
} else {
|
||||
Ok(PipelineData::empty())
|
||||
@ -143,25 +134,13 @@ fn handle_catch(
|
||||
/// `Err` when flow control to bubble up with `?`
|
||||
fn intercept_block_control(error: ShellError) -> Result<ShellError, ShellError> {
|
||||
match error {
|
||||
nu_protocol::ShellError::Break { .. } => Err(error),
|
||||
nu_protocol::ShellError::Continue { .. } => Err(error),
|
||||
nu_protocol::ShellError::Return { .. } => Err(error),
|
||||
ShellError::Break { .. } => Err(error),
|
||||
ShellError::Continue { .. } => Err(error),
|
||||
ShellError::Return { .. } => Err(error),
|
||||
_ => Ok(error),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert from `error` to [`Value::Record`] so the error information can be easily accessed in catch.
|
||||
fn err_to_record(error: ShellError, head: Span) -> Value {
|
||||
Value::record(
|
||||
record! {
|
||||
"msg" => Value::string(error.to_string(), head),
|
||||
"debug" => Value::string(format!("{error:?}"), head),
|
||||
"raw" => Value::error(error, head),
|
||||
},
|
||||
head,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
@ -73,27 +73,10 @@ impl Command for While {
|
||||
let block = engine_state.get_block(block_id);
|
||||
|
||||
match eval_block(engine_state, stack, block, PipelineData::empty()) {
|
||||
Err(ShellError::Break { .. }) => {
|
||||
break;
|
||||
}
|
||||
Err(ShellError::Continue { .. }) => {
|
||||
continue;
|
||||
}
|
||||
Err(err) => {
|
||||
return Err(err);
|
||||
}
|
||||
Ok(data) => {
|
||||
if let Some(status) = data.drain()? {
|
||||
let code = status.code();
|
||||
if code != 0 {
|
||||
return Ok(
|
||||
PipelineData::new_external_stream_with_only_exit_code(
|
||||
code,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ShellError::Break { .. }) => break,
|
||||
Err(ShellError::Continue { .. }) => continue,
|
||||
Err(err) => return Err(err),
|
||||
Ok(data) => data.drain()?,
|
||||
}
|
||||
} else {
|
||||
break;
|
||||
|
Reference in New Issue
Block a user