Handling errors instead of killing the REPL (#11953)

Handle all errors that happen within the REPL loop, display warning or
error messages, and return defaults where necessary.

This addresses @IanManske [Comment Item
1](https://github.com/nushell/nushell/pull/11860#issuecomment-1959947240)
in #11860

---------

Co-authored-by: Jack Wright <jack.wright@disqo.com>
This commit is contained in:
Jack Wright 2024-02-24 07:26:06 -08:00 committed by GitHub
parent 67a63162b2
commit 995989dad4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

View File

@ -6,7 +6,7 @@ use crate::{
NuHighlighter, NuValidator, NushellPrompt, NuHighlighter, NuValidator, NushellPrompt,
}; };
use crossterm::cursor::SetCursorStyle; use crossterm::cursor::SetCursorStyle;
use log::{trace, warn}; use log::{error, trace, warn};
use miette::{ErrReport, IntoDiagnostic, Result}; use miette::{ErrReport, IntoDiagnostic, Result};
use nu_cmd_base::util::get_guaranteed_cwd; use nu_cmd_base::util::get_guaranteed_cwd;
use nu_cmd_base::{hook::eval_hook, util::get_editor}; use nu_cmd_base::{hook::eval_hook, util::get_editor};
@ -26,6 +26,7 @@ use reedline::{
Reedline, SqliteBackedHistory, Vi, Reedline, SqliteBackedHistory, Vi,
}; };
use std::{ use std::{
collections::HashMap,
env::temp_dir, env::temp_dir,
io::{self, IsTerminal, Write}, io::{self, IsTerminal, Write},
panic::{catch_unwind, AssertUnwindSafe}, panic::{catch_unwind, AssertUnwindSafe},
@ -135,47 +136,32 @@ pub fn evaluate_repl(
let mut nu_prompt_cloned = nu_prompt.clone(); let mut nu_prompt_cloned = nu_prompt.clone();
match catch_unwind(AssertUnwindSafe(move || { match catch_unwind(AssertUnwindSafe(move || {
match loop_iteration( let (continue_loop, line_editor) = loop_iteration(LoopContext {
&mut current_engine_state, engine_state: &mut current_engine_state,
&mut current_stack, stack: &mut current_stack,
line_editor, line_editor,
&mut nu_prompt_cloned, nu_prompt: &mut nu_prompt_cloned,
&temp_file_cloned, temp_file: &temp_file_cloned,
use_color, use_color,
&mut entry_num, entry_num: &mut entry_num,
) { });
// pass the most recent version of the line_editor back // pass the most recent version of the line_editor back
Ok((continue_loop, line_editor)) => ( (
Ok(continue_loop), continue_loop,
current_engine_state, current_engine_state,
current_stack, current_stack,
line_editor, line_editor,
),
Err(e) => {
current_engine_state.recover_from_panic();
(
Err(e),
current_engine_state,
current_stack,
Reedline::create(),
) )
}
}
})) { })) {
Ok((result, es, s, le)) => { Ok((continue_loop, es, s, le)) => {
// setup state for the next iteration of the repl loop // setup state for the next iteration of the repl loop
previous_engine_state = es; previous_engine_state = es;
previous_stack = s; previous_stack = s;
line_editor = le; line_editor = le;
match result { if !continue_loop {
Ok(false) => {
break; break;
} }
Err(e) => {
return Err(e);
}
_ => (),
}
} }
Err(_) => { Err(_) => {
// line_editor is lost in the error case so reconstruct a new one // line_editor is lost in the error case so reconstruct a new one
@ -223,23 +209,34 @@ fn get_line_editor(
Ok(line_editor) Ok(line_editor)
} }
/// struct LoopContext<'a> {
engine_state: &'a mut EngineState,
stack: &'a mut Stack,
line_editor: Reedline,
nu_prompt: &'a mut NushellPrompt,
temp_file: &'a Path,
use_color: bool,
entry_num: &'a mut usize,
}
/// Perform one iteration of the REPL loop /// Perform one iteration of the REPL loop
/// Result is bool: continue loop, current reedline /// Result is bool: continue loop, current reedline
#[inline] #[inline]
fn loop_iteration( fn loop_iteration(ctx: LoopContext) -> (bool, Reedline) {
engine_state: &mut EngineState,
stack: &mut Stack,
line_editor: Reedline,
nu_prompt: &mut NushellPrompt,
temp_file: &Path,
use_color: bool,
entry_num: &mut usize,
) -> Result<(bool, Reedline)> {
use nu_cmd_base::hook; use nu_cmd_base::hook;
use reedline::Signal; use reedline::Signal;
let loop_start_time = std::time::Instant::now(); let loop_start_time = std::time::Instant::now();
let LoopContext {
engine_state,
stack,
line_editor,
nu_prompt,
temp_file,
use_color,
entry_num,
} = ctx;
let cwd = get_guaranteed_cwd(engine_state, stack); let cwd = get_guaranteed_cwd(engine_state, stack);
let mut start_time = std::time::Instant::now(); let mut start_time = std::time::Instant::now();
@ -363,9 +360,11 @@ fn loop_iteration(
line_editor = if let Ok((cmd, args)) = buffer_editor { line_editor = if let Ok((cmd, args)) = buffer_editor {
let mut command = std::process::Command::new(cmd); let mut command = std::process::Command::new(cmd);
command let envs = env_to_strings(engine_state, stack).unwrap_or_else(|e| {
.args(args) warn!("Couldn't convert environment variable values to strings: {e}");
.envs(env_to_strings(engine_state, stack)?); HashMap::default()
});
command.args(args).envs(envs);
line_editor.with_buffer_editor(command, temp_file.to_path_buf()) line_editor.with_buffer_editor(command, temp_file.to_path_buf())
} else { } else {
line_editor line_editor
@ -473,7 +472,7 @@ fn loop_iteration(
); );
if history_supports_meta { if history_supports_meta {
prepare_history_metadata(&s, &hostname, engine_state, &mut line_editor)?; prepare_history_metadata(&s, &hostname, engine_state, &mut line_editor);
} }
// Right before we start running the code the user gave us, fire the `pre_execution` // Right before we start running the code the user gave us, fire the `pre_execution`
@ -497,13 +496,14 @@ fn loop_iteration(
drop(repl); drop(repl);
if shell_integration { if shell_integration {
run_ansi_sequence(PRE_EXECUTE_MARKER)?; run_ansi_sequence(PRE_EXECUTE_MARKER);
} }
// Actual command execution logic starts from here // Actual command execution logic starts from here
let start_time = Instant::now(); let start_time = Instant::now();
match parse_operation(s.clone(), engine_state, stack)? { match parse_operation(s.clone(), engine_state, stack) {
Ok(operation) => match operation {
ReplOperation::AutoCd { cwd, target, span } => { ReplOperation::AutoCd { cwd, target, span } => {
do_auto_cd(target, cwd, stack, engine_state, span); do_auto_cd(target, cwd, stack, engine_state, span);
} }
@ -515,10 +515,12 @@ fn loop_iteration(
line_editor, line_editor,
shell_integration, shell_integration,
*entry_num, *entry_num,
)?; )
} }
// as the name implies, we do nothing in this case // as the name implies, we do nothing in this case
ReplOperation::DoNothing => {} ReplOperation::DoNothing => {}
},
Err(ref e) => error!("Error parsing operation: {e}"),
} }
let cmd_duration = start_time.elapsed(); let cmd_duration = start_time.elapsed();
@ -529,17 +531,19 @@ fn loop_iteration(
); );
if history_supports_meta { if history_supports_meta {
fill_in_result_related_history_metadata( if let Err(e) = fill_in_result_related_history_metadata(
&s, &s,
engine_state, engine_state,
cmd_duration, cmd_duration,
stack, stack,
&mut line_editor, &mut line_editor,
)?; ) {
warn!("Could not fill in result related history metadata: {e}");
}
} }
if shell_integration { if shell_integration {
do_shell_integration_finalize_command(hostname, engine_state, stack)?; do_shell_integration_finalize_command(hostname, engine_state, stack);
} }
flush_engine_state_repl_buffer(engine_state, &mut line_editor); flush_engine_state_repl_buffer(engine_state, &mut line_editor);
@ -547,16 +551,16 @@ fn loop_iteration(
Ok(Signal::CtrlC) => { Ok(Signal::CtrlC) => {
// `Reedline` clears the line content. New prompt is shown // `Reedline` clears the line content. New prompt is shown
if shell_integration { if shell_integration {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?; run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
} }
} }
Ok(Signal::CtrlD) => { Ok(Signal::CtrlD) => {
// When exiting clear to a new line // When exiting clear to a new line
if shell_integration { if shell_integration {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?; run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
} }
println!(); println!();
return Ok((false, line_editor)); return (false, line_editor);
} }
Err(err) => { Err(err) => {
let message = err.to_string(); let message = err.to_string();
@ -568,7 +572,7 @@ fn loop_iteration(
// Alternatively only allow that expected failures let the REPL loop // Alternatively only allow that expected failures let the REPL loop
} }
if shell_integration { if shell_integration {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?; run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
} }
} }
} }
@ -590,7 +594,7 @@ fn loop_iteration(
use_color, use_color,
); );
Ok((true, line_editor)) (true, line_editor)
} }
/// ///
@ -601,9 +605,9 @@ fn prepare_history_metadata(
hostname: &Option<String>, hostname: &Option<String>,
engine_state: &EngineState, engine_state: &EngineState,
line_editor: &mut Reedline, line_editor: &mut Reedline,
) -> Result<()> { ) {
if !s.is_empty() && line_editor.has_last_command_context() { if !s.is_empty() && line_editor.has_last_command_context() {
line_editor let result = line_editor
.update_last_command_context(&|mut c| { .update_last_command_context(&|mut c| {
c.start_timestamp = Some(chrono::Utc::now()); c.start_timestamp = Some(chrono::Utc::now());
c.hostname = hostname.clone(); c.hostname = hostname.clone();
@ -611,9 +615,11 @@ fn prepare_history_metadata(
c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd()); c.cwd = Some(StateWorkingSet::new(engine_state).get_cwd());
c c
}) })
.into_diagnostic()?; // todo: don't stop repl if error here? .into_diagnostic();
if let Err(e) = result {
warn!("Could not prepare history metadata: {e}");
}
} }
Ok(())
} }
/// ///
@ -766,7 +772,7 @@ fn do_run_cmd(
line_editor: Reedline, line_editor: Reedline,
shell_integration: bool, shell_integration: bool,
entry_num: usize, entry_num: usize,
) -> Result<Reedline> { ) -> Reedline {
trace!("eval source: {}", s); trace!("eval source: {}", s);
let mut cmds = s.split_whitespace(); let mut cmds = s.split_whitespace();
@ -792,8 +798,8 @@ fn do_run_cmd(
if shell_integration { if shell_integration {
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
let path = cwd.coerce_into_string()?; match cwd.coerce_into_string() {
Ok(path) => {
// Try to abbreviate string for windows title // Try to abbreviate string for windows title
let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() { let maybe_abbrev_path = if let Some(p) = nu_path::home_dir() {
path.replace(&p.as_path().display().to_string(), "~") path.replace(&p.as_path().display().to_string(), "~")
@ -803,7 +809,14 @@ fn do_run_cmd(
let binary_name = s.split_whitespace().next(); let binary_name = s.split_whitespace().next();
if let Some(binary_name) = binary_name { if let Some(binary_name) = binary_name {
run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}> {binary_name}\x07"))?; run_ansi_sequence(&format!(
"\x1b]2;{maybe_abbrev_path}> {binary_name}\x07"
));
}
}
Err(e) => {
warn!("Could not coerce working directory to string {e}");
}
} }
} }
} }
@ -817,7 +830,7 @@ fn do_run_cmd(
false, false,
); );
Ok(line_editor) line_editor
} }
/// ///
@ -828,17 +841,19 @@ fn do_shell_integration_finalize_command(
hostname: Option<String>, hostname: Option<String>,
engine_state: &EngineState, engine_state: &EngineState,
stack: &mut Stack, stack: &mut Stack,
) -> Result<()> { ) {
run_ansi_sequence(&get_command_finished_marker(stack, engine_state))?; run_ansi_sequence(&get_command_finished_marker(stack, engine_state));
if let Some(cwd) = stack.get_env_var(engine_state, "PWD") { if let Some(cwd) = stack.get_env_var(engine_state, "PWD") {
let path = cwd.coerce_into_string()?; match cwd.coerce_into_string() {
Ok(path) => {
// Supported escape sequences of Microsoft's Visual Studio Code (vscode) // Supported escape sequences of Microsoft's Visual Studio Code (vscode)
// https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences // https://code.visualstudio.com/docs/terminal/shell-integration#_supported-escape-sequences
if stack.get_env_var(engine_state, "TERM_PROGRAM") == Some(Value::test_string("vscode")) { if stack.get_env_var(engine_state, "TERM_PROGRAM")
== Some(Value::test_string("vscode"))
{
// If we're in vscode, run their specific ansi escape sequence. // If we're in vscode, run their specific ansi escape sequence.
// This is helpful for ctrl+g to change directories in the terminal. // This is helpful for ctrl+g to change directories in the terminal.
run_ansi_sequence(&format!("\x1b]633;P;Cwd={}\x1b\\", path))?; run_ansi_sequence(&format!("\x1b]633;P;Cwd={}\x1b\\", path));
} else { } else {
// Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir) // Otherwise, communicate the path as OSC 7 (often used for spawning new tabs in the same dir)
run_ansi_sequence(&format!( run_ansi_sequence(&format!(
@ -849,7 +864,7 @@ fn do_shell_integration_finalize_command(
), ),
if path.starts_with('/') { "" } else { "/" }, if path.starts_with('/') { "" } else { "/" },
percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS) percent_encoding::utf8_percent_encode(&path, percent_encoding::CONTROLS)
))?; ));
} }
// Try to abbreviate string for windows title // Try to abbreviate string for windows title
@ -864,10 +879,14 @@ fn do_shell_integration_finalize_command(
// ESC]0;stringBEL -- Set icon name and window title to string // ESC]0;stringBEL -- Set icon name and window title to string
// ESC]1;stringBEL -- Set icon name to string // ESC]1;stringBEL -- Set icon name to string
// ESC]2;stringBEL -- Set window title to string // ESC]2;stringBEL -- Set window title to string
run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}\x07"))?; run_ansi_sequence(&format!("\x1b]2;{maybe_abbrev_path}\x07"));
} }
run_ansi_sequence(RESET_APPLICATION_MODE)?; Err(e) => {
Ok(()) warn!("Could not coerce working directory to string {e}");
}
}
}
run_ansi_sequence(RESET_APPLICATION_MODE);
} }
/// ///
@ -1021,23 +1040,12 @@ fn get_command_finished_marker(stack: &Stack, engine_state: &EngineState) -> Str
format!("\x1b]133;D;{}\x1b\\", exit_code.unwrap_or(0)) format!("\x1b]133;D;{}\x1b\\", exit_code.unwrap_or(0))
} }
fn run_ansi_sequence(seq: &str) -> Result<(), ShellError> { fn run_ansi_sequence(seq: &str) {
io::stdout() if let Err(e) = io::stdout().write_all(seq.as_bytes()) {
.write_all(seq.as_bytes()) warn!("Error writing ansi sequence {e}");
.map_err(|e| ShellError::GenericError { } else if let Err(e) = io::stdout().flush() {
error: "Error writing ansi sequence".into(), warn!("Error flushing stdio {e}");
msg: e.to_string(), }
span: Some(Span::unknown()),
help: None,
inner: vec![],
})?;
io::stdout().flush().map_err(|e| ShellError::GenericError {
error: "Error flushing stdio".into(),
msg: e.to_string(),
span: Some(Span::unknown()),
help: None,
inner: vec![],
})
} }
// Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo' // Absolute paths with a drive letter, like 'C:', 'D:\', 'E:\foo'