mirror of
https://github.com/nushell/nushell.git
synced 2025-08-09 18:27:46 +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:
@ -2,10 +2,10 @@ use crate::util::eval_source;
|
||||
#[cfg(feature = "plugin")]
|
||||
use nu_path::canonicalize_with;
|
||||
#[cfg(feature = "plugin")]
|
||||
use nu_protocol::{engine::StateWorkingSet, report_error, ParseError, PluginRegistryFile, Spanned};
|
||||
use nu_protocol::{engine::StateWorkingSet, ParseError, PluginRegistryFile, Spanned};
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
report_error_new, HistoryFileFormat, PipelineData,
|
||||
report_shell_error, HistoryFileFormat, PipelineData,
|
||||
};
|
||||
#[cfg(feature = "plugin")]
|
||||
use nu_utils::perf;
|
||||
@ -36,7 +36,7 @@ pub fn read_plugin_file(
|
||||
.and_then(|p| Path::new(&p.item).extension())
|
||||
.is_some_and(|ext| ext == "nu")
|
||||
{
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Wrong plugin file format".into(),
|
||||
@ -81,7 +81,7 @@ pub fn read_plugin_file(
|
||||
return;
|
||||
}
|
||||
} else {
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: format!(
|
||||
@ -113,7 +113,7 @@ pub fn read_plugin_file(
|
||||
Ok(contents) => contents,
|
||||
Err(err) => {
|
||||
log::warn!("Failed to read plugin registry file: {err:?}");
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: format!(
|
||||
@ -146,7 +146,7 @@ pub fn read_plugin_file(
|
||||
nu_plugin_engine::load_plugin_file(&mut working_set, &contents, span);
|
||||
|
||||
if let Err(err) = engine_state.merge_delta(working_set.render()) {
|
||||
report_error_new(engine_state, &err);
|
||||
report_shell_error(engine_state, &err);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -166,7 +166,7 @@ pub fn add_plugin_file(
|
||||
) {
|
||||
use std::path::Path;
|
||||
|
||||
let working_set = StateWorkingSet::new(engine_state);
|
||||
use nu_protocol::report_parse_error;
|
||||
|
||||
if let Ok(cwd) = engine_state.cwd_as_string(None) {
|
||||
if let Some(plugin_file) = plugin_file {
|
||||
@ -181,8 +181,8 @@ pub fn add_plugin_file(
|
||||
engine_state.plugin_path = Some(path)
|
||||
} else {
|
||||
// It's an error if the directory for the plugin file doesn't exist.
|
||||
report_error(
|
||||
&working_set,
|
||||
report_parse_error(
|
||||
&StateWorkingSet::new(engine_state),
|
||||
&ParseError::FileNotFound(
|
||||
path_dir.to_string_lossy().into_owned(),
|
||||
plugin_file.span,
|
||||
@ -214,7 +214,8 @@ pub fn eval_config_contents(
|
||||
let prev_file = engine_state.file.take();
|
||||
engine_state.file = Some(config_path.clone());
|
||||
|
||||
eval_source(
|
||||
// TODO: ignore this error?
|
||||
let _ = eval_source(
|
||||
engine_state,
|
||||
stack,
|
||||
&contents,
|
||||
@ -230,11 +231,11 @@ pub fn eval_config_contents(
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -280,7 +281,7 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
|
||||
let old_contents = match std::fs::read(&old_plugin_file_path) {
|
||||
Ok(old_contents) => old_contents,
|
||||
Err(err) => {
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Can't read old plugin file to migrate".into(),
|
||||
@ -349,7 +350,7 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState, storage_path: &str) -
|
||||
.map_err(|e| e.into())
|
||||
.and_then(|file| contents.write_to(file, None))
|
||||
{
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
&engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Failed to save migrated plugin file".into(),
|
||||
|
@ -2,9 +2,10 @@ use log::info;
|
||||
use nu_engine::{convert_env_values, eval_block};
|
||||
use nu_parser::parse;
|
||||
use nu_protocol::{
|
||||
cli_error::report_compile_error,
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
report_error, PipelineData, ShellError, Spanned, Value,
|
||||
report_parse_error, report_parse_warning, PipelineData, ShellError, Spanned, Value,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -61,16 +62,16 @@ pub fn evaluate_commands(
|
||||
|
||||
let output = parse(&mut working_set, None, commands.item.as_bytes(), false);
|
||||
if let Some(warning) = working_set.parse_warnings.first() {
|
||||
report_error(&working_set, warning);
|
||||
report_parse_warning(&working_set, warning);
|
||||
}
|
||||
|
||||
if let Some(err) = working_set.parse_errors.first() {
|
||||
report_error(&working_set, err);
|
||||
report_parse_error(&working_set, err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Some(err) = working_set.compile_errors.first() {
|
||||
report_error(&working_set, err);
|
||||
report_compile_error(&working_set, err);
|
||||
// Not a fatal error, for now
|
||||
}
|
||||
|
||||
@ -92,11 +93,7 @@ pub fn evaluate_commands(
|
||||
t_mode.coerce_str()?.parse().unwrap_or_default();
|
||||
}
|
||||
|
||||
if let Some(status) = pipeline.print(engine_state, stack, no_newline, false)? {
|
||||
if status.code() != 0 {
|
||||
std::process::exit(status.code())
|
||||
}
|
||||
}
|
||||
pipeline.print(engine_state, stack, no_newline, false)?;
|
||||
|
||||
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
||||
|
||||
|
@ -4,9 +4,10 @@ use nu_engine::{convert_env_values, eval_block};
|
||||
use nu_parser::parse;
|
||||
use nu_path::canonicalize_with;
|
||||
use nu_protocol::{
|
||||
cli_error::report_compile_error,
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
report_error, PipelineData, ShellError, Span, Value,
|
||||
report_parse_error, report_parse_warning, PipelineData, ShellError, Span, Value,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
@ -77,17 +78,17 @@ pub fn evaluate_file(
|
||||
let block = parse(&mut working_set, Some(file_path_str), &file, false);
|
||||
|
||||
if let Some(warning) = working_set.parse_warnings.first() {
|
||||
report_error(&working_set, warning);
|
||||
report_parse_warning(&working_set, warning);
|
||||
}
|
||||
|
||||
// If any parse errors were found, report the first error and exit.
|
||||
if let Some(err) = working_set.parse_errors.first() {
|
||||
report_error(&working_set, err);
|
||||
report_parse_error(&working_set, err);
|
||||
std::process::exit(1);
|
||||
}
|
||||
|
||||
if let Some(err) = working_set.compile_errors.first() {
|
||||
report_error(&working_set, err);
|
||||
report_compile_error(&working_set, err);
|
||||
// Not a fatal error, for now
|
||||
}
|
||||
|
||||
@ -118,11 +119,7 @@ pub fn evaluate_file(
|
||||
};
|
||||
|
||||
// Print the pipeline output of the last command of the file.
|
||||
if let Some(status) = pipeline.print(engine_state, stack, true, false)? {
|
||||
if status.code() != 0 {
|
||||
std::process::exit(status.code())
|
||||
}
|
||||
}
|
||||
pipeline.print(engine_state, stack, true, false)?;
|
||||
|
||||
// Invoke the main command with arguments.
|
||||
// Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
|
||||
@ -140,7 +137,7 @@ pub fn evaluate_file(
|
||||
};
|
||||
|
||||
if exit_code != 0 {
|
||||
std::process::exit(exit_code)
|
||||
std::process::exit(exit_code);
|
||||
}
|
||||
|
||||
info!("evaluate {}:{}:{}", file!(), line!(), column!());
|
||||
|
@ -3,7 +3,7 @@ use log::trace;
|
||||
use nu_engine::ClosureEvalOnce;
|
||||
use nu_protocol::{
|
||||
engine::{EngineState, Stack},
|
||||
report_error_new, Config, PipelineData, Value,
|
||||
report_shell_error, Config, PipelineData, Value,
|
||||
};
|
||||
use reedline::Prompt;
|
||||
|
||||
@ -80,7 +80,7 @@ fn get_prompt_string(
|
||||
|
||||
result
|
||||
.map_err(|err| {
|
||||
report_error_new(engine_state, &err);
|
||||
report_shell_error(engine_state, &err);
|
||||
})
|
||||
.ok()
|
||||
}
|
||||
|
@ -27,7 +27,7 @@ use nu_parser::{lex, parse, trim_quotes_str};
|
||||
use nu_protocol::{
|
||||
config::NuCursorShape,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
report_error_new, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned,
|
||||
report_shell_error, HistoryConfig, HistoryFileFormat, PipelineData, ShellError, Span, Spanned,
|
||||
Value,
|
||||
};
|
||||
use nu_utils::{
|
||||
@ -88,7 +88,7 @@ pub fn evaluate_repl(
|
||||
let start_time = std::time::Instant::now();
|
||||
// Translate environment variables from Strings to Values
|
||||
if let Err(e) = convert_env_values(engine_state, &unique_stack) {
|
||||
report_error_new(engine_state, &e);
|
||||
report_shell_error(engine_state, &e);
|
||||
}
|
||||
perf!("translate env vars", start_time, use_color);
|
||||
|
||||
@ -98,7 +98,7 @@ pub fn evaluate_repl(
|
||||
Value::string("0823", Span::unknown()),
|
||||
);
|
||||
|
||||
unique_stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(0, Span::unknown()));
|
||||
unique_stack.set_last_exit_code(0, Span::unknown());
|
||||
|
||||
let mut line_editor = get_line_editor(engine_state, nushell_path, use_color)?;
|
||||
let temp_file = temp_dir().join(format!("{}.nu", uuid::Uuid::new_v4()));
|
||||
@ -286,7 +286,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
// Before doing anything, merge the environment from the previous REPL iteration into the
|
||||
// permanent state.
|
||||
if let Err(err) = engine_state.merge_env(&mut stack, cwd) {
|
||||
report_error_new(engine_state, &err);
|
||||
report_shell_error(engine_state, &err);
|
||||
}
|
||||
// Check whether $env.NU_USE_IR is set, so that the user can change it in the REPL
|
||||
// Temporary while IR eval is optional
|
||||
@ -302,7 +302,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
// fire the "pre_prompt" hook
|
||||
if let Some(hook) = engine_state.get_config().hooks.pre_prompt.clone() {
|
||||
if let Err(err) = eval_hook(engine_state, &mut stack, None, vec![], &hook, "pre_prompt") {
|
||||
report_error_new(engine_state, &err);
|
||||
report_shell_error(engine_state, &err);
|
||||
}
|
||||
}
|
||||
perf!("pre-prompt hook", start_time, use_color);
|
||||
@ -312,7 +312,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
// fire the "env_change" hook
|
||||
let env_change = engine_state.get_config().hooks.env_change.clone();
|
||||
if let Err(error) = hook::eval_env_change_hook(env_change, engine_state, &mut stack) {
|
||||
report_error_new(engine_state, &error)
|
||||
report_shell_error(engine_state, &error)
|
||||
}
|
||||
perf!("env-change hook", start_time, use_color);
|
||||
|
||||
@ -386,7 +386,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
trace!("adding menus");
|
||||
line_editor =
|
||||
add_menus(line_editor, engine_reference, &stack_arc, config).unwrap_or_else(|e| {
|
||||
report_error_new(engine_state, &e);
|
||||
report_shell_error(engine_state, &e);
|
||||
Reedline::create()
|
||||
});
|
||||
|
||||
@ -506,7 +506,7 @@ fn loop_iteration(ctx: LoopContext) -> (bool, Stack, Reedline) {
|
||||
&hook,
|
||||
"pre_execution",
|
||||
) {
|
||||
report_error_new(engine_state, &err);
|
||||
report_shell_error(engine_state, &err);
|
||||
}
|
||||
}
|
||||
|
||||
@ -808,7 +808,7 @@ fn do_auto_cd(
|
||||
) {
|
||||
let path = {
|
||||
if !path.exists() {
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::DirectoryNotFound {
|
||||
dir: path.to_string_lossy().to_string(),
|
||||
@ -820,7 +820,7 @@ fn do_auto_cd(
|
||||
};
|
||||
|
||||
if let PermissionResult::PermissionDenied(reason) = have_permission(path.clone()) {
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::IOError {
|
||||
msg: format!("Cannot change directory to {path}: {reason}"),
|
||||
@ -834,7 +834,7 @@ fn do_auto_cd(
|
||||
//FIXME: this only changes the current scope, but instead this environment variable
|
||||
//should probably be a block that loads the information from the state in the overlay
|
||||
if let Err(err) = stack.set_cwd(&path) {
|
||||
report_error_new(engine_state, &err);
|
||||
report_shell_error(engine_state, &err);
|
||||
return;
|
||||
};
|
||||
let cwd = Value::string(cwd, span);
|
||||
@ -867,7 +867,7 @@ fn do_auto_cd(
|
||||
"NUSHELL_LAST_SHELL".into(),
|
||||
Value::int(last_shell as i64, span),
|
||||
);
|
||||
stack.add_env_var("LAST_EXIT_CODE".into(), Value::int(0, Span::unknown()));
|
||||
stack.set_last_exit_code(0, Span::unknown());
|
||||
}
|
||||
|
||||
///
|
||||
@ -1141,7 +1141,7 @@ fn setup_keybindings(engine_state: &EngineState, line_editor: Reedline) -> Reedl
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
report_error_new(engine_state, &e);
|
||||
report_shell_error(engine_state, &e);
|
||||
line_editor
|
||||
}
|
||||
};
|
||||
|
@ -2,9 +2,11 @@ use nu_cmd_base::hook::eval_hook;
|
||||
use nu_engine::{eval_block, eval_block_with_early_return};
|
||||
use nu_parser::{escape_quote_string, lex, parse, unescape_unquote_string, Token, TokenContents};
|
||||
use nu_protocol::{
|
||||
cli_error::report_compile_error,
|
||||
debugger::WithoutDebug,
|
||||
engine::{EngineState, Stack, StateWorkingSet},
|
||||
report_error, report_error_new, PipelineData, ShellError, Span, Value,
|
||||
report_parse_error, report_parse_warning, report_shell_error, PipelineData, ShellError, Span,
|
||||
Value,
|
||||
};
|
||||
#[cfg(windows)]
|
||||
use nu_utils::enable_vt_processing;
|
||||
@ -39,7 +41,7 @@ fn gather_env_vars(
|
||||
init_cwd: &Path,
|
||||
) {
|
||||
fn report_capture_error(engine_state: &EngineState, env_str: &str, msg: &str) {
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: format!("Environment variable was not captured: {env_str}"),
|
||||
@ -70,7 +72,7 @@ fn gather_env_vars(
|
||||
}
|
||||
None => {
|
||||
// Could not capture current working directory
|
||||
report_error_new(
|
||||
report_shell_error(
|
||||
engine_state,
|
||||
&ShellError::GenericError {
|
||||
error: "Current directory is not a valid utf-8 path".into(),
|
||||
@ -210,18 +212,19 @@ pub fn eval_source(
|
||||
let start_time = std::time::Instant::now();
|
||||
|
||||
let exit_code = match evaluate_source(engine_state, stack, source, fname, input, allow_return) {
|
||||
Ok(code) => code.unwrap_or(0),
|
||||
Ok(failed) => {
|
||||
let code = failed.into();
|
||||
stack.set_last_exit_code(code, Span::unknown());
|
||||
code
|
||||
}
|
||||
Err(err) => {
|
||||
report_error_new(engine_state, &err);
|
||||
1
|
||||
report_shell_error(engine_state, &err);
|
||||
let code = err.exit_code();
|
||||
stack.set_last_error(&err);
|
||||
code
|
||||
}
|
||||
};
|
||||
|
||||
stack.add_env_var(
|
||||
"LAST_EXIT_CODE".to_string(),
|
||||
Value::int(exit_code.into(), Span::unknown()),
|
||||
);
|
||||
|
||||
// reset vt processing, aka ansi because illbehaved externals can break it
|
||||
#[cfg(windows)]
|
||||
{
|
||||
@ -244,7 +247,7 @@ fn evaluate_source(
|
||||
fname: &str,
|
||||
input: PipelineData,
|
||||
allow_return: bool,
|
||||
) -> Result<Option<i32>, ShellError> {
|
||||
) -> Result<bool, ShellError> {
|
||||
let (block, delta) = {
|
||||
let mut working_set = StateWorkingSet::new(engine_state);
|
||||
let output = parse(
|
||||
@ -254,16 +257,16 @@ fn evaluate_source(
|
||||
false,
|
||||
);
|
||||
if let Some(warning) = working_set.parse_warnings.first() {
|
||||
report_error(&working_set, warning);
|
||||
report_parse_warning(&working_set, warning);
|
||||
}
|
||||
|
||||
if let Some(err) = working_set.parse_errors.first() {
|
||||
report_error(&working_set, err);
|
||||
return Ok(Some(1));
|
||||
report_parse_error(&working_set, err);
|
||||
return Ok(true);
|
||||
}
|
||||
|
||||
if let Some(err) = working_set.compile_errors.first() {
|
||||
report_error(&working_set, err);
|
||||
report_compile_error(&working_set, err);
|
||||
// Not a fatal error, for now
|
||||
}
|
||||
|
||||
@ -278,25 +281,23 @@ fn evaluate_source(
|
||||
eval_block::<WithoutDebug>(engine_state, stack, &block, input)
|
||||
}?;
|
||||
|
||||
let status = if let PipelineData::ByteStream(..) = pipeline {
|
||||
pipeline.print(engine_state, stack, false, false)?
|
||||
if let PipelineData::ByteStream(..) = pipeline {
|
||||
pipeline.print(engine_state, stack, false, false)
|
||||
} else if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
||||
let pipeline = eval_hook(
|
||||
engine_state,
|
||||
stack,
|
||||
Some(pipeline),
|
||||
vec![],
|
||||
&hook,
|
||||
"display_output",
|
||||
)?;
|
||||
pipeline.print(engine_state, stack, false, false)
|
||||
} else {
|
||||
if let Some(hook) = engine_state.get_config().hooks.display_output.clone() {
|
||||
let pipeline = eval_hook(
|
||||
engine_state,
|
||||
stack,
|
||||
Some(pipeline),
|
||||
vec![],
|
||||
&hook,
|
||||
"display_output",
|
||||
)?;
|
||||
pipeline.print(engine_state, stack, false, false)
|
||||
} else {
|
||||
pipeline.print(engine_state, stack, true, false)
|
||||
}?
|
||||
};
|
||||
pipeline.print(engine_state, stack, true, false)
|
||||
}?;
|
||||
|
||||
Ok(status.map(|status| status.code()))
|
||||
Ok(false)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
|
Reference in New Issue
Block a user