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

@@ -3,7 +3,7 @@ use nu_parser::{escape_for_script_arg, escape_quote_string, parse};
use nu_protocol::{
ast::{Expr, Expression},
engine::StateWorkingSet,
report_error,
report_parse_error,
};
use nu_utils::stdout_write_all_and_flush;
@@ -68,7 +68,7 @@ pub(crate) fn parse_commandline_args(
let output = parse(&mut working_set, None, commandline_args.as_bytes(), false);
if let Some(err) = working_set.parse_errors.first() {
report_error(&working_set, err);
report_parse_error(&working_set, err);
std::process::exit(1);
}

View File

@@ -5,7 +5,7 @@ use nu_cli::{eval_config_contents, eval_source};
use nu_path::canonicalize_with;
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
report_error, report_error_new, Config, ParseError, PipelineData, Spanned,
report_parse_error, report_shell_error, Config, ParseError, PipelineData, Spanned,
};
use nu_utils::{get_default_config, get_default_env};
use std::{
@@ -34,19 +34,17 @@ pub(crate) fn read_config_file(
);
// Load config startup file
if let Some(file) = config_file {
let working_set = StateWorkingSet::new(engine_state);
match engine_state.cwd_as_string(Some(stack)) {
Ok(cwd) => {
if let Ok(path) = canonicalize_with(&file.item, cwd) {
eval_config_contents(path, engine_state, stack);
} else {
let e = ParseError::FileNotFound(file.item, file.span);
report_error(&working_set, &e);
report_parse_error(&StateWorkingSet::new(engine_state), &e);
}
}
Err(e) => {
report_error(&working_set, &e);
report_shell_error(engine_state, &e);
}
}
} else if let Some(mut config_path) = nu_path::config_dir() {
@@ -168,11 +166,11 @@ pub(crate) fn read_default_env_file(engine_state: &mut EngineState, stack: &mut
match engine_state.cwd(Some(stack)) {
Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) {
report_error_new(engine_state, &e);
report_shell_error(engine_state, &e);
}
}
Err(e) => {
report_error_new(engine_state, &e);
report_shell_error(engine_state, &e);
}
}
}
@@ -254,11 +252,11 @@ fn eval_default_config(
match engine_state.cwd(Some(stack)) {
Ok(cwd) => {
if let Err(e) = engine_state.merge_env(stack, cwd) {
report_error_new(engine_state, &e);
report_shell_error(engine_state, &e);
}
}
Err(e) => {
report_error_new(engine_state, &e);
report_shell_error(engine_state, &e);
}
}
}

View File

@@ -3,7 +3,7 @@ use nu_cli::NuCompleter;
use nu_parser::{flatten_block, parse, FlatShape};
use nu_protocol::{
engine::{EngineState, Stack, StateWorkingSet},
report_error_new, DeclId, ShellError, Span, Value, VarId,
report_shell_error, DeclId, ShellError, Span, Value, VarId,
};
use reedline::Completer;
use serde_json::{json, Value as JsonValue};
@@ -55,7 +55,7 @@ fn read_in_file<'a>(
let file = std::fs::read(file_path)
.into_diagnostic()
.unwrap_or_else(|e| {
report_error_new(
report_shell_error(
engine_state,
&ShellError::FileNotFoundCustom {
msg: format!("Could not read file '{}': {:?}", file_path, e.to_string()),

View File

@@ -25,7 +25,7 @@ use nu_cmd_base::util::get_init_cwd;
use nu_lsp::LanguageServer;
use nu_path::canonicalize_with;
use nu_protocol::{
engine::EngineState, report_error_new, ByteStream, PipelineData, ShellError, Span, Spanned,
engine::EngineState, report_shell_error, ByteStream, PipelineData, ShellError, Span, Spanned,
Value,
};
use nu_std::load_standard_library;
@@ -67,7 +67,7 @@ fn main() -> Result<()> {
};
if let Err(err) = engine_state.merge_delta(delta) {
report_error_new(&engine_state, &err);
report_shell_error(&engine_state, &err);
}
// TODO: make this conditional in the future
@@ -92,7 +92,7 @@ fn main() -> Result<()> {
.unwrap_or(PathBuf::from(&xdg_config_home))
.join("nushell")
{
report_error_new(
report_shell_error(
&engine_state,
&ShellError::InvalidXdgConfig {
xdg: xdg_config_home,
@@ -164,7 +164,7 @@ fn main() -> Result<()> {
let (args_to_nushell, script_name, args_to_script) = gather_commandline_args();
let parsed_nu_cli_args = parse_commandline_args(&args_to_nushell.join(" "), &mut engine_state)
.unwrap_or_else(|err| {
report_error_new(&engine_state, &err);
report_shell_error(&engine_state, &err);
std::process::exit(1)
});

View File

@@ -10,7 +10,7 @@ use nu_cli::read_plugin_file;
use nu_cli::{evaluate_commands, evaluate_file, evaluate_repl, EvaluateCommandsOpts};
use nu_protocol::{
engine::{EngineState, Stack},
report_error_new, PipelineData, Spanned,
report_shell_error, PipelineData, Spanned,
};
use nu_utils::perf;
@@ -85,7 +85,7 @@ pub(crate) fn run_commands(
engine_state.generate_nu_constant();
let start_time = std::time::Instant::now();
if let Err(err) = evaluate_commands(
let result = evaluate_commands(
commands,
engine_state,
&mut stack,
@@ -95,11 +95,13 @@ pub(crate) fn run_commands(
error_style: parsed_nu_cli_args.error_style,
no_newline: parsed_nu_cli_args.no_newline.is_some(),
},
) {
report_error_new(engine_state, &err);
std::process::exit(1);
}
);
perf!("evaluate_commands", start_time, use_color);
if let Err(err) = result {
report_shell_error(engine_state, &err);
std::process::exit(err.exit_code());
}
}
pub(crate) fn run_file(
@@ -158,29 +160,19 @@ pub(crate) fn run_file(
engine_state.generate_nu_constant();
let start_time = std::time::Instant::now();
if let Err(err) = evaluate_file(
let result = evaluate_file(
script_name,
&args_to_script,
engine_state,
&mut stack,
input,
) {
report_error_new(engine_state, &err);
std::process::exit(1);
}
);
perf!("evaluate_file", start_time, use_color);
let start_time = std::time::Instant::now();
let last_exit_code = stack.get_env_var(&*engine_state, "LAST_EXIT_CODE");
if let Some(last_exit_code) = last_exit_code {
let value = last_exit_code.as_int();
if let Ok(value) = value {
if value != 0 {
std::process::exit(value as i32);
}
}
if let Err(err) = result {
report_shell_error(engine_state, &err);
std::process::exit(err.exit_code());
}
perf!("get exit code", start_time, use_color);
}
pub(crate) fn run_repl(