diff --git a/crates/nu-cli/src/commands/history/history_.rs b/crates/nu-cli/src/commands/history/history_.rs index 40d951966e..00efd547d4 100644 --- a/crates/nu-cli/src/commands/history/history_.rs +++ b/crates/nu-cli/src/commands/history/history_.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::HistoryFileFormat; +use nu_protocol::{shell_error::io::IoError, HistoryFileFormat}; use reedline::{ FileBackedHistory, History as ReedlineHistory, HistoryItem, SearchDirection, SearchQuery, SqliteBackedHistory, @@ -93,10 +93,11 @@ impl Command for History { ) }) }) - .ok_or(ShellError::FileNotFound { - file: history_path.display().to_string(), - span: head, - })? + .ok_or(IoError::new( + std::io::ErrorKind::NotFound, + head, + history_path, + ))? .into_pipeline_data(head, signals)), HistoryFileFormat::Sqlite => Ok(history_reader .and_then(|h| { @@ -109,10 +110,11 @@ impl Command for History { .enumerate() .map(move |(idx, entry)| create_history_record(idx, entry, long, head)) }) - .ok_or(ShellError::FileNotFound { - file: history_path.display().to_string(), - span: head, - })? + .ok_or(IoError::new( + std::io::ErrorKind::NotFound, + head, + history_path, + ))? .into_pipeline_data(head, signals)), } } diff --git a/crates/nu-cli/src/commands/history/history_import.rs b/crates/nu-cli/src/commands/history/history_import.rs index 7666bd7eb7..b9764306f8 100644 --- a/crates/nu-cli/src/commands/history/history_import.rs +++ b/crates/nu-cli/src/commands/history/history_import.rs @@ -1,7 +1,10 @@ use std::path::{Path, PathBuf}; use nu_engine::command_prelude::*; -use nu_protocol::HistoryFileFormat; +use nu_protocol::{ + shell_error::{self, io::IoError}, + HistoryFileFormat, +}; use reedline::{ FileBackedHistory, History, HistoryItem, ReedlineError, SearchQuery, SqliteBackedHistory, @@ -69,17 +72,16 @@ Note that history item IDs are ignored when importing from file."# call: &Call, input: PipelineData, ) -> Result { + let span = call.head; let ok = Ok(Value::nothing(call.head).into_pipeline_data()); let Some(history) = engine_state.history_config() else { return ok; }; let Some(current_history_path) = history.file_path() else { - return Err(ShellError::ConfigDirNotFound { - span: Some(call.head), - }); + return Err(ShellError::ConfigDirNotFound { span: span.into() }); }; - if let Some(bak_path) = backup(¤t_history_path)? { + if let Some(bak_path) = backup(¤t_history_path, span)? { println!("Backed history to {}", bak_path.display()); } match input { @@ -216,7 +218,7 @@ fn item_from_record(mut rec: Record, span: Span) -> Result Result Result { +fn duration_from_value(v: Value, span: Span) -> Result { chrono::Duration::nanoseconds(v.as_duration()?) .to_std() - .map_err(|_| ShellError::IOError { - msg: "negative duration not supported".to_string(), - }) + .map_err(|_| ShellError::NeedsPositiveValue { span }) } -fn find_backup_path(path: &Path) -> Result { +fn find_backup_path(path: &Path, span: Span) -> Result { let Ok(mut bak_path) = path.to_path_buf().into_os_string().into_string() else { // This isn't fundamentally problem, but trying to work with OsString is a nightmare. - return Err(ShellError::IOError { - msg: "History path mush be representable as UTF-8".to_string(), + return Err(ShellError::GenericError { + error: "History path not UTF-8".to_string(), + msg: "History path must be representable as UTF-8".to_string(), + span: Some(span), + help: None, + inner: vec![], }); }; bak_path.push_str(".bak"); @@ -260,24 +264,45 @@ fn find_backup_path(path: &Path) -> Result { return Ok(PathBuf::from(bak_path)); } } - Err(ShellError::IOError { - msg: "Too many existing backup files".to_string(), + Err(ShellError::GenericError { + error: "Too many backup files".to_string(), + msg: "Found too many existing backup files".to_string(), + span: Some(span), + help: None, + inner: vec![], }) } -fn backup(path: &Path) -> Result, ShellError> { +fn backup(path: &Path, span: Span) -> Result, ShellError> { match path.metadata() { Ok(md) if md.is_file() => (), Ok(_) => { - return Err(ShellError::IOError { - msg: "history path exists but is not a file".to_string(), - }) + return Err(IoError::new_with_additional_context( + shell_error::io::ErrorKind::NotAFile, + span, + PathBuf::from(path), + "history path exists but is not a file", + ) + .into()) } Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None), - Err(e) => return Err(e.into()), + Err(e) => { + return Err(IoError::new_internal( + e.kind(), + "Could not get metadata", + nu_protocol::location!(), + ) + .into()) + } } - let bak_path = find_backup_path(path)?; - std::fs::copy(path, &bak_path)?; + let bak_path = find_backup_path(path, span)?; + std::fs::copy(path, &bak_path).map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not copy backup", + nu_protocol::location!(), + ) + })?; Ok(Some(bak_path)) } @@ -388,7 +413,7 @@ mod tests { for name in existing { std::fs::File::create_new(dir.path().join(name)).unwrap(); } - let got = find_backup_path(&dir.path().join("history.dat")).unwrap(); + let got = find_backup_path(&dir.path().join("history.dat"), Span::test_data()).unwrap(); assert_eq!(got, dir.path().join(want)) } @@ -400,7 +425,7 @@ mod tests { write!(&mut history, "123").unwrap(); let want_bak_path = dir.path().join("history.dat.bak"); assert_eq!( - backup(&dir.path().join("history.dat")), + backup(&dir.path().join("history.dat"), Span::test_data()), Ok(Some(want_bak_path.clone())) ); let got_data = String::from_utf8(std::fs::read(want_bak_path).unwrap()).unwrap(); @@ -410,7 +435,7 @@ mod tests { #[test] fn test_backup_no_file() { let dir = tempfile::tempdir().unwrap(); - let bak_path = backup(&dir.path().join("history.dat")).unwrap(); + let bak_path = backup(&dir.path().join("history.dat"), Span::test_data()).unwrap(); assert!(bak_path.is_none()); } } diff --git a/crates/nu-cli/src/commands/keybindings_listen.rs b/crates/nu-cli/src/commands/keybindings_listen.rs index 40cda5652d..ec7b33f296 100644 --- a/crates/nu-cli/src/commands/keybindings_listen.rs +++ b/crates/nu-cli/src/commands/keybindings_listen.rs @@ -2,6 +2,7 @@ use crossterm::{ event::Event, event::KeyCode, event::KeyEvent, execute, terminal, QueueableCommand, }; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::io::{stdout, Write}; #[derive(Clone)] @@ -39,7 +40,13 @@ impl Command for KeybindingsListen { match print_events(engine_state) { Ok(v) => Ok(v.into_pipeline_data()), Err(e) => { - terminal::disable_raw_mode()?; + terminal::disable_raw_mode().map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not disable raw mode", + nu_protocol::location!(), + ) + })?; Err(ShellError::GenericError { error: "Error with input".into(), msg: "".into(), @@ -63,8 +70,20 @@ impl Command for KeybindingsListen { pub fn print_events(engine_state: &EngineState) -> Result { let config = engine_state.get_config(); - stdout().flush()?; - terminal::enable_raw_mode()?; + stdout().flush().map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not flush stdout", + nu_protocol::location!(), + ) + })?; + terminal::enable_raw_mode().map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not enable raw mode", + nu_protocol::location!(), + ) + })?; if config.use_kitty_protocol { if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() { @@ -94,7 +113,9 @@ pub fn print_events(engine_state: &EngineState) -> Result { let mut stdout = std::io::BufWriter::new(std::io::stderr()); loop { - let event = crossterm::event::read()?; + let event = crossterm::event::read().map_err(|err| { + IoError::new_internal(err.kind(), "Could not read event", nu_protocol::location!()) + })?; if event == Event::Key(KeyCode::Esc.into()) { break; } @@ -113,9 +134,25 @@ pub fn print_events(engine_state: &EngineState) -> Result { _ => "".to_string(), }; - stdout.queue(crossterm::style::Print(o))?; - stdout.queue(crossterm::style::Print("\r\n"))?; - stdout.flush()?; + stdout.queue(crossterm::style::Print(o)).map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not print output record", + nu_protocol::location!(), + ) + })?; + stdout + .queue(crossterm::style::Print("\r\n")) + .map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not print linebreak", + nu_protocol::location!(), + ) + })?; + stdout.flush().map_err(|err| { + IoError::new_internal(err.kind(), "Could not flush", nu_protocol::location!()) + })?; } if config.use_kitty_protocol { @@ -125,7 +162,13 @@ pub fn print_events(engine_state: &EngineState) -> Result { ); } - terminal::disable_raw_mode()?; + terminal::disable_raw_mode().map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not disable raw mode", + nu_protocol::location!(), + ) + })?; Ok(Value::nothing(Span::unknown())) } diff --git a/crates/nu-cli/src/config_files.rs b/crates/nu-cli/src/config_files.rs index 28332fe998..bb54a860d6 100644 --- a/crates/nu-cli/src/config_files.rs +++ b/crates/nu-cli/src/config_files.rs @@ -18,7 +18,7 @@ const OLD_PLUGIN_FILE: &str = "plugin.nu"; #[cfg(feature = "plugin")] pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option>) { - use nu_protocol::ShellError; + use nu_protocol::{shell_error::io::IoError, ShellError}; use std::path::Path; let span = plugin_file.as_ref().map(|s| s.span); @@ -78,16 +78,12 @@ pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option bool { use nu_protocol::{ - PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature, - ShellError, + shell_error::io::IoError, PluginExample, PluginIdentity, PluginRegistryItem, + PluginRegistryItemData, PluginSignature, ShellError, }; use std::collections::BTreeMap; @@ -324,7 +320,15 @@ pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool { // Write the new file let new_plugin_file_path = config_dir.join(PLUGIN_FILE); if let Err(err) = std::fs::File::create(&new_plugin_file_path) - .map_err(|e| e.into()) + .map_err(|err| { + IoError::new_internal_with_path( + err.kind(), + "Could not create new plugin file", + nu_protocol::location!(), + new_plugin_file_path.clone(), + ) + }) + .map_err(ShellError::from) .and_then(|file| contents.write_to(file, None)) { report_shell_error( diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index aa4dbfca78..8b252bf17e 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -7,9 +7,11 @@ use nu_protocol::{ cli_error::report_compile_error, debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, - report_parse_error, report_parse_warning, PipelineData, ShellError, Span, Value, + report_parse_error, report_parse_warning, + shell_error::io::IoError, + PipelineData, ShellError, Span, Value, }; -use std::sync::Arc; +use std::{path::PathBuf, sync::Arc}; /// Entry point for evaluating a file. /// @@ -24,11 +26,14 @@ pub fn evaluate_file( ) -> Result<(), ShellError> { let cwd = engine_state.cwd_as_string(Some(stack))?; - let file_path = - canonicalize_with(&path, cwd).map_err(|err| ShellError::FileNotFoundCustom { - msg: format!("Could not access file '{path}': {err}"), - span: Span::unknown(), - })?; + let file_path = canonicalize_with(&path, cwd).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + PathBuf::from(&path), + "Could not access file", + ) + })?; let file_path_str = file_path .to_str() @@ -40,18 +45,24 @@ pub fn evaluate_file( span: Span::unknown(), })?; - let file = std::fs::read(&file_path).map_err(|err| ShellError::FileNotFoundCustom { - msg: format!("Could not read file '{file_path_str}': {err}"), - span: Span::unknown(), + let file = std::fs::read(&file_path).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + file_path.clone(), + "Could not read file", + ) })?; engine_state.file = Some(file_path.clone()); - let parent = file_path - .parent() - .ok_or_else(|| ShellError::FileNotFoundCustom { - msg: format!("The file path '{file_path_str}' does not have a parent"), - span: Span::unknown(), - })?; + let parent = file_path.parent().ok_or_else(|| { + IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, + Span::unknown(), + file_path.clone(), + "The file path does not have a parent", + ) + })?; stack.add_env_var( "FILE_PWD".to_string(), diff --git a/crates/nu-cli/src/repl.rs b/crates/nu-cli/src/repl.rs index b8fef7355d..72da95d449 100644 --- a/crates/nu-cli/src/repl.rs +++ b/crates/nu-cli/src/repl.rs @@ -21,6 +21,7 @@ use nu_color_config::StyleComputer; #[allow(deprecated)] use nu_engine::env_to_strings; use nu_parser::{lex, parse, trim_quotes_str}; +use nu_protocol::shell_error::io::IoError; use nu_protocol::{ config::NuCursorShape, engine::{EngineState, Stack, StateWorkingSet}, @@ -846,21 +847,26 @@ fn do_auto_cd( if !path.exists() { report_shell_error( engine_state, - &ShellError::DirectoryNotFound { - dir: path.to_string_lossy().to_string(), + &ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, span, - }, + PathBuf::from(&path), + "Cannot change directory", + )), ); } path.to_string_lossy().to_string() }; - if let PermissionResult::PermissionDenied(reason) = have_permission(path.clone()) { + if let PermissionResult::PermissionDenied(_) = have_permission(path.clone()) { report_shell_error( engine_state, - &ShellError::IOError { - msg: format!("Cannot change directory to {path}: {reason}"), - }, + &ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::PermissionDenied, + span, + PathBuf::from(path), + "Cannot change directory", + )), ); return; } diff --git a/crates/nu-cmd-extra/src/extra/strings/format/bits.rs b/crates/nu-cmd-extra/src/extra/strings/format/bits.rs index 693c6905e8..b34d429c6c 100644 --- a/crates/nu-cmd-extra/src/extra/strings/format/bits.rs +++ b/crates/nu-cmd-extra/src/extra/strings/format/bits.rs @@ -3,7 +3,7 @@ use std::io::{self, Read, Write}; use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; -use nu_protocol::Signals; +use nu_protocol::{shell_error::io::IoError, Signals}; use num_traits::ToPrimitive; struct Arguments { @@ -142,7 +142,11 @@ fn byte_stream_to_bits(stream: ByteStream, head: Span) -> ByteStream { ByteStreamType::String, move |buffer| { let mut byte = [0]; - if reader.read(&mut byte[..]).err_span(head)? > 0 { + if reader + .read(&mut byte[..]) + .map_err(|err| IoError::new(err.kind(), head, None))? + > 0 + { // Format the byte as bits if is_first { is_first = false; diff --git a/crates/nu-cmd-lang/src/core_commands/do_.rs b/crates/nu-cmd-lang/src/core_commands/do_.rs index 62ddea5324..fe788983da 100644 --- a/crates/nu-cmd-lang/src/core_commands/do_.rs +++ b/crates/nu-cmd-lang/src/core_commands/do_.rs @@ -1,7 +1,9 @@ use nu_engine::{command_prelude::*, get_eval_block_with_early_return, redirect_env}; #[cfg(feature = "os")] use nu_protocol::process::{ChildPipe, ChildProcess}; -use nu_protocol::{engine::Closure, ByteStream, ByteStreamSource, OutDest}; +use nu_protocol::{ + engine::Closure, shell_error::io::IoError, ByteStream, ByteStreamSource, OutDest, +}; use std::{ io::{Cursor, Read}, @@ -143,10 +145,16 @@ impl Command for Do { .name("stdout consumer".to_string()) .spawn(move || { let mut buf = Vec::new(); - stdout.read_to_end(&mut buf)?; + stdout.read_to_end(&mut buf).map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not read stdout to end", + nu_protocol::location!(), + ) + })?; Ok::<_, ShellError>(buf) }) - .err_span(head) + .map_err(|err| IoError::new(err.kind(), head, None)) }) .transpose()?; @@ -156,7 +164,9 @@ impl Command for Do { None => String::new(), Some(mut stderr) => { let mut buf = String::new(); - stderr.read_to_string(&mut buf).err_span(span)?; + stderr + .read_to_string(&mut buf) + .map_err(|err| IoError::new(err.kind(), span, None))?; buf } }; diff --git a/crates/nu-cmd-plugin/src/commands/plugin/add.rs b/crates/nu-cmd-plugin/src/commands/plugin/add.rs index c603ddbb8c..301b88fdff 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/add.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/add.rs @@ -1,8 +1,10 @@ use crate::util::{get_plugin_dirs, modify_plugin_file}; use nu_engine::command_prelude::*; use nu_plugin_engine::{GetPlugin, PersistentPlugin}; -use nu_protocol::{PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin}; -use std::sync::Arc; +use nu_protocol::{ + shell_error::io::IoError, PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin, +}; +use std::{path::PathBuf, sync::Arc}; #[derive(Clone)] pub struct PluginAdd; @@ -86,11 +88,14 @@ apparent the next time `nu` is next launched with that plugin registry file. let filename_expanded = nu_path::locate_in_dirs(&filename.item, &cwd, || { get_plugin_dirs(engine_state, stack) }) - .err_span(filename.span)?; + .map_err(|err| IoError::new(err.kind(), filename.span, PathBuf::from(filename.item)))?; let shell_expanded = shell .as_ref() - .map(|s| nu_path::canonicalize_with(&s.item, &cwd).err_span(s.span)) + .map(|s| { + nu_path::canonicalize_with(&s.item, &cwd) + .map_err(|err| IoError::new(err.kind(), s.span, None)) + }) .transpose()?; // Parse the plugin filename so it can be used to spawn the plugin diff --git a/crates/nu-cmd-plugin/src/util.rs b/crates/nu-cmd-plugin/src/util.rs index 57de225ace..053900ef19 100644 --- a/crates/nu-cmd-plugin/src/util.rs +++ b/crates/nu-cmd-plugin/src/util.rs @@ -1,6 +1,6 @@ #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir}; -use nu_protocol::{engine::StateWorkingSet, PluginRegistryFile}; +use nu_protocol::{engine::StateWorkingSet, shell_error::io::IoError, PluginRegistryFile}; use std::{ fs::{self, File}, path::PathBuf, @@ -45,21 +45,16 @@ pub(crate) fn read_plugin_file( // Try to read the plugin file if it exists if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) { PluginRegistryFile::read_from( - File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned { - msg: format!( - "failed to read `{}`: {}", - plugin_registry_file_path.display(), - err - ), - span: file_span, - })?, + File::open(&plugin_registry_file_path) + .map_err(|err| IoError::new(err.kind(), file_span, plugin_registry_file_path))?, Some(file_span), ) } else if let Some(path) = custom_path { - Err(ShellError::FileNotFound { - file: path.item.clone(), - span: path.span, - }) + Err(ShellError::Io(IoError::new( + std::io::ErrorKind::NotFound, + path.span, + PathBuf::from(&path.item), + ))) } else { Ok(PluginRegistryFile::default()) } @@ -80,13 +75,8 @@ pub(crate) fn modify_plugin_file( // Try to read the plugin file if it exists let mut contents = if fs::metadata(&plugin_registry_file_path).is_ok_and(|m| m.len() > 0) { PluginRegistryFile::read_from( - File::open(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned { - msg: format!( - "failed to read `{}`: {}", - plugin_registry_file_path.display(), - err - ), - span: file_span, + File::open(&plugin_registry_file_path).map_err(|err| { + IoError::new(err.kind(), file_span, plugin_registry_file_path.clone()) })?, Some(file_span), )? @@ -99,14 +89,8 @@ pub(crate) fn modify_plugin_file( // Save the modified file on success contents.write_to( - File::create(&plugin_registry_file_path).map_err(|err| ShellError::IOErrorSpanned { - msg: format!( - "failed to create `{}`: {}", - plugin_registry_file_path.display(), - err - ), - span: file_span, - })?, + File::create(&plugin_registry_file_path) + .map_err(|err| IoError::new(err.kind(), file_span, plugin_registry_file_path))?, Some(span), )?; diff --git a/crates/nu-command/src/bytes/ends_with.rs b/crates/nu-command/src/bytes/ends_with.rs index 774b45aefc..23167a7901 100644 --- a/crates/nu-command/src/bytes/ends_with.rs +++ b/crates/nu-command/src/bytes/ends_with.rs @@ -1,5 +1,6 @@ use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::{ collections::VecDeque, io::{self, BufRead}, @@ -76,7 +77,7 @@ impl Command for BytesEndsWith { Ok(&[]) => break, Ok(buf) => buf, Err(e) if e.kind() == io::ErrorKind::Interrupted => continue, - Err(e) => return Err(e.into_spanned(span).into()), + Err(e) => return Err(IoError::new(e.kind(), span, None).into()), }; let len = buf.len(); if len >= cap { diff --git a/crates/nu-command/src/bytes/starts_with.rs b/crates/nu-command/src/bytes/starts_with.rs index 39db1a80ce..9681b4ccbe 100644 --- a/crates/nu-command/src/bytes/starts_with.rs +++ b/crates/nu-command/src/bytes/starts_with.rs @@ -1,5 +1,6 @@ use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::io::Read; struct Arguments { @@ -71,7 +72,7 @@ impl Command for BytesStartsWith { reader .take(pattern.len() as u64) .read_to_end(&mut start) - .err_span(span)?; + .map_err(|err| IoError::new(err.kind(), span, None))?; Ok(Value::bool(start == pattern, head).into_pipeline_data()) } else { diff --git a/crates/nu-command/src/conversions/into/string.rs b/crates/nu-command/src/conversions/into/string.rs index 460f6d973d..261afe1dd7 100644 --- a/crates/nu-command/src/conversions/into/string.rs +++ b/crates/nu-command/src/conversions/into/string.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use nu_cmd_base::input_handler::{operate, CmdArgument}; use nu_engine::command_prelude::*; -use nu_protocol::{into_code, Config}; +use nu_protocol::{shell_error::into_code, Config}; use nu_utils::get_system_locale; use num_format::ToFormattedString; diff --git a/crates/nu-command/src/database/values/sqlite.rs b/crates/nu-command/src/database/values/sqlite.rs index 620ac95324..8959cf5998 100644 --- a/crates/nu-command/src/database/values/sqlite.rs +++ b/crates/nu-command/src/database/values/sqlite.rs @@ -2,7 +2,10 @@ use super::definitions::{ db_column::DbColumn, db_constraint::DbConstraint, db_foreignkey::DbForeignKey, db_index::DbIndex, db_table::DbTable, }; -use nu_protocol::{CustomValue, PipelineData, Record, ShellError, Signals, Span, Spanned, Value}; +use nu_protocol::{ + shell_error::io::IoError, CustomValue, PipelineData, Record, ShellError, Signals, Span, + Spanned, Value, +}; use rusqlite::{ types::ValueRef, Connection, DatabaseName, Error as SqliteError, OpenFlags, Row, Statement, ToSql, @@ -38,24 +41,22 @@ impl SQLiteDatabase { } pub fn try_from_path(path: &Path, span: Span, signals: Signals) -> Result { - let mut file = File::open(path).map_err(|e| ShellError::ReadingFile { - msg: e.to_string(), - span, - })?; + let mut file = + File::open(path).map_err(|e| IoError::new(e.kind(), span, PathBuf::from(path)))?; let mut buf: [u8; 16] = [0; 16]; file.read_exact(&mut buf) - .map_err(|e| ShellError::ReadingFile { - msg: e.to_string(), - span, - }) + .map_err(|e| ShellError::Io(IoError::new(e.kind(), span, PathBuf::from(path)))) .and_then(|_| { if buf == SQLITE_MAGIC_BYTES { Ok(SQLiteDatabase::new(path, signals)) } else { - Err(ShellError::ReadingFile { - msg: "Not a SQLite file".into(), - span, + Err(ShellError::GenericError { + error: "Not a SQLite file".into(), + msg: format!("Could not read '{}' as SQLite file", path.display()), + span: Some(span), + help: None, + inner: vec![], }) } }) diff --git a/crates/nu-command/src/env/config/config_.rs b/crates/nu-command/src/env/config/config_.rs index 71afd57c5a..f60b4be290 100644 --- a/crates/nu-command/src/env/config/config_.rs +++ b/crates/nu-command/src/env/config/config_.rs @@ -60,6 +60,8 @@ pub(super) fn start_editor( call: &Call, ) -> Result { // Find the editor executable. + + use nu_protocol::shell_error::io::IoError; let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?; let paths = nu_engine::env::path_str(engine_state, stack, call.head)?; let cwd = engine_state.cwd(Some(stack))?; @@ -99,13 +101,22 @@ pub(super) fn start_editor( // Spawn the child process. On Unix, also put the child process to // foreground if we're in an interactive session. #[cfg(windows)] - let child = ForegroundChild::spawn(command)?; + let child = ForegroundChild::spawn(command); #[cfg(unix)] let child = ForegroundChild::spawn( command, engine_state.is_interactive, &engine_state.pipeline_externals_state, - )?; + ); + + let child = child.map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + call.head, + None, + "Could not spawn foreground child", + ) + })?; // Wrap the output into a `PipelineData::ByteStream`. let child = nu_protocol::process::ChildProcess::new(child, None, false, call.head)?; diff --git a/crates/nu-command/src/env/config/config_reset.rs b/crates/nu-command/src/env/config/config_reset.rs index 03c022c3d7..5215ac6a1d 100644 --- a/crates/nu-command/src/env/config/config_reset.rs +++ b/crates/nu-command/src/env/config/config_reset.rs @@ -1,8 +1,9 @@ use chrono::Local; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use nu_utils::{get_scaffold_config, get_scaffold_env}; -use std::io::Write; +use std::{io::Write, path::PathBuf}; #[derive(Clone)] pub struct ConfigReset; @@ -58,19 +59,23 @@ impl Command for ConfigReset { "oldconfig-{}.nu", Local::now().format("%F-%H-%M-%S"), )); - if std::fs::rename(nu_config.clone(), backup_path).is_err() { - return Err(ShellError::FileNotFoundCustom { - msg: "config.nu could not be backed up".into(), + if let Err(err) = std::fs::rename(nu_config.clone(), &backup_path) { + return Err(ShellError::Io(IoError::new_with_additional_context( + err.kind(), span, - }); + PathBuf::from(backup_path), + "config.nu could not be backed up", + ))); } } - if let Ok(mut file) = std::fs::File::create(nu_config) { - if writeln!(&mut file, "{config_file}").is_err() { - return Err(ShellError::FileNotFoundCustom { - msg: "config.nu could not be written to".into(), + if let Ok(mut file) = std::fs::File::create(&nu_config) { + if let Err(err) = writeln!(&mut file, "{config_file}") { + return Err(ShellError::Io(IoError::new_with_additional_context( + err.kind(), span, - }); + PathBuf::from(nu_config), + "config.nu could not be written to", + ))); } } } @@ -81,19 +86,23 @@ impl Command for ConfigReset { if !no_backup { let mut backup_path = config_path.clone(); backup_path.push(format!("oldenv-{}.nu", Local::now().format("%F-%H-%M-%S"),)); - if std::fs::rename(env_config.clone(), backup_path).is_err() { - return Err(ShellError::FileNotFoundCustom { - msg: "env.nu could not be backed up".into(), + if let Err(err) = std::fs::rename(env_config.clone(), &backup_path) { + return Err(ShellError::Io(IoError::new_with_additional_context( + err.kind(), span, - }); + PathBuf::from(backup_path), + "env.nu could not be backed up", + ))); } } - if let Ok(mut file) = std::fs::File::create(env_config) { - if writeln!(&mut file, "{config_file}").is_err() { - return Err(ShellError::FileNotFoundCustom { - msg: "env.nu could not be written to".into(), + if let Ok(mut file) = std::fs::File::create(&env_config) { + if let Err(err) = writeln!(&mut file, "{config_file}") { + return Err(ShellError::Io(IoError::new_with_additional_context( + err.kind(), span, - }); + PathBuf::from(env_config), + "env.nu could not be written to", + ))); } } } diff --git a/crates/nu-command/src/env/source_env.rs b/crates/nu-command/src/env/source_env.rs index 342c31aed4..a16bbe557f 100644 --- a/crates/nu-command/src/env/source_env.rs +++ b/crates/nu-command/src/env/source_env.rs @@ -2,7 +2,7 @@ use nu_engine::{ command_prelude::*, find_in_dirs_env, get_dirs_var_from_call, get_eval_block_with_early_return, redirect_env, }; -use nu_protocol::{engine::CommandType, BlockId}; +use nu_protocol::{engine::CommandType, shell_error::io::IoError, BlockId}; use std::path::PathBuf; /// Source a file for environment variables. @@ -65,10 +65,11 @@ impl Command for SourceEnv { )? { PathBuf::from(&path) } else { - return Err(ShellError::FileNotFound { - file: source_filename.item, - span: source_filename.span, - }); + return Err(ShellError::Io(IoError::new( + std::io::ErrorKind::NotFound, + source_filename.span, + PathBuf::from(source_filename.item), + ))); }; if let Some(parent) = file_path.parent() { diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index 2af932bcb4..3ba438746d 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -1,4 +1,7 @@ +use std::path::PathBuf; + use nu_engine::command_prelude::*; +use nu_protocol::shell_error::{self, io::IoError}; use nu_utils::filesystem::{have_permission, PermissionResult}; #[derive(Clone)] @@ -77,25 +80,39 @@ impl Command for Cd { if physical { if let Ok(path) = nu_path::canonicalize_with(path_no_whitespace, &cwd) { if !path.is_dir() { - return Err(ShellError::NotADirectory { span: v.span }); + return Err(shell_error::io::IoError::new( + shell_error::io::ErrorKind::NotADirectory, + v.span, + None, + ) + .into()); }; path } else { - return Err(ShellError::DirectoryNotFound { - dir: path_no_whitespace.to_string(), - span: v.span, - }); + return Err(shell_error::io::IoError::new( + std::io::ErrorKind::NotFound, + v.span, + PathBuf::from(path_no_whitespace), + ) + .into()); } } else { let path = nu_path::expand_path_with(path_no_whitespace, &cwd, true); if !path.exists() { - return Err(ShellError::DirectoryNotFound { - dir: path_no_whitespace.to_string(), - span: v.span, - }); + return Err(shell_error::io::IoError::new( + std::io::ErrorKind::NotFound, + v.span, + PathBuf::from(path_no_whitespace), + ) + .into()); }; if !path.is_dir() { - return Err(ShellError::NotADirectory { span: v.span }); + return Err(shell_error::io::IoError::new( + shell_error::io::ErrorKind::NotADirectory, + v.span, + path, + ) + .into()); }; path } @@ -117,13 +134,9 @@ impl Command for Cd { stack.set_cwd(path)?; Ok(PipelineData::empty()) } - PermissionResult::PermissionDenied(reason) => Err(ShellError::IOError { - msg: format!( - "Cannot change directory to {}: {}", - path.to_string_lossy(), - reason - ), - }), + PermissionResult::PermissionDenied(_) => { + Err(IoError::new(std::io::ErrorKind::PermissionDenied, call.head, path).into()) + } } } diff --git a/crates/nu-command/src/filesystem/ls.rs b/crates/nu-command/src/filesystem/ls.rs index 68f35c5294..19418b1a70 100644 --- a/crates/nu-command/src/filesystem/ls.rs +++ b/crates/nu-command/src/filesystem/ls.rs @@ -5,7 +5,7 @@ use nu_engine::glob_from; use nu_engine::{command_prelude::*, env::current_dir}; use nu_glob::MatchOptions; use nu_path::{expand_path_with, expand_to_real_path}; -use nu_protocol::{DataSource, NuGlob, PipelineMetadata, Signals}; +use nu_protocol::{shell_error::io::IoError, DataSource, NuGlob, PipelineMetadata, Signals}; use pathdiff::diff_paths; use rayon::prelude::*; #[cfg(unix)] @@ -254,10 +254,12 @@ fn ls_for_one_pattern( if let Some(path) = pattern_arg { // it makes no sense to list an empty string. if path.item.as_ref().is_empty() { - return Err(ShellError::FileNotFoundCustom { - msg: "empty string('') directory or file does not exist".to_string(), - span: path.span, - }); + return Err(ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, + path.span, + PathBuf::from(path.item.to_string()), + "empty string('') directory or file does not exist", + ))); } match path.item { NuGlob::DoNotExpand(p) => Some(Spanned { @@ -283,10 +285,7 @@ fn ls_for_one_pattern( nu_path::expand_path_with(pat.item.as_ref(), &cwd, pat.item.is_expand()); // Avoid checking and pushing "*" to the path when directory (do not show contents) flag is true if !directory && tmp_expanded.is_dir() { - if read_dir(&tmp_expanded, p_tag, use_threads)? - .next() - .is_none() - { + if read_dir(tmp_expanded, p_tag, use_threads)?.next().is_none() { return Ok(Value::test_nothing().into_pipeline_data()); } just_read_dir = !(pat.item.is_expand() && nu_glob::is_glob(pat.item.as_ref())); @@ -305,7 +304,7 @@ fn ls_for_one_pattern( // Avoid pushing "*" to the default path when directory (do not show contents) flag is true if directory { (NuGlob::Expand(".".to_string()), false) - } else if read_dir(&cwd, p_tag, use_threads)?.next().is_none() { + } else if read_dir(cwd.clone(), p_tag, use_threads)?.next().is_none() { return Ok(Value::test_nothing().into_pipeline_data()); } else { (NuGlob::Expand("*".to_string()), false) @@ -318,7 +317,7 @@ fn ls_for_one_pattern( let path = pattern_arg.into_spanned(p_tag); let (prefix, paths) = if just_read_dir { let expanded = nu_path::expand_path_with(path.item.as_ref(), &cwd, path.item.is_expand()); - let paths = read_dir(&expanded, p_tag, use_threads)?; + let paths = read_dir(expanded.clone(), p_tag, use_threads)?; // just need to read the directory, so prefix is path itself. (Some(expanded), paths) } else { @@ -350,7 +349,16 @@ fn ls_for_one_pattern( let signals_clone = signals.clone(); let pool = if use_threads { - let count = std::thread::available_parallelism()?.get(); + let count = std::thread::available_parallelism() + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + call_span, + None, + "Could not get available parallelism", + ) + })? + .get(); create_pool(count)? } else { create_pool(1)? @@ -910,14 +918,12 @@ mod windows_helper { &mut find_data, ) { Ok(_) => Ok(find_data), - Err(e) => Err(ShellError::ReadingFile { - msg: format!( - "Could not read metadata for '{}':\n '{}'", - filename.to_string_lossy(), - e - ), + Err(e) => Err(ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::Other, span, - }), + PathBuf::from(filename), + format!("Could not read metadata: {e}"), + ))), } } } @@ -950,28 +956,17 @@ mod windows_helper { #[allow(clippy::type_complexity)] fn read_dir( - f: &Path, + f: PathBuf, span: Span, use_threads: bool, ) -> Result> + Send>, ShellError> { let items = f .read_dir() - .map_err(|error| { - if error.kind() == std::io::ErrorKind::PermissionDenied { - return ShellError::GenericError { - error: "Permission denied".into(), - msg: "The permissions may not allow access for this user".into(), - span: Some(span), - help: None, - inner: vec![], - }; - } - - error.into() - })? - .map(|d| { + .map_err(|err| IoError::new(err.kind(), span, f.clone()))? + .map(move |d| { d.map(|r| r.path()) - .map_err(|e| ShellError::IOError { msg: e.to_string() }) + .map_err(|err| IoError::new(err.kind(), span, f.clone())) + .map_err(ShellError::from) }); if !use_threads { let mut collected = items.collect::>(); diff --git a/crates/nu-command/src/filesystem/mktemp.rs b/crates/nu-command/src/filesystem/mktemp.rs index 9a0c041f15..5ebd054acb 100644 --- a/crates/nu-command/src/filesystem/mktemp.rs +++ b/crates/nu-command/src/filesystem/mktemp.rs @@ -106,14 +106,10 @@ impl Command for Mktemp { }; let res = match uu_mktemp::mktemp(&options) { - Ok(res) => { - res.into_os_string() - .into_string() - .map_err(|e| ShellError::IOErrorSpanned { - msg: e.to_string_lossy().to_string(), - span, - })? - } + Ok(res) => res + .into_os_string() + .into_string() + .map_err(|_| ShellError::NonUtf8 { span })?, Err(e) => { return Err(ShellError::GenericError { error: format!("{}", e), diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index db00f35d6d..a372d7e79e 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -1,7 +1,11 @@ #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir, get_eval_block}; -use nu_protocol::{ast, DataSource, NuGlob, PipelineMetadata}; -use std::path::Path; +use nu_protocol::{ + ast, + shell_error::{self, io::IoError}, + DataSource, NuGlob, PipelineMetadata, +}; +use std::path::{Path, PathBuf}; #[cfg(feature = "sqlite")] use crate::database::SQLiteDatabase; @@ -87,25 +91,10 @@ impl Command for Open { for path in nu_engine::glob_from(&path, &cwd, call_span, None) .map_err(|err| match err { - ShellError::DirectoryNotFound { span, .. } => ShellError::FileNotFound { - file: path.item.to_string(), - span, - }, - // that particular error in `nu_engine::glob_from` doesn't have a span attached - // to it, so let's add it - ShellError::GenericError { - error, - msg, - span: _, - help, - inner, - } if error.as_str() == "Permission denied" => ShellError::GenericError { - error, - msg, - span: Some(arg_span), - help, - inner, - }, + ShellError::Io(mut err) => { + err.span = arg_span; + err.into() + } _ => err, })? .1 @@ -114,24 +103,26 @@ impl Command for Open { let path = Path::new(&path); if permission_denied(path) { + let err = IoError::new( + std::io::ErrorKind::PermissionDenied, + arg_span, + PathBuf::from(path), + ); + #[cfg(unix)] - let error_msg = match path.metadata() { - Ok(md) => format!( - "The permissions of {:o} does not allow access for this user", - md.permissions().mode() & 0o0777 - ), - Err(e) => e.to_string(), + let err = { + let mut err = err; + err.additional_context = Some(match path.metadata() { + Ok(md) => format!( + "The permissions of {:o} does not allow access for this user", + md.permissions().mode() & 0o0777 + ), + Err(e) => e.to_string(), + }); + err }; - #[cfg(not(unix))] - let error_msg = String::from("Permission denied"); - return Err(ShellError::GenericError { - error: "Permission denied".into(), - msg: error_msg, - span: Some(arg_span), - help: None, - inner: vec![], - }); + return Err(err.into()); } else { #[cfg(feature = "sqlite")] if !raw { @@ -147,18 +138,18 @@ impl Command for Open { } } - let file = match std::fs::File::open(path) { - Ok(file) => file, - Err(err) => { - return Err(ShellError::GenericError { - error: "Permission denied".into(), - msg: err.to_string(), - span: Some(arg_span), - help: None, - inner: vec![], - }); - } - }; + if path.is_dir() { + // At least under windows this check ensures that we don't get a + // permission denied error on directories + return Err(ShellError::Io(IoError::new( + shell_error::io::ErrorKind::IsADirectory, + arg_span, + PathBuf::from(path), + ))); + } + + let file = std::fs::File::open(path) + .map_err(|err| IoError::new(err.kind(), arg_span, PathBuf::from(path)))?; // No content_type by default - Is added later if no converter is found let stream = PipelineData::ByteStream( diff --git a/crates/nu-command/src/filesystem/rm.rs b/crates/nu-command/src/filesystem/rm.rs index 9a13442aa3..91daf99d8c 100644 --- a/crates/nu-command/src/filesystem/rm.rs +++ b/crates/nu-command/src/filesystem/rm.rs @@ -3,7 +3,11 @@ use super::util::try_interaction; use nu_engine::{command_prelude::*, env::current_dir}; use nu_glob::MatchOptions; use nu_path::expand_path_with; -use nu_protocol::{report_shell_error, NuGlob}; +use nu_protocol::{ + report_shell_error, + shell_error::{self, io::IoError}, + NuGlob, +}; #[cfg(unix)] use std::os::unix::prelude::FileTypeExt; use std::{ @@ -299,9 +303,17 @@ fn rm( } } Err(e) => { - // glob_from may canonicalize path and return `DirectoryNotFound` + // glob_from may canonicalize path and return an error when a directory is not found // nushell should suppress the error if `--force` is used. - if !(force && matches!(e, ShellError::DirectoryNotFound { .. })) { + if !(force + && matches!( + e, + ShellError::Io(IoError { + kind: shell_error::io::ErrorKind::Std(std::io::ErrorKind::NotFound), + .. + }) + )) + { return Err(e); } } @@ -413,8 +425,7 @@ fn rm( }; if let Err(e) = result { - let msg = format!("Could not delete {:}: {e:}", f.to_string_lossy()); - Err(ShellError::RemoveNotPossible { msg, span }) + Err(ShellError::Io(IoError::new(e.kind(), span, f))) } else if verbose { let msg = if interactive && !confirmed { "not deleted" diff --git a/crates/nu-command/src/filesystem/save.rs b/crates/nu-command/src/filesystem/save.rs index 67fd1a2271..bd580eec8c 100644 --- a/crates/nu-command/src/filesystem/save.rs +++ b/crates/nu-command/src/filesystem/save.rs @@ -4,8 +4,8 @@ use nu_engine::get_eval_block; use nu_engine::{command_prelude::*, current_dir}; use nu_path::expand_path_with; use nu_protocol::{ - ast, byte_stream::copy_with_signals, process::ChildPipe, ByteStreamSource, DataSource, OutDest, - PipelineMetadata, Signals, + ast, byte_stream::copy_with_signals, process::ChildPipe, shell_error::io::IoError, + ByteStreamSource, DataSource, OutDest, PipelineMetadata, Signals, }; use std::{ fs::File, @@ -86,6 +86,7 @@ impl Command for Save { span: arg.span, }); + let from_io_error = IoError::factory(span, path.item.as_path()); match input { PipelineData::ByteStream(stream, metadata) => { check_saving_to_source_file(metadata.as_ref(), &path, stderr_path.as_ref())?; @@ -129,7 +130,7 @@ impl Command for Save { io::copy(&mut tee, &mut io::stderr()) } } - .err_span(span)?; + .map_err(|err| IoError::new(err.kind(), span, None))?; } Ok(()) } @@ -153,7 +154,7 @@ impl Command for Save { ) }) .transpose() - .err_span(span)?; + .map_err(&from_io_error)?; let res = match stdout { ChildPipe::Pipe(pipe) => { @@ -203,15 +204,10 @@ impl Command for Save { let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; for val in ls { file.write_all(&value_to_bytes(val)?) - .map_err(|err| ShellError::IOError { - msg: err.to_string(), - })?; - file.write_all("\n".as_bytes()) - .map_err(|err| ShellError::IOError { - msg: err.to_string(), - })?; + .map_err(&from_io_error)?; + file.write_all("\n".as_bytes()).map_err(&from_io_error)?; } - file.flush()?; + file.flush().map_err(&from_io_error)?; Ok(PipelineData::empty()) } @@ -232,11 +228,8 @@ impl Command for Save { // Only open file after successful conversion let (mut file, _) = get_files(&path, stderr_path.as_ref(), append, force)?; - file.write_all(&bytes).map_err(|err| ShellError::IOError { - msg: err.to_string(), - })?; - - file.flush()?; + file.write_all(&bytes).map_err(&from_io_error)?; + file.flush().map_err(&from_io_error)?; Ok(PipelineData::empty()) } @@ -420,33 +413,27 @@ fn prepare_path( } fn open_file(path: &Path, span: Span, append: bool) -> Result { - let file = match (append, path.exists()) { - (true, true) => std::fs::OpenOptions::new().append(true).open(path), + let file: Result = match (append, path.exists()) + { + (true, true) => std::fs::OpenOptions::new() + .append(true) + .open(path) + .map_err(|err| err.kind().into()), _ => { // This is a temporary solution until `std::fs::File::create` is fixed on Windows (rust-lang/rust#134893) // A TOCTOU problem exists here, which may cause wrong error message to be shown #[cfg(target_os = "windows")] if path.is_dir() { - // It should be `io::ErrorKind::IsADirectory` but it's not available in stable yet (1.83) - Err(io::Error::new( - io::ErrorKind::Unsupported, - "Is a directory (os error 21)", - )) + Err(nu_protocol::shell_error::io::ErrorKind::IsADirectory) } else { - std::fs::File::create(path) + std::fs::File::create(path).map_err(|err| err.kind().into()) } #[cfg(not(target_os = "windows"))] - std::fs::File::create(path) + std::fs::File::create(path).map_err(|err| err.kind().into()) } }; - file.map_err(|e| ShellError::GenericError { - error: format!("Problem with [{}], Permission denied", path.display()), - msg: e.to_string(), - span: Some(span), - help: None, - inner: vec![], - }) + file.map_err(|err_kind| ShellError::Io(IoError::new(err_kind, span, PathBuf::from(path)))) } /// Get output file and optional stderr file @@ -493,6 +480,9 @@ fn stream_to_file( span: Span, progress: bool, ) -> Result<(), ShellError> { + // TODO: maybe we can get a path in here + let from_io_error = IoError::factory(span, None); + // https://github.com/nushell/nushell/pull/9377 contains the reason for not using `BufWriter` if progress { let mut bytes_processed = 0; @@ -512,7 +502,7 @@ fn stream_to_file( match reader.fill_buf() { Ok(&[]) => break Ok(()), Ok(buf) => { - file.write_all(buf).err_span(span)?; + file.write_all(buf).map_err(&from_io_error)?; let len = buf.len(); reader.consume(len); bytes_processed += len as u64; @@ -530,9 +520,9 @@ fn stream_to_file( if let Err(err) = res { let _ = file.flush(); bar.abandoned_msg("# Error while saving #".to_owned()); - Err(err.into_spanned(span).into()) + Err(from_io_error(err).into()) } else { - file.flush().err_span(span)?; + file.flush().map_err(&from_io_error)?; Ok(()) } } else { diff --git a/crates/nu-command/src/filesystem/ucp.rs b/crates/nu-command/src/filesystem/ucp.rs index 40315d9e78..a65cbece66 100644 --- a/crates/nu-command/src/filesystem/ucp.rs +++ b/crates/nu-command/src/filesystem/ucp.rs @@ -1,6 +1,6 @@ #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir}; -use nu_protocol::NuGlob; +use nu_protocol::{shell_error::io::IoError, NuGlob}; use std::path::PathBuf; use uu_cp::{BackupMode, CopyMode, UpdateMode}; @@ -197,10 +197,11 @@ impl Command for UCp { .map(|f| f.1)? .collect(); if exp_files.is_empty() { - return Err(ShellError::FileNotFound { - file: p.item.to_string(), - span: p.span, - }); + return Err(ShellError::Io(IoError::new( + std::io::ErrorKind::NotFound, + p.span, + PathBuf::from(p.item.to_string()), + ))); }; let mut app_vals: Vec = Vec::new(); for v in exp_files { diff --git a/crates/nu-command/src/filesystem/umv.rs b/crates/nu-command/src/filesystem/umv.rs index cdf0c05b1f..c76ab9f376 100644 --- a/crates/nu-command/src/filesystem/umv.rs +++ b/crates/nu-command/src/filesystem/umv.rs @@ -1,7 +1,7 @@ #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir}; use nu_path::expand_path_with; -use nu_protocol::NuGlob; +use nu_protocol::{shell_error::io::IoError, NuGlob}; use std::{ffi::OsString, path::PathBuf}; use uu_mv::{BackupMode, UpdateMode}; @@ -138,10 +138,11 @@ impl Command for UMv { .map(|f| f.1)? .collect(); if exp_files.is_empty() { - return Err(ShellError::FileNotFound { - file: p.item.to_string(), - span: p.span, - }); + return Err(ShellError::Io(IoError::new( + std::io::ErrorKind::NotFound, + p.span, + PathBuf::from(p.item.to_string()), + ))); }; let mut app_vals: Vec = Vec::new(); for v in exp_files { diff --git a/crates/nu-command/src/filesystem/utouch.rs b/crates/nu-command/src/filesystem/utouch.rs index 9fbf9805de..d887a5c411 100644 --- a/crates/nu-command/src/filesystem/utouch.rs +++ b/crates/nu-command/src/filesystem/utouch.rs @@ -3,8 +3,8 @@ use filetime::FileTime; use nu_engine::command_prelude::*; use nu_glob::{glob, is_glob}; use nu_path::expand_path_with; -use nu_protocol::NuGlob; -use std::{io::ErrorKind, path::PathBuf}; +use nu_protocol::{shell_error::io::IoError, NuGlob}; +use std::path::PathBuf; use uu_touch::{error::TouchError, ChangeTimes, InputFile, Options, Source}; #[derive(Clone)] @@ -225,20 +225,12 @@ impl Command for UTouch { }, TouchError::ReferenceFileInaccessible(reference_path, io_err) => { let span = reference_span.expect("touch should've been given a reference file"); - if io_err.kind() == ErrorKind::NotFound { - ShellError::FileNotFound { - span, - file: reference_path.display().to_string(), - } - } else { - ShellError::GenericError { - error: io_err.to_string(), - msg: format!("Failed to read metadata of {}", reference_path.display()), - span: Some(span), - help: None, - inner: Vec::new(), - } - } + ShellError::Io(IoError::new_with_additional_context( + io_err.kind(), + span, + reference_path, + "failed to read metadata", + )) } _ => ShellError::GenericError { error: err.to_string(), diff --git a/crates/nu-command/src/filesystem/watch.rs b/crates/nu-command/src/filesystem/watch.rs index dd21304173..3028181a21 100644 --- a/crates/nu-command/src/filesystem/watch.rs +++ b/crates/nu-command/src/filesystem/watch.rs @@ -9,6 +9,7 @@ use nu_engine::{command_prelude::*, ClosureEval}; use nu_protocol::{ engine::{Closure, StateWorkingSet}, format_shell_error, + shell_error::io::IoError, }; use std::{ path::PathBuf, @@ -83,11 +84,12 @@ impl Command for Watch { let path = match nu_path::canonicalize_with(path_no_whitespace, cwd) { Ok(p) => p, - Err(_) => { - return Err(ShellError::DirectoryNotFound { - dir: path_no_whitespace.to_string(), - span: path_arg.span, - }) + Err(err) => { + return Err(ShellError::Io(IoError::new( + err.kind(), + path_arg.span, + PathBuf::from(path_no_whitespace), + ))) } }; @@ -151,14 +153,22 @@ impl Command for Watch { let mut debouncer = match new_debouncer(debounce_duration, None, tx) { Ok(d) => d, Err(e) => { - return Err(ShellError::IOError { - msg: format!("Failed to create watcher: {e}"), - }) + return Err(ShellError::GenericError { + error: "Failed to create watcher".to_string(), + msg: e.to_string(), + span: Some(call.head), + help: None, + inner: vec![], + }); } }; if let Err(e) = debouncer.watcher().watch(&path, recursive_mode) { - return Err(ShellError::IOError { - msg: format!("Failed to create watcher: {e}"), + return Err(ShellError::GenericError { + error: "Failed to create watcher".to_string(), + msg: e.to_string(), + span: Some(call.head), + help: None, + inner: vec![], }); } // need to cache to make sure that rename event works. @@ -249,13 +259,21 @@ impl Command for Watch { } } Ok(Err(_)) => { - return Err(ShellError::IOError { + return Err(ShellError::GenericError { + error: "Receiving events failed".to_string(), msg: "Unexpected errors when receiving events".into(), - }) + span: None, + help: None, + inner: vec![], + }); } Err(RecvTimeoutError::Disconnected) => { - return Err(ShellError::IOError { + return Err(ShellError::GenericError { + error: "Disconnected".to_string(), msg: "Unexpected disconnect from file watcher".into(), + span: None, + help: None, + inner: vec![], }); } Err(RecvTimeoutError::Timeout) => {} diff --git a/crates/nu-command/src/filters/chunks.rs b/crates/nu-command/src/filters/chunks.rs index 93757c36e8..5149713644 100644 --- a/crates/nu-command/src/filters/chunks.rs +++ b/crates/nu-command/src/filters/chunks.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::ListStream; +use nu_protocol::{shell_error::io::IoError, ListStream}; use std::{ io::{BufRead, Cursor, ErrorKind}, num::NonZeroUsize, @@ -119,6 +119,7 @@ pub fn chunks( chunk_size: NonZeroUsize, span: Span, ) -> Result { + let from_io_error = IoError::factory(span, None); match input { PipelineData::Value(Value::List { vals, .. }, metadata) => { let chunks = ChunksIter::new(vals, chunk_size, span); @@ -136,7 +137,7 @@ pub fn chunks( }; let value_stream = chunk_read.map(move |chunk| match chunk { Ok(chunk) => Value::binary(chunk, span), - Err(e) => Value::error(e.into(), span), + Err(e) => Value::error(from_io_error(e).into(), span), }); let pipeline_data_with_metadata = value_stream.into_pipeline_data_with_metadata( span, @@ -155,7 +156,7 @@ pub fn chunks( }; let value_stream = chunk_read.map(move |chunk| match chunk { Ok(chunk) => Value::binary(chunk, span), - Err(e) => Value::error(e.into(), span), + Err(e) => Value::error(from_io_error(e).into(), span), }); value_stream.into_pipeline_data_with_metadata( span, diff --git a/crates/nu-command/src/filters/empty.rs b/crates/nu-command/src/filters/empty.rs index f4dd428b77..92a540fa46 100644 --- a/crates/nu-command/src/filters/empty.rs +++ b/crates/nu-command/src/filters/empty.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::io::Read; pub fn empty( @@ -41,7 +42,12 @@ pub fn empty( let span = stream.span(); match stream.reader() { Some(reader) => { - let is_empty = reader.bytes().next().transpose().err_span(span)?.is_none(); + let is_empty = reader + .bytes() + .next() + .transpose() + .map_err(|err| IoError::new(err.kind(), span, None))? + .is_none(); if negate { Ok(Value::bool(!is_empty, head).into_pipeline_data()) } else { diff --git a/crates/nu-command/src/filters/first.rs b/crates/nu-command/src/filters/first.rs index 5695f823c7..a1f15c64ce 100644 --- a/crates/nu-command/src/filters/first.rs +++ b/crates/nu-command/src/filters/first.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::Signals; +use nu_protocol::{shell_error::io::IoError, Signals}; use std::io::Read; #[derive(Clone)] @@ -180,7 +180,11 @@ fn first_helper( if return_single_element { // Take a single byte let mut byte = [0u8]; - if reader.read(&mut byte).err_span(span)? > 0 { + if reader + .read(&mut byte) + .map_err(|err| IoError::new(err.kind(), span, None))? + > 0 + { Ok(Value::int(byte[0] as i64, head).into_pipeline_data()) } else { Err(ShellError::AccessEmptyContent { span: head }) diff --git a/crates/nu-command/src/filters/interleave.rs b/crates/nu-command/src/filters/interleave.rs index f31a3411ae..4f2b457af3 100644 --- a/crates/nu-command/src/filters/interleave.rs +++ b/crates/nu-command/src/filters/interleave.rs @@ -1,5 +1,5 @@ use nu_engine::{command_prelude::*, ClosureEvalOnce}; -use nu_protocol::engine::Closure; +use nu_protocol::{engine::Closure, shell_error::io::IoError}; use std::{sync::mpsc, thread}; #[derive(Clone)] @@ -137,10 +137,7 @@ interleave } }) .map(|_| ()) - .map_err(|err| ShellError::IOErrorSpanned { - msg: err.to_string(), - span: head, - }) + .map_err(|err| IoError::new(err.kind(), head, None).into()) }) })?; diff --git a/crates/nu-command/src/filters/last.rs b/crates/nu-command/src/filters/last.rs index 127833dcd4..77b57880f6 100644 --- a/crates/nu-command/src/filters/last.rs +++ b/crates/nu-command/src/filters/last.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::{collections::VecDeque, io::Read}; #[derive(Clone)] @@ -165,7 +166,7 @@ impl Command for Last { let mut buf = VecDeque::with_capacity(rows + TAKE as usize); loop { let taken = std::io::copy(&mut (&mut reader).take(TAKE), &mut buf) - .err_span(span)?; + .map_err(|err| IoError::new(err.kind(), span, None))?; if buf.len() > rows { buf.drain(..(buf.len() - rows)); } diff --git a/crates/nu-command/src/filters/tee.rs b/crates/nu-command/src/filters/tee.rs index e643939894..c6b61a2fb5 100644 --- a/crates/nu-command/src/filters/tee.rs +++ b/crates/nu-command/src/filters/tee.rs @@ -2,8 +2,8 @@ use nu_engine::{command_prelude::*, get_eval_block_with_early_return}; #[cfg(feature = "os")] use nu_protocol::process::ChildPipe; use nu_protocol::{ - byte_stream::copy_with_signals, engine::Closure, report_shell_error, ByteStream, - ByteStreamSource, OutDest, PipelineMetadata, Signals, + byte_stream::copy_with_signals, engine::Closure, report_shell_error, shell_error::io::IoError, + ByteStream, ByteStreamSource, OutDest, PipelineMetadata, Signals, }; use std::{ io::{self, Read, Write}, @@ -82,6 +82,7 @@ use it in your pipeline."# input: PipelineData, ) -> Result { let head = call.head; + let from_io_error = IoError::factory(head, None); let use_stderr = call.has_flag(engine_state, stack, "stderr")?; let closure: Spanned = call.req(engine_state, stack, 0)?; @@ -263,7 +264,7 @@ use it in your pipeline."# let input = rx.into_pipeline_data_with_metadata(span, signals, metadata_clone); eval_block(input) }) - .err_span(call.head)? + .map_err(&from_io_error)? .map(move |result| result.unwrap_or_else(|err| Value::error(err, closure_span))) .into_pipeline_data_with_metadata( span, @@ -278,7 +279,7 @@ use it in your pipeline."# tee_once(engine_state_arc, move || { eval_block(value_clone.into_pipeline_data_with_metadata(metadata_clone)) }) - .err_span(call.head)?; + .map_err(&from_io_error)?; Ok(value.into_pipeline_data_with_metadata(metadata)) } } @@ -439,7 +440,9 @@ fn spawn_tee( ); eval_block(PipelineData::ByteStream(stream, info.metadata)) }) - .err_span(info.span)?; + .map_err(|err| { + IoError::new_with_additional_context(err.kind(), info.span, None, "Could not spawn tee") + })?; Ok(TeeThread { sender, thread }) } @@ -478,7 +481,15 @@ fn copy_on_thread( copy_with_signals(src, dest, span, &signals)?; Ok(()) }) - .map_err(|e| e.into_spanned(span).into()) + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + span, + None, + "Could not spawn stderr copier", + ) + .into() + }) } #[cfg(feature = "os")] @@ -521,7 +532,12 @@ fn tee_forwards_errors_back_immediately() { use std::time::Duration; let slow_input = (0..100).inspect(|_| std::thread::sleep(Duration::from_millis(1))); let iter = tee(slow_input, |_| { - Err(ShellError::IOError { msg: "test".into() }) + Err(ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::Other, + Span::test_data(), + None, + "test", + ))) }) .expect("io error"); for result in iter { @@ -548,7 +564,12 @@ fn tee_waits_for_the_other_thread() { let iter = tee(0..100, move |_| { std::thread::sleep(Duration::from_millis(10)); waited_clone.store(true, Ordering::Relaxed); - Err(ShellError::IOError { msg: "test".into() }) + Err(ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::Other, + Span::test_data(), + None, + "test", + ))) }) .expect("io error"); let last = iter.last(); diff --git a/crates/nu-command/src/formats/from/json.rs b/crates/nu-command/src/formats/from/json.rs index 36a05ea4e1..17a632d949 100644 --- a/crates/nu-command/src/formats/from/json.rs +++ b/crates/nu-command/src/formats/from/json.rs @@ -1,7 +1,7 @@ use std::io::{BufRead, Cursor}; use nu_engine::command_prelude::*; -use nu_protocol::{ListStream, Signals}; +use nu_protocol::{shell_error::io::IoError, ListStream, Signals}; #[derive(Clone)] pub struct FromJson; @@ -134,7 +134,7 @@ fn read_json_lines( .lines() .filter(|line| line.as_ref().is_ok_and(|line| !line.trim().is_empty()) || line.is_err()) .map(move |line| { - let line = line.err_span(span)?; + let line = line.map_err(|err| IoError::new(err.kind(), span, None))?; if strict { convert_string_to_value_strict(&line, span) } else { diff --git a/crates/nu-command/src/formats/to/delimited.rs b/crates/nu-command/src/formats/to/delimited.rs index ac04739a3d..4054935098 100644 --- a/crates/nu-command/src/formats/to/delimited.rs +++ b/crates/nu-command/src/formats/to/delimited.rs @@ -1,16 +1,14 @@ use csv::WriterBuilder; use nu_cmd_base::formats::to::delimited::merge_descriptors; use nu_protocol::{ - ByteStream, ByteStreamType, Config, PipelineData, ShellError, Signals, Span, Spanned, Value, + shell_error::io::IoError, ByteStream, ByteStreamType, Config, PipelineData, ShellError, + Signals, Span, Spanned, Value, }; use std::{iter, sync::Arc}; fn make_csv_error(error: csv::Error, format_name: &str, head: Span) -> ShellError { if let csv::ErrorKind::Io(error) = error.kind() { - ShellError::IOErrorSpanned { - msg: error.to_string(), - span: head, - } + IoError::new(error.kind(), head, None).into() } else { ShellError::GenericError { error: format!("Failed to generate {format_name} data"), diff --git a/crates/nu-command/src/formats/to/msgpack.rs b/crates/nu-command/src/formats/to/msgpack.rs index f7c4c7df3d..ae1d11e542 100644 --- a/crates/nu-command/src/formats/to/msgpack.rs +++ b/crates/nu-command/src/formats/to/msgpack.rs @@ -5,7 +5,7 @@ use std::io; use byteorder::{BigEndian, WriteBytesExt}; use nu_engine::command_prelude::*; -use nu_protocol::{ast::PathMember, Signals, Spanned}; +use nu_protocol::{ast::PathMember, shell_error::io::IoError, Signals, Spanned}; use rmp::encode as mp; /// Max recursion depth @@ -138,7 +138,7 @@ impl From for ShellError { help: None, inner: vec![], }, - WriteError::Io(err, span) => err.into_spanned(span).into(), + WriteError::Io(err, span) => ShellError::Io(IoError::new(err.kind(), span, None)), WriteError::Shell(err) => *err, } } diff --git a/crates/nu-command/src/formats/to/msgpackz.rs b/crates/nu-command/src/formats/to/msgpackz.rs index e4166b15d3..c6d667280f 100644 --- a/crates/nu-command/src/formats/to/msgpackz.rs +++ b/crates/nu-command/src/formats/to/msgpackz.rs @@ -1,6 +1,7 @@ use std::io::Write; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use super::msgpack::write_value; @@ -80,7 +81,8 @@ impl Command for ToMsgpackz { ); write_value(&mut out, &value, 0)?; - out.flush().err_span(call.head)?; + out.flush() + .map_err(|err| IoError::new(err.kind(), call.head, None))?; drop(out); Ok(Value::binary(out_buf, call.head).into_pipeline_data()) diff --git a/crates/nu-command/src/formats/to/text.rs b/crates/nu-command/src/formats/to/text.rs index 2fc3046e4e..2736fe24e5 100644 --- a/crates/nu-command/src/formats/to/text.rs +++ b/crates/nu-command/src/formats/to/text.rs @@ -1,6 +1,8 @@ use chrono_humanize::HumanTime; use nu_engine::command_prelude::*; -use nu_protocol::{format_duration, ByteStream, Config, PipelineMetadata}; +use nu_protocol::{ + format_duration, shell_error::io::IoError, ByteStream, Config, PipelineMetadata, +}; use std::io::Write; const LINE_ENDING: &str = if cfg!(target_os = "windows") { @@ -72,6 +74,7 @@ impl Command for ToText { } PipelineData::ListStream(stream, meta) => { let span = stream.span(); + let from_io_error = IoError::factory(head, None); let stream = if no_newline { let mut first = true; let mut iter = stream.into_inner(); @@ -87,7 +90,7 @@ impl Command for ToText { if first { first = false; } else { - write!(buf, "{LINE_ENDING}").err_span(head)?; + write!(buf, "{LINE_ENDING}").map_err(&from_io_error)?; } // TODO: write directly into `buf` instead of creating an intermediate // string. @@ -98,7 +101,7 @@ impl Command for ToText { &config, serialize_types, ); - write!(buf, "{str}").err_span(head)?; + write!(buf, "{str}").map_err(&from_io_error)?; Ok(true) }, ) diff --git a/crates/nu-command/src/misc/source.rs b/crates/nu-command/src/misc/source.rs index c1be441bb0..de692c557c 100644 --- a/crates/nu-command/src/misc/source.rs +++ b/crates/nu-command/src/misc/source.rs @@ -1,6 +1,6 @@ use nu_engine::{command_prelude::*, get_eval_block_with_early_return}; use nu_path::canonicalize_with; -use nu_protocol::{engine::CommandType, BlockId}; +use nu_protocol::{engine::CommandType, shell_error::io::IoError, BlockId}; /// Source a file for environment variables. #[derive(Clone)] @@ -55,11 +55,8 @@ impl Command for Source { let cwd = engine_state.cwd_as_string(Some(stack))?; let pb = std::path::PathBuf::from(block_id_name); let parent = pb.parent().unwrap_or(std::path::Path::new("")); - let file_path = - canonicalize_with(pb.as_path(), cwd).map_err(|err| ShellError::FileNotFoundCustom { - msg: format!("Could not access file '{}': {err}", pb.as_path().display()), - span: Span::unknown(), - })?; + let file_path = canonicalize_with(pb.as_path(), cwd) + .map_err(|err| IoError::new(err.kind(), call.head, pb.clone()))?; // Note: We intentionally left out PROCESS_PATH since it's supposed to // to work like argv[0] in C, which is the name of the program being executed. diff --git a/crates/nu-command/src/network/http/client.rs b/crates/nu-command/src/network/http/client.rs index a5cad69b2c..28a8bc8207 100644 --- a/crates/nu-command/src/network/http/client.rs +++ b/crates/nu-command/src/network/http/client.rs @@ -6,10 +6,11 @@ use base64::{ }; use multipart_rs::MultipartWriter; use nu_engine::command_prelude::*; -use nu_protocol::{ByteStream, LabeledError, Signals}; +use nu_protocol::{shell_error::io::IoError, ByteStream, LabeledError, Signals}; use serde_json::Value as JsonValue; use std::{ collections::HashMap, + error::Error as StdError, io::Cursor, path::PathBuf, str::FromStr, @@ -184,6 +185,7 @@ pub fn request_add_authorization_header( request } +#[derive(Debug)] #[allow(clippy::large_enum_variant)] pub enum ShellErrorOrRequestError { ShellError(ShellError), @@ -372,10 +374,10 @@ fn send_multipart_request( Value::Record { val, .. } => { let mut builder = MultipartWriter::new(); - let err = |e| { - ShellErrorOrRequestError::ShellError(ShellError::IOError { - msg: format!("failed to build multipart data: {}", e), - }) + let err = |e: std::io::Error| { + ShellErrorOrRequestError::ShellError( + IoError::new_with_additional_context(e.kind(), span, None, e).into(), + ) }; for (col, val) in val.into_owned() { @@ -464,6 +466,14 @@ fn send_cancellable_request( let ret = request_fn(); let _ = tx.send(ret); // may fail if the user has cancelled the operation }) + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + span, + None, + "Could not spawn HTTP requester", + ) + }) .map_err(ShellError::from)?; // ...and poll the channel for responses @@ -519,6 +529,14 @@ fn send_cancellable_request_bytes( // may fail if the user has cancelled the operation let _ = tx.send(ret); }) + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + span, + None, + "Could not spawn HTTP requester", + ) + }) .map_err(ShellError::from)?; // ...and poll the channel for responses @@ -618,27 +636,56 @@ pub fn request_add_custom_headers( fn handle_response_error(span: Span, requested_url: &str, response_err: Error) -> ShellError { match response_err { - Error::Status(301, _) => ShellError::NetworkFailure { msg: format!("Resource moved permanently (301): {requested_url:?}"), span }, - Error::Status(400, _) => { - ShellError::NetworkFailure { msg: format!("Bad request (400) to {requested_url:?}"), span } - } - Error::Status(403, _) => { - ShellError::NetworkFailure { msg: format!("Access forbidden (403) to {requested_url:?}"), span } - } - Error::Status(404, _) => ShellError::NetworkFailure { msg: format!("Requested file not found (404): {requested_url:?}"), span }, - Error::Status(408, _) => { - ShellError::NetworkFailure { msg: format!("Request timeout (408): {requested_url:?}"), span } - } - Error::Status(_, _) => ShellError::NetworkFailure { msg: format!( + Error::Status(301, _) => ShellError::NetworkFailure { + msg: format!("Resource moved permanently (301): {requested_url:?}"), + span, + }, + Error::Status(400, _) => ShellError::NetworkFailure { + msg: format!("Bad request (400) to {requested_url:?}"), + span, + }, + Error::Status(403, _) => ShellError::NetworkFailure { + msg: format!("Access forbidden (403) to {requested_url:?}"), + span, + }, + Error::Status(404, _) => ShellError::NetworkFailure { + msg: format!("Requested file not found (404): {requested_url:?}"), + span, + }, + Error::Status(408, _) => ShellError::NetworkFailure { + msg: format!("Request timeout (408): {requested_url:?}"), + span, + }, + Error::Status(_, _) => ShellError::NetworkFailure { + msg: format!( "Cannot make request to {:?}. Error is {:?}", requested_url, response_err.to_string() - ), span }, - - Error::Transport(t) => match t { - t if t.kind() == ErrorKind::ConnectionFailed => ShellError::NetworkFailure { msg: format!("Cannot make request to {requested_url}, there was an error establishing a connection.",), span }, - t => ShellError::NetworkFailure { msg: t.to_string(), span }, + ), + span, }, + + Error::Transport(t) => { + let generic_network_failure = || ShellError::NetworkFailure { + msg: t.to_string(), + span, + }; + match t.kind() { + ErrorKind::ConnectionFailed => ShellError::NetworkFailure { msg: format!("Cannot make request to {requested_url}, there was an error establishing a connection.",), span }, + ErrorKind::Io => 'io: { + let Some(source) = t.source() else { + break 'io generic_network_failure(); + }; + + let Some(io_error) = source.downcast_ref::() else { + break 'io generic_network_failure(); + }; + + ShellError::Io(IoError::new(io_error.kind(), span, None)) + } + _ => generic_network_failure() + } + } } } diff --git a/crates/nu-command/src/network/port.rs b/crates/nu-command/src/network/port.rs index eb77ab1d32..fe3c987b85 100644 --- a/crates/nu-command/src/network/port.rs +++ b/crates/nu-command/src/network/port.rs @@ -1,4 +1,5 @@ use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::net::{Ipv4Addr, SocketAddr, SocketAddrV4, TcpListener}; @@ -61,12 +62,14 @@ fn get_free_port( stack: &mut Stack, call: &Call, ) -> Result { + let from_io_error = IoError::factory(call.head, None); + let start_port: Option> = call.opt(engine_state, stack, 0)?; let end_port: Option> = call.opt(engine_state, stack, 1)?; let listener = if start_port.is_none() && end_port.is_none() { // get free port from system. - TcpListener::bind("127.0.0.1:0")? + TcpListener::bind("127.0.0.1:0").map_err(&from_io_error)? } else { let (start_port, start_span) = match start_port { Some(p) => (p.item, Some(p.span)), @@ -118,20 +121,25 @@ fn get_free_port( }); } - // try given port one by one. - match (start_port..=end_port) - .map(|port| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port))) - .find_map(|addr| TcpListener::bind(addr).ok()) - { - Some(listener) => listener, - None => { - return Err(ShellError::IOError { - msg: "Every port has been tried, but no valid one was found".to_string(), - }) + 'search: { + let mut last_err = None; + for port in start_port..=end_port { + let addr = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, port)); + match TcpListener::bind(addr) { + Ok(listener) => break 'search Ok(listener), + Err(err) => last_err = Some(err), + } } - } + + Err(IoError::new_with_additional_context( + last_err.expect("range not empty, validated before").kind(), + range_span, + None, + "Every port has been tried, but no valid one was found", + )) + }? }; - let free_port = listener.local_addr()?.port(); + let free_port = listener.local_addr().map_err(&from_io_error)?.port(); Ok(Value::int(free_port as i64, call.head).into_pipeline_data()) } diff --git a/crates/nu-command/src/path/exists.rs b/crates/nu-command/src/path/exists.rs index 3e40bef28a..0c482a5fe5 100644 --- a/crates/nu-command/src/path/exists.rs +++ b/crates/nu-command/src/path/exists.rs @@ -2,7 +2,7 @@ use super::PathSubcommandArguments; #[allow(deprecated)] use nu_engine::{command_prelude::*, current_dir, current_dir_const}; use nu_path::expand_path_with; -use nu_protocol::engine::StateWorkingSet; +use nu_protocol::{engine::StateWorkingSet, shell_error::io::IoError}; use std::path::{Path, PathBuf}; struct Arguments { @@ -140,7 +140,7 @@ fn exists(path: &Path, span: Span, args: &Arguments) -> Value { // symlink_metadata returns true if the file/folder exists // whether it is a symbolic link or not. Sorry, but returns Err // in every other scenario including the NotFound - std::fs::symlink_metadata(path).map_or_else( + std::fs::symlink_metadata(&path).map_or_else( |e| match e.kind() { std::io::ErrorKind::NotFound => Ok(false), _ => Err(e), @@ -153,15 +153,7 @@ fn exists(path: &Path, span: Span, args: &Arguments) -> Value { Value::bool( match exists { Ok(exists) => exists, - Err(err) => { - return Value::error( - ShellError::IOErrorSpanned { - msg: err.to_string(), - span, - }, - span, - ) - } + Err(err) => return Value::error(IoError::new(err.kind(), span, path).into(), span), }, span, ) diff --git a/crates/nu-command/src/path/self_.rs b/crates/nu-command/src/path/self_.rs index 242fbea7a8..49c1de8624 100644 --- a/crates/nu-command/src/path/self_.rs +++ b/crates/nu-command/src/path/self_.rs @@ -1,6 +1,6 @@ use nu_engine::command_prelude::*; use nu_path::expand_path_with; -use nu_protocol::engine::StateWorkingSet; +use nu_protocol::{engine::StateWorkingSet, shell_error::io::IoError}; #[derive(Clone)] pub struct SubCommand; @@ -54,23 +54,25 @@ impl Command for SubCommand { ) -> Result { let path: Option = call.opt_const(working_set, 0)?; let cwd = working_set.permanent_state.cwd(None)?; - let current_file = - working_set - .files - .top() - .ok_or_else(|| ShellError::FileNotFoundCustom { - msg: "Couldn't find current file".into(), - span: call.head, - })?; + let current_file = working_set.files.top().ok_or_else(|| { + IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, + call.head, + None, + "Couldn't find current file", + ) + })?; let out = if let Some(path) = path { let dir = expand_path_with( - current_file - .parent() - .ok_or_else(|| ShellError::FileNotFoundCustom { - msg: "Couldn't find current file's parent.".into(), - span: call.head, - })?, + current_file.parent().ok_or_else(|| { + IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, + call.head, + current_file.to_owned(), + "Couldn't find current file's parent.", + ) + })?, &cwd, true, ); diff --git a/crates/nu-command/src/path/type.rs b/crates/nu-command/src/path/type.rs index 81cdaf8cfe..6abed1319a 100644 --- a/crates/nu-command/src/path/type.rs +++ b/crates/nu-command/src/path/type.rs @@ -1,7 +1,7 @@ use super::PathSubcommandArguments; use nu_engine::command_prelude::*; use nu_path::AbsolutePathBuf; -use nu_protocol::engine::StateWorkingSet; +use nu_protocol::{engine::StateWorkingSet, shell_error::io::IoError}; use std::{io, path::Path}; struct Arguments { @@ -108,7 +108,7 @@ fn path_type(path: &Path, span: Span, args: &Arguments) -> Value { match path.symlink_metadata() { Ok(metadata) => Value::string(get_file_type(&metadata), span), Err(err) if err.kind() == io::ErrorKind::NotFound => Value::nothing(span), - Err(err) => Value::error(err.into_spanned(span).into(), span), + Err(err) => Value::error(IoError::new(err.kind(), span, None).into(), span), } } diff --git a/crates/nu-command/src/platform/clear.rs b/crates/nu-command/src/platform/clear.rs index 21e3a3054c..38bb659df6 100644 --- a/crates/nu-command/src/platform/clear.rs +++ b/crates/nu-command/src/platform/clear.rs @@ -4,6 +4,7 @@ use crossterm::{ QueueableCommand, }; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::io::Write; @@ -41,19 +42,27 @@ impl Command for Clear { call: &Call, _input: PipelineData, ) -> Result { + let from_io_error = IoError::factory(call.head, None); match call.has_flag(engine_state, stack, "keep-scrollback")? { true => { std::io::stdout() - .queue(MoveTo(0, 0))? - .queue(ClearCommand(ClearType::All))? - .flush()?; + .queue(MoveTo(0, 0)) + .map_err(&from_io_error)? + .queue(ClearCommand(ClearType::All)) + .map_err(&from_io_error)? + .flush() + .map_err(&from_io_error)?; } _ => { std::io::stdout() - .queue(MoveTo(0, 0))? - .queue(ClearCommand(ClearType::All))? - .queue(ClearCommand(ClearType::Purge))? - .flush()?; + .queue(MoveTo(0, 0)) + .map_err(&from_io_error)? + .queue(ClearCommand(ClearType::All)) + .map_err(&from_io_error)? + .queue(ClearCommand(ClearType::Purge)) + .map_err(&from_io_error)? + .flush() + .map_err(&from_io_error)?; } }; diff --git a/crates/nu-command/src/platform/dir_info.rs b/crates/nu-command/src/platform/dir_info.rs index 9b8ea1929e..d96e43403b 100644 --- a/crates/nu-command/src/platform/dir_info.rs +++ b/crates/nu-command/src/platform/dir_info.rs @@ -1,6 +1,6 @@ use filesize::file_real_size_fast; use nu_glob::Pattern; -use nu_protocol::{record, ShellError, Signals, Span, Value}; +use nu_protocol::{record, shell_error::io::IoError, ShellError, Signals, Span, Value}; use std::path::PathBuf; #[derive(Debug, Clone)] @@ -77,7 +77,7 @@ impl FileInfo { long, }) } - Err(e) => Err(e.into()), + Err(e) => Err(IoError::new(e.kind(), tag, path).into()), } } } @@ -91,6 +91,7 @@ impl DirInfo { signals: &Signals, ) -> Result { let path = path.into(); + let from_io_error = IoError::factory(span, path.as_path()); let mut s = Self { dirs: Vec::new(), @@ -99,7 +100,7 @@ impl DirInfo { size: 0, blocks: 0, tag: params.tag, - path, + path: path.clone(), long: params.long, }; @@ -108,7 +109,7 @@ impl DirInfo { s.size = d.len(); // dir entry size s.blocks = file_real_size_fast(&s.path, &d).ok().unwrap_or(0); } - Err(e) => s = s.add_error(e.into()), + Err(e) => s = s.add_error(from_io_error(e).into()), }; match std::fs::read_dir(&s.path) { @@ -122,13 +123,13 @@ impl DirInfo { s = s.add_dir(i.path(), depth, params, span, signals)? } Ok(_t) => s = s.add_file(i.path(), params), - Err(e) => s = s.add_error(e.into()), + Err(e) => s = s.add_error(from_io_error(e).into()), }, - Err(e) => s = s.add_error(e.into()), + Err(e) => s = s.add_error(from_io_error(e).into()), } } } - Err(e) => s = s.add_error(e.into()), + Err(e) => s = s.add_error(from_io_error(e).into()), } Ok(s) } diff --git a/crates/nu-command/src/platform/input/input_.rs b/crates/nu-command/src/platform/input/input_.rs index f792db992e..5e088098d2 100644 --- a/crates/nu-command/src/platform/input/input_.rs +++ b/crates/nu-command/src/platform/input/input_.rs @@ -7,6 +7,7 @@ use crossterm::{ }; use itertools::Itertools; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::{io::Write, time::Duration}; @@ -69,6 +70,8 @@ impl Command for Input { span: call.head, }); + let from_io_error = IoError::factory(call.head, None); + if numchar.item < 1 { return Err(ShellError::UnsupportedInput { msg: "Number of characters to read has to be positive".to_string(), @@ -89,11 +92,11 @@ impl Command for Input { let mut buf = String::new(); - crossterm::terminal::enable_raw_mode()?; + crossterm::terminal::enable_raw_mode().map_err(&from_io_error)?; // clear terminal events - while crossterm::event::poll(Duration::from_secs(0))? { + while crossterm::event::poll(Duration::from_secs(0)).map_err(&from_io_error)? { // If there's an event, read it to remove it from the queue - let _ = crossterm::event::read()?; + let _ = crossterm::event::read().map_err(&from_io_error)?; } loop { @@ -110,10 +113,14 @@ impl Command for Input { || k.modifiers == KeyModifiers::CONTROL { if k.modifiers == KeyModifiers::CONTROL && c == 'c' { - crossterm::terminal::disable_raw_mode()?; - return Err(ShellError::IOError { - msg: "SIGINT".to_string(), - }); + crossterm::terminal::disable_raw_mode() + .map_err(&from_io_error)?; + return Err(IoError::new( + std::io::ErrorKind::Interrupted, + call.head, + None, + ) + .into()); } continue; } @@ -138,8 +145,8 @@ impl Command for Input { }, Ok(_) => continue, Err(event_error) => { - crossterm::terminal::disable_raw_mode()?; - return Err(event_error.into()); + crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?; + return Err(from_io_error(event_error).into()); } } if !suppress_output { @@ -148,16 +155,18 @@ impl Command for Input { std::io::stdout(), terminal::Clear(ClearType::CurrentLine), cursor::MoveToColumn(0), - )?; + ) + .map_err(|err| IoError::new(err.kind(), call.head, None))?; if let Some(prompt) = &prompt { - execute!(std::io::stdout(), Print(prompt.to_string()))?; + execute!(std::io::stdout(), Print(prompt.to_string())) + .map_err(&from_io_error)?; } - execute!(std::io::stdout(), Print(buf.to_string()))?; + execute!(std::io::stdout(), Print(buf.to_string())).map_err(&from_io_error)?; } } - crossterm::terminal::disable_raw_mode()?; + crossterm::terminal::disable_raw_mode().map_err(&from_io_error)?; if !suppress_output { - std::io::stdout().write_all(b"\n")?; + std::io::stdout().write_all(b"\n").map_err(&from_io_error)?; } match default_val { Some(val) if buf.is_empty() => Ok(Value::string(val, call.head).into_pipeline_data()), diff --git a/crates/nu-command/src/platform/input/input_listen.rs b/crates/nu-command/src/platform/input/input_listen.rs index 69e96e1966..56241d6689 100644 --- a/crates/nu-command/src/platform/input/input_listen.rs +++ b/crates/nu-command/src/platform/input/input_listen.rs @@ -5,6 +5,7 @@ use crossterm::event::{ use crossterm::{execute, terminal}; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use num_traits::AsPrimitive; use std::io::stdout; @@ -83,7 +84,7 @@ There are 4 `key_type` variants: let add_raw = call.has_flag(engine_state, stack, "raw")?; let config = engine_state.get_config(); - terminal::enable_raw_mode()?; + terminal::enable_raw_mode().map_err(|err| IoError::new(err.kind(), head, None))?; if config.use_kitty_protocol { if let Ok(false) = crossterm::terminal::supports_keyboard_enhancement() { @@ -111,7 +112,7 @@ There are 4 `key_type` variants: ); } - let console_state = event_type_filter.enable_events()?; + let console_state = event_type_filter.enable_events(head)?; loop { let event = crossterm::event::read().map_err(|_| ShellError::GenericError { error: "Error with user input".into(), @@ -122,7 +123,7 @@ There are 4 `key_type` variants: })?; let event = parse_event(head, &event, &event_type_filter, add_raw); if let Some(event) = event { - terminal::disable_raw_mode()?; + terminal::disable_raw_mode().map_err(|err| IoError::new(err.kind(), head, None))?; if config.use_kitty_protocol { let _ = execute!( std::io::stdout(), @@ -226,17 +227,20 @@ impl EventTypeFilter { /// Enable capturing of all events allowed by this filter. /// Call [`DeferredConsoleRestore::restore`] when done capturing events to restore /// console state - fn enable_events(&self) -> Result { + fn enable_events(&self, span: Span) -> Result { if self.listen_mouse { - crossterm::execute!(stdout(), EnableMouseCapture)?; + crossterm::execute!(stdout(), EnableMouseCapture) + .map_err(|err| IoError::new(err.kind(), span, None))?; } if self.listen_paste { - crossterm::execute!(stdout(), EnableBracketedPaste)?; + crossterm::execute!(stdout(), EnableBracketedPaste) + .map_err(|err| IoError::new(err.kind(), span, None))?; } if self.listen_focus { - crossterm::execute!(stdout(), crossterm::event::EnableFocusChange)?; + crossterm::execute!(stdout(), crossterm::event::EnableFocusChange) + .map_err(|err| IoError::new(err.kind(), span, None))?; } Ok(DeferredConsoleRestore { diff --git a/crates/nu-command/src/platform/input/list.rs b/crates/nu-command/src/platform/input/list.rs index 7d9761f296..91a31aebd8 100644 --- a/crates/nu-command/src/platform/input/list.rs +++ b/crates/nu-command/src/platform/input/list.rs @@ -1,5 +1,6 @@ use dialoguer::{console::Term, FuzzySelect, MultiSelect, Select}; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use std::fmt::{Display, Formatter}; @@ -141,8 +142,13 @@ impl Command for InputList { .items(&options) .report(false) .interact_on_opt(&Term::stderr()) - .map_err(|err| ShellError::IOError { - msg: format!("{}: {}", INTERACT_ERROR, err), + .map_err(|dialoguer::Error::IO(err)| { + IoError::new_with_additional_context( + err.kind(), + call.head, + None, + INTERACT_ERROR, + ) })?, ) } else if fuzzy { @@ -158,8 +164,13 @@ impl Command for InputList { .default(0) .report(false) .interact_on_opt(&Term::stderr()) - .map_err(|err| ShellError::IOError { - msg: format!("{}: {}", INTERACT_ERROR, err), + .map_err(|dialoguer::Error::IO(err)| { + IoError::new_with_additional_context( + err.kind(), + call.head, + None, + INTERACT_ERROR, + ) })?, ) } else { @@ -174,8 +185,13 @@ impl Command for InputList { .default(0) .report(false) .interact_on_opt(&Term::stderr()) - .map_err(|err| ShellError::IOError { - msg: format!("{}: {}", INTERACT_ERROR, err), + .map_err(|dialoguer::Error::IO(err)| { + IoError::new_with_additional_context( + err.kind(), + call.head, + None, + INTERACT_ERROR, + ) })?, ) }; diff --git a/crates/nu-command/src/platform/term/term_query.rs b/crates/nu-command/src/platform/term/term_query.rs index 2cb11f1dc3..167f660c22 100644 --- a/crates/nu-command/src/platform/term/term_query.rs +++ b/crates/nu-command/src/platform/term/term_query.rs @@ -4,6 +4,7 @@ use std::{ }; use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; const CTRL_C: u8 = 3; @@ -98,15 +99,19 @@ The `prefix` is not included in the output." let prefix = prefix.unwrap_or_default(); let terminator: Option> = call.get_flag(engine_state, stack, "terminator")?; - crossterm::terminal::enable_raw_mode()?; + crossterm::terminal::enable_raw_mode() + .map_err(|err| IoError::new(err.kind(), call.head, None))?; scopeguard::defer! { let _ = crossterm::terminal::disable_raw_mode(); } // clear terminal events - while crossterm::event::poll(Duration::from_secs(0))? { + while crossterm::event::poll(Duration::from_secs(0)) + .map_err(|err| IoError::new(err.kind(), call.head, None))? + { // If there's an event, read it to remove it from the queue - let _ = crossterm::event::read()?; + let _ = crossterm::event::read() + .map_err(|err| IoError::new(err.kind(), call.head, None))?; } let mut b = [0u8; 1]; @@ -115,13 +120,19 @@ The `prefix` is not included in the output." { let mut stdout = std::io::stdout().lock(); - stdout.write_all(&query)?; - stdout.flush()?; + stdout + .write_all(&query) + .map_err(|err| IoError::new(err.kind(), call.head, None))?; + stdout + .flush() + .map_err(|err| IoError::new(err.kind(), call.head, None))?; } // Validate and skip prefix for bc in prefix { - stdin.read_exact(&mut b)?; + stdin + .read_exact(&mut b) + .map_err(|err| IoError::new(err.kind(), call.head, None))?; if b[0] != bc { return Err(ShellError::GenericError { error: "Input did not begin with expected sequence".into(), @@ -138,7 +149,9 @@ The `prefix` is not included in the output." if let Some(terminator) = terminator { loop { - stdin.read_exact(&mut b)?; + stdin + .read_exact(&mut b) + .map_err(|err| IoError::new(err.kind(), call.head, None))?; if b[0] == CTRL_C { return Err(ShellError::InterruptedByUser { @@ -158,7 +171,9 @@ The `prefix` is not included in the output." } } else { loop { - stdin.read_exact(&mut b)?; + stdin + .read_exact(&mut b) + .map_err(|err| IoError::new(err.kind(), call.head, None))?; if b[0] == CTRL_C { break; diff --git a/crates/nu-command/src/strings/str_/join.rs b/crates/nu-command/src/strings/str_/join.rs index e40ffb9417..e500da8343 100644 --- a/crates/nu-command/src/strings/str_/join.rs +++ b/crates/nu-command/src/strings/str_/join.rs @@ -1,5 +1,5 @@ use nu_engine::command_prelude::*; -use nu_protocol::Signals; +use nu_protocol::{shell_error::io::IoError, Signals}; use std::io::Write; @@ -94,13 +94,15 @@ fn run( Signals::empty(), ByteStreamType::String, move |buffer| { + let from_io_error = IoError::factory(span, None); + // Write each input to the buffer if let Some(value) = iter.next() { // Write the separator if this is not the first if first { first = false; } else if let Some(separator) = &separator { - write!(buffer, "{}", separator)?; + write!(buffer, "{}", separator).map_err(&from_io_error)?; } match value { @@ -109,8 +111,9 @@ fn run( } // Hmm, not sure what we actually want. // `to_expanded_string` formats dates as human readable which feels funny. - Value::Date { val, .. } => write!(buffer, "{val:?}")?, - value => write!(buffer, "{}", value.to_expanded_string("\n", &config))?, + Value::Date { val, .. } => write!(buffer, "{val:?}").map_err(&from_io_error)?, + value => write!(buffer, "{}", value.to_expanded_string("\n", &config)) + .map_err(&from_io_error)?, } Ok(true) } else { diff --git a/crates/nu-command/src/system/nu_check.rs b/crates/nu-command/src/system/nu_check.rs index ff3047a490..ca0d09965a 100644 --- a/crates/nu-command/src/system/nu_check.rs +++ b/crates/nu-command/src/system/nu_check.rs @@ -1,7 +1,10 @@ use nu_engine::{command_prelude::*, find_in_dirs_env, get_dirs_var_from_call}; use nu_parser::{parse, parse_module_block, parse_module_file_or_dir, unescape_unquote_string}; -use nu_protocol::engine::{FileStack, StateWorkingSet}; -use std::path::Path; +use nu_protocol::{ + engine::{FileStack, StateWorkingSet}, + shell_error::io::IoError, +}; +use std::path::{Path, PathBuf}; #[derive(Clone)] pub struct NuCheck; @@ -89,17 +92,15 @@ impl Command for NuCheck { stack, get_dirs_var_from_call(stack, call), ) { - Ok(path) => { - if let Some(path) = path { - path - } else { - return Err(ShellError::FileNotFound { - file: path_str.item, - span: path_span, - }); - } + Ok(Some(path)) => path, + Ok(None) => { + return Err(ShellError::Io(IoError::new( + std::io::ErrorKind::NotFound, + path_span, + PathBuf::from(path_str.item), + ))) } - Err(error) => return Err(error), + Err(err) => return Err(err), }; let result = if as_module || path.is_dir() { @@ -258,13 +259,13 @@ fn parse_file_script( ) -> Result { let filename = check_path(working_set, path_span, call_head)?; - if let Ok(contents) = std::fs::read(path) { - parse_script(working_set, Some(&filename), &contents, is_debug, call_head) - } else { - Err(ShellError::IOErrorSpanned { - msg: "Could not read path".to_string(), - span: path_span, - }) + match std::fs::read(path) { + Ok(contents) => parse_script(working_set, Some(&filename), &contents, is_debug, call_head), + Err(err) => Err(ShellError::Io(IoError::new( + err.kind(), + path_span, + PathBuf::from(path), + ))), } } diff --git a/crates/nu-command/src/system/registry_query.rs b/crates/nu-command/src/system/registry_query.rs index 3365a7d440..cadb3aac29 100644 --- a/crates/nu-command/src/system/registry_query.rs +++ b/crates/nu-command/src/system/registry_query.rs @@ -1,5 +1,6 @@ use nu_engine::command_prelude::*; +use nu_protocol::shell_error::io::IoError; use windows::{core::PCWSTR, Win32::System::Environment::ExpandEnvironmentStringsW}; use winreg::{enums::*, types::FromRegValue, RegKey}; @@ -90,7 +91,9 @@ fn registry_query( let registry_value: Option> = call.opt(engine_state, stack, 1)?; let reg_hive = get_reg_hive(engine_state, stack, call)?; - let reg_key = reg_hive.open_subkey(registry_key.item)?; + let reg_key = reg_hive + .open_subkey(registry_key.item) + .map_err(|err| IoError::new(err.kind(), *registry_key_span, None))?; if registry_value.is_none() { let mut reg_values = vec![]; diff --git a/crates/nu-command/src/system/run_external.rs b/crates/nu-command/src/system/run_external.rs index b917577ba9..70e423387a 100644 --- a/crates/nu-command/src/system/run_external.rs +++ b/crates/nu-command/src/system/run_external.rs @@ -2,7 +2,8 @@ use nu_cmd_base::hook::eval_hook; use nu_engine::{command_prelude::*, env_to_strings}; use nu_path::{dots::expand_ndots_safe, expand_tilde, AbsolutePath}; use nu_protocol::{ - did_you_mean, process::ChildProcess, ByteStream, NuGlob, OutDest, Signals, UseAnsiColoring, + did_you_mean, process::ChildProcess, shell_error::io::IoError, ByteStream, NuGlob, OutDest, + Signals, UseAnsiColoring, }; use nu_system::ForegroundChild; use nu_utils::IgnoreCaseExt; @@ -169,7 +170,9 @@ impl Command for External { // canonicalize the path to the script so that tests pass let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) { - canonicalize_with(&expanded_name, cwd)? + canonicalize_with(&expanded_name, cwd).map_err(|err| { + IoError::new(err.kind(), call.head, PathBuf::from(&expanded_name)) + })? } else { // If we can't get the current working directory, just provide the expanded name expanded_name @@ -191,13 +194,22 @@ impl Command for External { let stdout = stack.stdout(); let stderr = stack.stderr(); let merged_stream = if matches!(stdout, OutDest::Pipe) && matches!(stderr, OutDest::Pipe) { - let (reader, writer) = os_pipe::pipe()?; - command.stdout(writer.try_clone()?); + let (reader, writer) = + os_pipe::pipe().map_err(|err| IoError::new(err.kind(), call.head, None))?; + command.stdout( + writer + .try_clone() + .map_err(|err| IoError::new(err.kind(), call.head, None))?, + ); command.stderr(writer); Some(reader) } else { - command.stdout(Stdio::try_from(stdout)?); - command.stderr(Stdio::try_from(stderr)?); + command.stdout( + Stdio::try_from(stdout).map_err(|err| IoError::new(err.kind(), call.head, None))?, + ); + command.stderr( + Stdio::try_from(stderr).map_err(|err| IoError::new(err.kind(), call.head, None))?, + ); None }; @@ -231,13 +243,22 @@ impl Command for External { // Spawn the child process. On Unix, also put the child process to // foreground if we're in an interactive session. #[cfg(windows)] - let mut child = ForegroundChild::spawn(command)?; + let child = ForegroundChild::spawn(command); #[cfg(unix)] - let mut child = ForegroundChild::spawn( + let child = ForegroundChild::spawn( command, engine_state.is_interactive, &engine_state.pipeline_externals_state, - )?; + ); + + let mut child = child.map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not spawn foreground child", + ) + })?; // If we need to copy data into the child process, do it now. if let Some(data) = data_to_copy_into_stdin { @@ -249,7 +270,14 @@ impl Command for External { .spawn(move || { let _ = write_pipeline_data(engine_state, stack, data, stdin); }) - .err_span(call.head)?; + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + call.head, + None, + "Could not spawn external stdin worker", + ) + })?; } // Wrap the output into a `PipelineData::ByteStream`. @@ -414,7 +442,14 @@ fn write_pipeline_data( if let PipelineData::ByteStream(stream, ..) = data { stream.write_to(writer)?; } else if let PipelineData::Value(Value::Binary { val, .. }, ..) = data { - writer.write_all(&val)?; + writer.write_all(&val).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not write pipeline data", + ) + })?; } else { stack.start_collect_value(); @@ -428,7 +463,14 @@ fn write_pipeline_data( // Write the output. for value in output { let bytes = value.coerce_into_binary()?; - writer.write_all(&bytes)?; + writer.write_all(&bytes).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not write pipeline data", + ) + })?; } } Ok(()) diff --git a/crates/nu-command/src/viewers/table.rs b/crates/nu-command/src/viewers/table.rs index bac035f94a..147555558e 100644 --- a/crates/nu-command/src/viewers/table.rs +++ b/crates/nu-command/src/viewers/table.rs @@ -13,7 +13,8 @@ use nu_engine::{command_prelude::*, env_to_string}; use nu_path::form::Absolute; use nu_pretty_hex::HexConfig; use nu_protocol::{ - ByteStream, Config, DataSource, ListStream, PipelineMetadata, Signals, TableMode, ValueIterator, + shell_error::io::IoError, ByteStream, Config, DataSource, ListStream, PipelineMetadata, + Signals, TableMode, ValueIterator, }; use nu_table::{ common::configure_table, CollapsedTable, ExpandedTable, JustTable, NuRecordsValue, NuTable, @@ -518,7 +519,7 @@ fn pretty_hex_stream(stream: ByteStream, span: Span) -> ByteStream { (&mut reader) .take(cfg.width as u64) .read_to_end(&mut read_buf) - .err_span(span)?; + .map_err(|err| IoError::new(err.kind(), span, None))?; if !read_buf.is_empty() { nu_pretty_hex::hex_write(&mut write_buf, &read_buf, cfg, Some(true)) diff --git a/crates/nu-command/tests/commands/cd.rs b/crates/nu-command/tests/commands/cd.rs index 1b8e2dad5a..7dd648d135 100644 --- a/crates/nu-command/tests/commands/cd.rs +++ b/crates/nu-command/tests/commands/cd.rs @@ -188,7 +188,7 @@ fn filesystem_not_a_directory() { actual.err ); assert!( - actual.err.contains("is not a directory"), + actual.err.contains("nu::shell::io::not_a_directory"), "actual={:?}", actual.err ); @@ -210,7 +210,7 @@ fn filesystem_directory_not_found() { actual.err ); assert!( - actual.err.contains("directory not found"), + actual.err.contains("nu::shell::io::not_found"), "actual={:?}", actual.err ); @@ -282,7 +282,7 @@ fn cd_permission_denied_folder() { cd banned " ); - assert!(actual.err.contains("Cannot change directory to")); + assert!(actual.err.contains("nu::shell::io::permission_denied")); nu!( cwd: dirs.test(), " diff --git a/crates/nu-command/tests/commands/du.rs b/crates/nu-command/tests/commands/du.rs index c88eb546be..5289a1cc95 100644 --- a/crates/nu-command/tests/commands/du.rs +++ b/crates/nu-command/tests/commands/du.rs @@ -93,7 +93,7 @@ fn du_with_multiple_path() { // report errors if one path not exists let actual = nu!(cwd: "tests/fixtures", "du cp asdf | get path | path basename"); - assert!(actual.err.contains("directory not found")); + assert!(actual.err.contains("nu::shell::io::not_found")); assert!(!actual.status.success()); // du with spreading empty list should returns nothing. diff --git a/crates/nu-command/tests/commands/ls.rs b/crates/nu-command/tests/commands/ls.rs index 416a6aaf7f..736a504935 100644 --- a/crates/nu-command/tests/commands/ls.rs +++ b/crates/nu-command/tests/commands/ls.rs @@ -610,7 +610,7 @@ fn can_list_system_folder() { fn list_a_directory_not_exists() { Playground::setup("ls_test_directory_not_exists", |dirs, _sandbox| { let actual = nu!(cwd: dirs.test(), "ls a_directory_not_exists"); - assert!(actual.err.contains("directory not found")); + assert!(actual.err.contains("nu::shell::io::not_found")); }) } @@ -735,7 +735,7 @@ fn list_with_tilde() { assert!(actual.out.contains("f2.txt")); assert!(actual.out.contains("~tilde")); let actual = nu!(cwd: dirs.test(), "ls ~tilde"); - assert!(actual.err.contains("does not exist")); + assert!(actual.err.contains("nu::shell::io::not_found")); // pass variable let actual = nu!(cwd: dirs.test(), "let f = '~tilde'; ls $f"); @@ -762,7 +762,7 @@ fn list_with_multiple_path() { // report errors if one path not exists let actual = nu!(cwd: dirs.test(), "ls asdf f1.txt"); - assert!(actual.err.contains("directory not found")); + assert!(actual.err.contains("nu::shell::io::not_found")); assert!(!actual.status.success()); // ls with spreading empty list should returns nothing. diff --git a/crates/nu-command/tests/commands/move_/umv.rs b/crates/nu-command/tests/commands/move_/umv.rs index 98e2a753bf..82a2091c13 100644 --- a/crates/nu-command/tests/commands/move_/umv.rs +++ b/crates/nu-command/tests/commands/move_/umv.rs @@ -202,7 +202,7 @@ fn errors_if_source_doesnt_exist() { cwd: dirs.test(), "mv non-existing-file test_folder/" ); - assert!(actual.err.contains("Directory not found")); + assert!(actual.err.contains("nu::shell::io::not_found")); }) } diff --git a/crates/nu-command/tests/commands/network/http/delete.rs b/crates/nu-command/tests/commands/network/http/delete.rs index ec466944de..c503745346 100644 --- a/crates/nu-command/tests/commands/network/http/delete.rs +++ b/crates/nu-command/tests/commands/network/http/delete.rs @@ -140,10 +140,6 @@ fn http_delete_timeout() { format!("http delete --max-time 100ms {url}", url = server.url()).as_str() )); - assert!(&actual.err.contains("nu::shell::network_failure")); - - #[cfg(not(target_os = "windows"))] - assert!(&actual.err.contains("timed out reading response")); - #[cfg(target_os = "windows")] - assert!(&actual.err.contains(super::WINDOWS_ERROR_TIMEOUT_SLOW_LINK)); + assert!(&actual.err.contains("nu::shell::io::timed_out")); + assert!(&actual.err.contains("Timed out")); } diff --git a/crates/nu-command/tests/commands/network/http/get.rs b/crates/nu-command/tests/commands/network/http/get.rs index a4245ee4db..a789f1e93a 100644 --- a/crates/nu-command/tests/commands/network/http/get.rs +++ b/crates/nu-command/tests/commands/network/http/get.rs @@ -334,10 +334,6 @@ fn http_get_timeout() { format!("http get --max-time 100ms {url}", url = server.url()).as_str() )); - assert!(&actual.err.contains("nu::shell::network_failure")); - - #[cfg(not(target_os = "windows"))] - assert!(&actual.err.contains("timed out reading response")); - #[cfg(target_os = "windows")] - assert!(&actual.err.contains(super::WINDOWS_ERROR_TIMEOUT_SLOW_LINK)); + assert!(&actual.err.contains("nu::shell::io::timed_out")); + assert!(&actual.err.contains("Timed out")); } diff --git a/crates/nu-command/tests/commands/network/http/mod.rs b/crates/nu-command/tests/commands/network/http/mod.rs index 07a042f674..4a96a9c5ea 100644 --- a/crates/nu-command/tests/commands/network/http/mod.rs +++ b/crates/nu-command/tests/commands/network/http/mod.rs @@ -5,14 +5,3 @@ mod options; mod patch; mod post; mod put; - -/// String representation of the Windows error code for timeouts on slow links. -/// -/// Use this constant in tests instead of matching partial error message content, -/// such as `"did not properly respond after a period of time"`, which can vary by language. -/// The specific string `"(os error 10060)"` is consistent across all locales, as it represents -/// the raw error code rather than localized text. -/// -/// For more details, see the [Microsoft docs](https://learn.microsoft.com/en-us/troubleshoot/windows-client/networking/10060-connection-timed-out-with-proxy-server). -#[cfg(all(test, windows))] -const WINDOWS_ERROR_TIMEOUT_SLOW_LINK: &str = "(os error 10060)"; diff --git a/crates/nu-command/tests/commands/network/http/options.rs b/crates/nu-command/tests/commands/network/http/options.rs index cbe9c7bd8e..92ce747690 100644 --- a/crates/nu-command/tests/commands/network/http/options.rs +++ b/crates/nu-command/tests/commands/network/http/options.rs @@ -59,10 +59,6 @@ fn http_options_timeout() { format!("http options --max-time 100ms {url}", url = server.url()).as_str() )); - assert!(&actual.err.contains("nu::shell::network_failure")); - - #[cfg(not(target_os = "windows"))] - assert!(&actual.err.contains("timed out reading response")); - #[cfg(target_os = "windows")] - assert!(&actual.err.contains(super::WINDOWS_ERROR_TIMEOUT_SLOW_LINK)); + assert!(&actual.err.contains("nu::shell::io::timed_out")); + assert!(&actual.err.contains("Timed out")); } diff --git a/crates/nu-command/tests/commands/network/http/patch.rs b/crates/nu-command/tests/commands/network/http/patch.rs index 660f335864..6b1ed54d38 100644 --- a/crates/nu-command/tests/commands/network/http/patch.rs +++ b/crates/nu-command/tests/commands/network/http/patch.rs @@ -184,10 +184,6 @@ fn http_patch_timeout() { .as_str() )); - assert!(&actual.err.contains("nu::shell::network_failure")); - - #[cfg(not(target_os = "windows"))] - assert!(&actual.err.contains("timed out reading response")); - #[cfg(target_os = "windows")] - assert!(&actual.err.contains(super::WINDOWS_ERROR_TIMEOUT_SLOW_LINK)); + assert!(&actual.err.contains("nu::shell::io::timed_out")); + assert!(&actual.err.contains("Timed out")); } diff --git a/crates/nu-command/tests/commands/network/http/post.rs b/crates/nu-command/tests/commands/network/http/post.rs index 99ef44acaf..f90efba278 100644 --- a/crates/nu-command/tests/commands/network/http/post.rs +++ b/crates/nu-command/tests/commands/network/http/post.rs @@ -298,10 +298,6 @@ fn http_post_timeout() { .as_str() )); - assert!(&actual.err.contains("nu::shell::network_failure")); - - #[cfg(not(target_os = "windows"))] - assert!(&actual.err.contains("timed out reading response")); - #[cfg(target_os = "windows")] - assert!(&actual.err.contains(super::WINDOWS_ERROR_TIMEOUT_SLOW_LINK)); + assert!(&actual.err.contains("nu::shell::io::timed_out")); + assert!(&actual.err.contains("Timed out")); } diff --git a/crates/nu-command/tests/commands/network/http/put.rs b/crates/nu-command/tests/commands/network/http/put.rs index 3f3bae3998..b66330a18b 100644 --- a/crates/nu-command/tests/commands/network/http/put.rs +++ b/crates/nu-command/tests/commands/network/http/put.rs @@ -184,10 +184,6 @@ fn http_put_timeout() { .as_str() )); - assert!(&actual.err.contains("nu::shell::network_failure")); - - #[cfg(not(target_os = "windows"))] - assert!(&actual.err.contains("timed out reading response")); - #[cfg(target_os = "windows")] - assert!(&actual.err.contains(super::WINDOWS_ERROR_TIMEOUT_SLOW_LINK)); + assert!(&actual.err.contains("nu::shell::io::timed_out")); + assert!(&actual.err.contains("Timed out")); } diff --git a/crates/nu-command/tests/commands/nu_check.rs b/crates/nu-command/tests/commands/nu_check.rs index ee8bf32c24..971349778e 100644 --- a/crates/nu-command/tests/commands/nu_check.rs +++ b/crates/nu-command/tests/commands/nu_check.rs @@ -172,7 +172,7 @@ fn file_not_exist() { " )); - assert!(actual.err.contains("file not found")); + assert!(actual.err.contains("nu::shell::io::not_found")); }) } diff --git a/crates/nu-command/tests/commands/open.rs b/crates/nu-command/tests/commands/open.rs index 986b621c99..09df04b97a 100644 --- a/crates/nu-command/tests/commands/open.rs +++ b/crates/nu-command/tests/commands/open.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use nu_test_support::fs::Stub::EmptyFile; use nu_test_support::fs::Stub::FileWithContent; use nu_test_support::fs::Stub::FileWithContentToBeTrimmed; @@ -248,14 +250,13 @@ fn errors_if_file_not_found() { // // This seems to be not directly affected by localization compared to the OS // provided error message - let expected = "File not found"; - assert!( - actual.err.contains(expected), - "Error:\n{}\ndoes not contain{}", - actual.err, - expected - ); + assert!(actual.err.contains("nu::shell::io::not_found")); + assert!(actual.err.contains( + &PathBuf::from_iter(["tests", "fixtures", "formats", "i_dont_exist.txt"]) + .display() + .to_string() + )); } #[test] diff --git a/crates/nu-command/tests/commands/path/self_.rs b/crates/nu-command/tests/commands/path/self_.rs index b0c47195c8..6a70f72c77 100644 --- a/crates/nu-command/tests/commands/path/self_.rs +++ b/crates/nu-command/tests/commands/path/self_.rs @@ -60,5 +60,5 @@ fn self_path_runtime() { fn self_path_repl() { let actual = nu!("const foo = path self; $foo"); assert!(!actual.status.success()); - assert!(actual.err.contains("nu::shell::file_not_found")); + assert!(actual.err.contains("nu::shell::io::not_found")); } diff --git a/crates/nu-command/tests/commands/rm.rs b/crates/nu-command/tests/commands/rm.rs index fdd7258bd4..3bfc093bd0 100644 --- a/crates/nu-command/tests/commands/rm.rs +++ b/crates/nu-command/tests/commands/rm.rs @@ -454,14 +454,8 @@ fn rm_prints_filenames_on_error() { assert!(files_exist_at(&file_names, test_dir)); for file_name in file_names { - let path = test_dir.join(file_name); - let substr = format!("Could not delete {}", path.to_string_lossy()); - assert!( - actual.err.contains(&substr), - "Matching: {}\n=== Command stderr:\n{}\n=== End stderr", - substr, - actual.err - ); + assert!(actual.err.contains("nu::shell::io::permission_denied")); + assert!(actual.err.contains(file_name)); } }); } diff --git a/crates/nu-command/tests/commands/save.rs b/crates/nu-command/tests/commands/save.rs index 8c2ea535b7..e37985eb38 100644 --- a/crates/nu-command/tests/commands/save.rs +++ b/crates/nu-command/tests/commands/save.rs @@ -532,5 +532,5 @@ fn force_save_to_dir() { "aaa" | save -f .. "#); - assert!(actual.err.contains("Is a directory")); + assert!(actual.err.contains("nu::shell::io::is_a_directory")); } diff --git a/crates/nu-engine/src/command_prelude.rs b/crates/nu-engine/src/command_prelude.rs index 0c5c250a27..9b9ed40477 100644 --- a/crates/nu-engine/src/command_prelude.rs +++ b/crates/nu-engine/src/command_prelude.rs @@ -2,7 +2,9 @@ pub use crate::CallExt; pub use nu_protocol::{ ast::CellPath, engine::{Call, Command, EngineState, Stack, StateWorkingSet}, - record, ByteStream, ByteStreamType, Category, ErrSpan, Example, IntoInterruptiblePipelineData, + record, + shell_error::io::IoError, + ByteStream, ByteStreamType, Category, ErrSpan, Example, IntoInterruptiblePipelineData, IntoPipelineData, IntoSpanned, IntoValue, PipelineData, Record, ShellError, Signature, Span, Spanned, SyntaxShape, Type, Value, }; diff --git a/crates/nu-engine/src/env.rs b/crates/nu-engine/src/env.rs index 345d81d6f7..0242d0b6b0 100644 --- a/crates/nu-engine/src/env.rs +++ b/crates/nu-engine/src/env.rs @@ -3,6 +3,7 @@ use nu_path::canonicalize_with; use nu_protocol::{ ast::Expr, engine::{Call, EngineState, Stack, StateWorkingSet}, + shell_error::io::IoError, ShellError, Span, Type, Value, VarId, }; use std::{ @@ -218,9 +219,12 @@ pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result Result, path: &Value, append: bool) -> Result { let no_glob_for_pattern = matches!(pattern.item, NuGlob::DoNotExpand(_)); + let pattern_span = pattern.span; let (prefix, pattern) = if nu_glob::is_glob(pattern.item.as_ref()) { // Pattern contains glob, split it let mut p = PathBuf::new(); @@ -80,22 +80,7 @@ pub fn glob_from( } Ok(p) => p, Err(err) => { - return match err.kind() { - ErrorKind::PermissionDenied => Err(ShellError::GenericError { - error: "Permission denied".into(), - msg: err.to_string(), - span: None, - help: None, - inner: vec![], - }), - // Previously, all these errors were treated as "directory not found." - // Now, permission denied errors are handled separately. - // TODO: Refine handling of I/O errors for more precise responses. - _ => Err(ShellError::DirectoryNotFound { - dir: path.to_string_lossy().to_string(), - span: pattern.span, - }), - }; + return Err(IoError::new(err.kind(), pattern_span, path).into()); } }; (path.parent().map(|parent| parent.to_path_buf()), path) diff --git a/crates/nu-explore/src/nu_common/command.rs b/crates/nu-explore/src/nu_common/command.rs index ef0157ac67..a41e7cf45b 100644 --- a/crates/nu-explore/src/nu_common/command.rs +++ b/crates/nu-explore/src/nu_common/command.rs @@ -14,16 +14,24 @@ pub fn run_command_with_value( stack: &mut Stack, ) -> Result { if is_ignored_command(command) { - return Err(ShellError::IOError { - msg: String::from("the command is ignored"), + return Err(ShellError::GenericError { + error: "Command ignored".to_string(), + msg: "the command is ignored".to_string(), + span: None, + help: None, + inner: vec![], }); } let pipeline = PipelineData::Value(input.clone(), None); let pipeline = run_nu_command(engine_state, stack, command, pipeline)?; if let PipelineData::Value(Value::Error { error, .. }, ..) = pipeline { - Err(ShellError::IOError { + Err(ShellError::GenericError { + error: "Error from pipeline".to_string(), msg: error.to_string(), + span: None, + help: None, + inner: vec![*error], }) } else { Ok(pipeline) @@ -69,8 +77,12 @@ fn eval_source2( ); if let Some(err) = working_set.parse_errors.first() { - return Err(ShellError::IOError { + return Err(ShellError::GenericError { + error: "Parse error".to_string(), msg: err.to_string(), + span: None, + help: None, + inner: vec![], }); } @@ -79,8 +91,12 @@ fn eval_source2( // We need to merge different info other wise things like PIPEs etc will not work. if let Err(err) = engine_state.merge_delta(delta) { - return Err(ShellError::IOError { + return Err(ShellError::GenericError { + error: "Merge error".to_string(), msg: err.to_string(), + span: None, + help: None, + inner: vec![err], }); } diff --git a/crates/nu-plugin-core/src/communication_mode/mod.rs b/crates/nu-plugin-core/src/communication_mode/mod.rs index 576cfe866c..19d89363bd 100644 --- a/crates/nu-plugin-core/src/communication_mode/mod.rs +++ b/crates/nu-plugin-core/src/communication_mode/mod.rs @@ -2,7 +2,8 @@ use std::ffi::OsStr; use std::io::{Stdin, Stdout}; use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio}; -use nu_protocol::ShellError; +use nu_protocol::shell_error::io::IoError; +use nu_protocol::{ShellError, Span}; #[cfg(feature = "local-socket")] mod local_socket; @@ -84,8 +85,15 @@ impl CommunicationMode { let listener = interpret_local_socket_name(name) .and_then(|name| ListenerOptions::new().name(name).create_sync()) - .map_err(|err| ShellError::IOError { - msg: format!("failed to open socket for plugin: {err}"), + .map_err(|err| { + IoError::new_internal( + err.kind(), + format!( + "Could not interpret local socket name {:?}", + name.to_string_lossy() + ), + nu_protocol::location!(), + ) })?; Ok(PreparedServerCommunication::LocalSocket { listener }) } @@ -107,8 +115,15 @@ impl CommunicationMode { interpret_local_socket_name(name) .and_then(|name| ls::Stream::connect(name)) - .map_err(|err| ShellError::IOError { - msg: format!("failed to connect to socket: {err}"), + .map_err(|err| { + ShellError::Io(IoError::new_internal( + err.kind(), + format!( + "Could not interpret local socket name {:?}", + name.to_string_lossy() + ), + nu_protocol::location!(), + )) }) }; // Reverse order from the server: read in, write out @@ -171,7 +186,16 @@ impl PreparedServerCommunication { // output) and one for write (the plugin input) // // Be non-blocking on Accept only, so we can timeout. - listener.set_nonblocking(ListenerNonblockingMode::Accept)?; + listener + .set_nonblocking(ListenerNonblockingMode::Accept) + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not set non-blocking mode accept for listener", + ) + })?; let mut get_socket = || { let mut result = None; while let Ok(None) = child.try_wait() { @@ -179,7 +203,14 @@ impl PreparedServerCommunication { Ok(stream) => { // Success! Ensure the stream is in nonblocking mode though, for // good measure. Had an issue without this on macOS. - stream.set_nonblocking(false)?; + stream.set_nonblocking(false).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not disable non-blocking mode for listener", + ) + })?; result = Some(stream); break; } @@ -187,7 +218,11 @@ impl PreparedServerCommunication { if !is_would_block_err(&err) { // `WouldBlock` is ok, just means it's not ready yet, but some other // kind of error should be reported - return Err(err.into()); + return Err(ShellError::Io(IoError::new( + err.kind(), + Span::unknown(), + None, + ))); } } } diff --git a/crates/nu-plugin-core/src/interface/mod.rs b/crates/nu-plugin-core/src/interface/mod.rs index de04bd6387..01f91b7c1c 100644 --- a/crates/nu-plugin-core/src/interface/mod.rs +++ b/crates/nu-plugin-core/src/interface/mod.rs @@ -2,8 +2,8 @@ use nu_plugin_protocol::{ByteStreamInfo, ListStreamInfo, PipelineDataHeader, StreamMessage}; use nu_protocol::{ - engine::Sequence, ByteStream, IntoSpanned, ListStream, PipelineData, Reader, ShellError, - Signals, + engine::Sequence, shell_error::io::IoError, ByteStream, ListStream, PipelineData, Reader, + ShellError, Signals, Span, }; use std::{ io::{Read, Write}, @@ -80,8 +80,12 @@ where } fn flush(&self) -> Result<(), ShellError> { - self.0.lock().flush().map_err(|err| ShellError::IOError { - msg: err.to_string(), + self.0.lock().flush().map_err(|err| { + ShellError::Io(IoError::new_internal( + err.kind(), + "PluginWrite could not flush", + nu_protocol::location!(), + )) }) } @@ -106,8 +110,12 @@ where let mut lock = self.0.lock().map_err(|_| ShellError::NushellFailed { msg: "writer mutex poisoned".into(), })?; - lock.flush().map_err(|err| ShellError::IOError { - msg: err.to_string(), + lock.flush().map_err(|err| { + ShellError::Io(IoError::new_internal( + err.kind(), + "PluginWrite could not flush", + nu_protocol::location!(), + )) }) } } @@ -332,7 +340,7 @@ where writer.write_all(std::iter::from_fn(move || match reader.read(buf) { Ok(0) => None, Ok(len) => Some(Ok(buf[..len].to_vec())), - Err(err) => Some(Err(ShellError::from(err.into_spanned(span)))), + Err(err) => Some(Err(ShellError::from(IoError::new(err.kind(), span, None)))), }))?; Ok(()) } @@ -357,6 +365,14 @@ where log::warn!("Error while writing pipeline in background: {err}"); } result + }) + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not spawn plugin stream background writer", + ) })?, )), } diff --git a/crates/nu-plugin-core/src/interface/tests.rs b/crates/nu-plugin-core/src/interface/tests.rs index 6b4ad85d7f..28093ecd62 100644 --- a/crates/nu-plugin-core/src/interface/tests.rs +++ b/crates/nu-plugin-core/src/interface/tests.rs @@ -8,8 +8,8 @@ use nu_plugin_protocol::{ StreamMessage, }; use nu_protocol::{ - engine::Sequence, ByteStream, ByteStreamSource, ByteStreamType, DataSource, ListStream, - PipelineData, PipelineMetadata, ShellError, Signals, Span, Value, + engine::Sequence, shell_error::io::IoError, ByteStream, ByteStreamSource, ByteStreamType, + DataSource, ListStream, PipelineData, PipelineMetadata, ShellError, Signals, Span, Value, }; use std::{path::Path, sync::Arc}; @@ -245,7 +245,8 @@ fn read_pipeline_data_byte_stream() -> Result<(), ShellError> { match stream.into_source() { ByteStreamSource::Read(mut read) => { let mut buf = Vec::new(); - read.read_to_end(&mut buf)?; + read.read_to_end(&mut buf) + .map_err(|err| IoError::new(err.kind(), test_span, None))?; let iter = buf.chunks_exact(out_pattern.len()); assert_eq!(iter.len(), iterations); for chunk in iter { diff --git a/crates/nu-plugin-core/src/serializers/json.rs b/crates/nu-plugin-core/src/serializers/json.rs index 7dc868b806..8dfadac2d0 100644 --- a/crates/nu-plugin-core/src/serializers/json.rs +++ b/crates/nu-plugin-core/src/serializers/json.rs @@ -1,5 +1,5 @@ use nu_plugin_protocol::{PluginInput, PluginOutput}; -use nu_protocol::ShellError; +use nu_protocol::{location, shell_error::io::IoError, ShellError}; use serde::Deserialize; use crate::{Encoder, PluginEncoder}; @@ -26,8 +26,12 @@ impl Encoder for JsonSerializer { writer: &mut impl std::io::Write, ) -> Result<(), nu_protocol::ShellError> { serde_json::to_writer(&mut *writer, plugin_input).map_err(json_encode_err)?; - writer.write_all(b"\n").map_err(|err| ShellError::IOError { - msg: err.to_string(), + writer.write_all(b"\n").map_err(|err| { + ShellError::Io(IoError::new_internal( + err.kind(), + "Failed to write final line break", + location!(), + )) }) } @@ -49,8 +53,12 @@ impl Encoder for JsonSerializer { writer: &mut impl std::io::Write, ) -> Result<(), ShellError> { serde_json::to_writer(&mut *writer, plugin_output).map_err(json_encode_err)?; - writer.write_all(b"\n").map_err(|err| ShellError::IOError { - msg: err.to_string(), + writer.write_all(b"\n").map_err(|err| { + ShellError::Io(IoError::new_internal( + err.kind(), + "JsonSerializer could not encode linebreak", + nu_protocol::location!(), + )) }) } @@ -68,9 +76,11 @@ impl Encoder for JsonSerializer { /// Handle a `serde_json` encode error. fn json_encode_err(err: serde_json::Error) -> ShellError { if err.is_io() { - ShellError::IOError { - msg: err.to_string(), - } + ShellError::Io(IoError::new_internal( + err.io_error_kind().expect("is io"), + "Could not encode with json", + nu_protocol::location!(), + )) } else { ShellError::PluginFailedToEncode { msg: err.to_string(), @@ -83,9 +93,11 @@ fn json_decode_err(err: serde_json::Error) -> Result, ShellError> { if err.is_eof() { Ok(None) } else if err.is_io() { - Err(ShellError::IOError { - msg: err.to_string(), - }) + Err(ShellError::Io(IoError::new_internal( + err.io_error_kind().expect("is io"), + "Could not decode with json", + nu_protocol::location!(), + ))) } else { Err(ShellError::PluginFailedToDecode { msg: err.to_string(), diff --git a/crates/nu-plugin-core/src/serializers/msgpack.rs b/crates/nu-plugin-core/src/serializers/msgpack.rs index bf136fd790..372511631c 100644 --- a/crates/nu-plugin-core/src/serializers/msgpack.rs +++ b/crates/nu-plugin-core/src/serializers/msgpack.rs @@ -1,7 +1,7 @@ use std::io::ErrorKind; use nu_plugin_protocol::{PluginInput, PluginOutput}; -use nu_protocol::ShellError; +use nu_protocol::{shell_error::io::IoError, ShellError}; use serde::Deserialize; use crate::{Encoder, PluginEncoder}; @@ -64,9 +64,12 @@ fn rmp_encode_err(err: rmp_serde::encode::Error) -> ShellError { match err { rmp_serde::encode::Error::InvalidValueWrite(_) => { // I/O error - ShellError::IOError { - msg: err.to_string(), - } + ShellError::Io(IoError::new_internal( + // TODO: get a better kind here + std::io::ErrorKind::Other, + "Could not encode with rmp", + nu_protocol::location!(), + )) } _ => { // Something else @@ -87,9 +90,12 @@ fn rmp_decode_err(err: rmp_serde::decode::Error) -> Result, ShellEr Ok(None) } else { // I/O error - Err(ShellError::IOError { - msg: err.to_string(), - }) + Err(ShellError::Io(IoError::new_internal( + // TODO: get a better kind here + std::io::ErrorKind::Other, + "Could not decode with rmp", + nu_protocol::location!(), + ))) } } _ => { diff --git a/crates/nu-plugin-core/src/serializers/tests.rs b/crates/nu-plugin-core/src/serializers/tests.rs index b27f852ee4..76503fa96e 100644 --- a/crates/nu-plugin-core/src/serializers/tests.rs +++ b/crates/nu-plugin-core/src/serializers/tests.rs @@ -36,18 +36,18 @@ macro_rules! generate_tests { let mut buffered = std::io::BufReader::new(ErrorProducer); match Encoder::::decode(&encoder, &mut buffered) { Ok(_) => panic!("decode: i/o error was not passed through"), - Err(ShellError::IOError { .. }) => (), // okay + Err(ShellError::Io(_)) => (), // okay Err(other) => panic!( "decode: got other error, should have been a \ - ShellError::IOError: {other:?}" + ShellError::Io: {other:?}" ), } match Encoder::::decode(&encoder, &mut buffered) { Ok(_) => panic!("decode: i/o error was not passed through"), - Err(ShellError::IOError { .. }) => (), // okay + Err(ShellError::Io(_)) => (), // okay Err(other) => panic!( "decode: got other error, should have been a \ - ShellError::IOError: {other:?}" + ShellError::Io: {other:?}" ), } } @@ -378,9 +378,11 @@ macro_rules! generate_tests { .with_url("https://example.org/test/error") .with_help("some help") .with_label("msg", Span::new(2, 30)) - .with_inner(ShellError::IOError { - msg: "io error".into(), - }); + .with_inner(ShellError::Io(IoError::new( + std::io::ErrorKind::NotFound, + Span::test_data(), + None, + ))); let response = PluginCallResponse::Error(error.clone()); let output = PluginOutput::CallResponse(6, response); diff --git a/crates/nu-plugin-engine/src/interface/mod.rs b/crates/nu-plugin-engine/src/interface/mod.rs index 93d75d0ec9..9eebe6aaed 100644 --- a/crates/nu-plugin-engine/src/interface/mod.rs +++ b/crates/nu-plugin-engine/src/interface/mod.rs @@ -381,8 +381,13 @@ impl PluginInterfaceManager { // don't block this.state.writer.write(&PluginInput::EngineCallResponse( engine_call_id, - EngineCallResponse::Error(ShellError::IOError { - msg: "Can't make engine call because the original caller hung up".into(), + EngineCallResponse::Error(ShellError::GenericError { + error: "Caller hung up".to_string(), + msg: "Can't make engine call because the original caller hung up" + .to_string(), + span: None, + help: None, + inner: vec![], }), ))?; this.state.writer.flush() diff --git a/crates/nu-plugin-engine/src/interface/tests.rs b/crates/nu-plugin-engine/src/interface/tests.rs index 4761da6824..de9f379305 100644 --- a/crates/nu-plugin-engine/src/interface/tests.rs +++ b/crates/nu-plugin-engine/src/interface/tests.rs @@ -6,6 +6,7 @@ use crate::{ plugin_custom_value_with_source::WithSource, test_util::*, PluginCustomValueWithSource, PluginSource, }; +use nu_engine::command_prelude::IoError; use nu_plugin_core::{interface_test_util::TestCase, Interface, InterfaceManager}; use nu_plugin_protocol::{ test_util::{expected_test_custom_value, test_plugin_custom_value}, @@ -86,9 +87,12 @@ fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Resul } fn test_io_error() -> ShellError { - ShellError::IOError { - msg: "test io error".into(), - } + ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::Other, + Span::test_data(), + None, + "test io error", + )) } fn check_test_io_error(error: &ShellError) { diff --git a/crates/nu-plugin-engine/src/persistent.rs b/crates/nu-plugin-engine/src/persistent.rs index e664b37f87..8d444a4d1c 100644 --- a/crates/nu-plugin-engine/src/persistent.rs +++ b/crates/nu-plugin-engine/src/persistent.rs @@ -7,8 +7,9 @@ use super::{PluginInterface, PluginSource}; use nu_plugin_core::CommunicationMode; use nu_protocol::{ engine::{EngineState, Stack}, + shell_error::io::IoError, HandlerGuard, Handlers, PluginGcConfig, PluginIdentity, PluginMetadata, RegisteredPlugin, - ShellError, + ShellError, Span, }; use std::{ collections::HashMap, @@ -184,7 +185,14 @@ impl PersistentPlugin { })?; // Start the plugin garbage collector - let gc = PluginGc::new(mutable.gc_config.clone(), &self)?; + let gc = PluginGc::new(mutable.gc_config.clone(), &self).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not start plugin gc", + ) + })?; let pid = child.id(); let interface = make_plugin_interface( diff --git a/crates/nu-plugin-test-support/src/spawn_fake_plugin.rs b/crates/nu-plugin-test-support/src/spawn_fake_plugin.rs index d954f6e683..710332fbec 100644 --- a/crates/nu-plugin-test-support/src/spawn_fake_plugin.rs +++ b/crates/nu-plugin-test-support/src/spawn_fake_plugin.rs @@ -4,7 +4,7 @@ use nu_plugin::Plugin; use nu_plugin_core::{InterfaceManager, PluginRead, PluginWrite}; use nu_plugin_engine::{PluginInterfaceManager, PluginSource}; use nu_plugin_protocol::{PluginInput, PluginOutput}; -use nu_protocol::{PluginIdentity, ShellError}; +use nu_protocol::{shell_error::io::IoError, PluginIdentity, ShellError}; use crate::fake_persistent_plugin::FakePersistentPlugin; @@ -21,8 +21,12 @@ impl PluginWrite for FakePluginWrite { fn write(&self, data: &T) -> Result<(), ShellError> { self.0 .send(data.clone()) - .map_err(|err| ShellError::IOError { - msg: err.to_string(), + .map_err(|e| ShellError::GenericError { + error: "Error sending data".to_string(), + msg: e.to_string(), + span: None, + help: None, + inner: vec![], }) } @@ -59,7 +63,14 @@ pub(crate) fn spawn_fake_plugin( // Start the interface reader on another thread std::thread::Builder::new() .name(format!("fake plugin interface reader ({name})")) - .spawn(move || manager.consume_all(output_read).expect("Plugin read error"))?; + .spawn(move || manager.consume_all(output_read).expect("Plugin read error")) + .map_err(|err| { + IoError::new_internal( + err.kind(), + format!("Could not spawn fake plugin interface reader ({name})"), + nu_protocol::location!(), + ) + })?; // Start the plugin on another thread let name_string = name.to_owned(); @@ -73,6 +84,13 @@ pub(crate) fn spawn_fake_plugin( move || output_write, ) .expect("Plugin runner error") + }) + .map_err(|err| { + IoError::new_internal( + err.kind(), + format!("Could not spawn fake plugin runner ({name})"), + nu_protocol::location!(), + ) })?; Ok(reg_plugin) diff --git a/crates/nu-plugin/src/plugin/interface/mod.rs b/crates/nu-plugin/src/plugin/interface/mod.rs index be7b2e0dfc..c3052448b1 100644 --- a/crates/nu-plugin/src/plugin/interface/mod.rs +++ b/crates/nu-plugin/src/plugin/interface/mod.rs @@ -1046,8 +1046,12 @@ impl ForegroundGuard { { use nix::unistd::{setpgid, Pid}; // This should always succeed, frankly, but handle the error just in case - setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(|err| ShellError::IOError { - msg: err.to_string(), + setpgid(Pid::from_raw(0), Pid::from_raw(0)).map_err(|err| { + nu_protocol::shell_error::io::IoError::new_internal( + std::io::Error::from(err).kind(), + "Could not set pgid", + nu_protocol::location!(), + ) })?; } interface.leave_foreground()?; diff --git a/crates/nu-plugin/src/plugin/interface/tests.rs b/crates/nu-plugin/src/plugin/interface/tests.rs index 5ff9f6f6cd..b6bf267dcb 100644 --- a/crates/nu-plugin/src/plugin/interface/tests.rs +++ b/crates/nu-plugin/src/plugin/interface/tests.rs @@ -1,6 +1,7 @@ use crate::test_util::TestCaseExt; use super::{EngineInterfaceManager, ReceivedPluginCall}; +use nu_engine::command_prelude::IoError; use nu_plugin_core::{interface_test_util::TestCase, Interface, InterfaceManager}; use nu_plugin_protocol::{ test_util::{expected_test_custom_value, test_plugin_custom_value, TestCustomValue}, @@ -88,9 +89,12 @@ fn manager_consume_all_exits_after_streams_and_interfaces_are_dropped() -> Resul } fn test_io_error() -> ShellError { - ShellError::IOError { - msg: "test io error".into(), - } + ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::Other, + Span::test_data(), + None, + "test io error", + )) } fn check_test_io_error(error: &ShellError) { diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index c5af681f5e..d01a0e2600 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -387,7 +387,7 @@ pub enum ServePluginError { impl From for ServePluginError { fn from(error: ShellError) -> Self { match error { - ShellError::IOError { .. } => ServePluginError::IOError(error), + ShellError::Io(_) => ServePluginError::IOError(error), ShellError::PluginFailedToLoad { .. } => ServePluginError::Incompatible(error), _ => ServePluginError::UnreportedError(error), } diff --git a/crates/nu-protocol/src/engine/engine_state.rs b/crates/nu-protocol/src/engine/engine_state.rs index 8270e4111a..dbded4eb63 100644 --- a/crates/nu-protocol/src/engine/engine_state.rs +++ b/crates/nu-protocol/src/engine/engine_state.rs @@ -7,6 +7,7 @@ use crate::{ Variable, Visibility, DEFAULT_OVERLAY_NAME, }, eval_const::create_nu_constant, + shell_error::io::IoError, BlockId, Category, Config, DeclId, FileId, GetSpan, Handlers, HistoryConfig, Module, ModuleId, OverlayId, ShellError, SignalAction, Signals, Signature, Span, SpanId, Type, Value, VarId, VirtualPathId, @@ -322,8 +323,14 @@ impl EngineState { } let cwd = self.cwd(Some(stack))?; - // TODO: better error - std::env::set_current_dir(cwd)?; + std::env::set_current_dir(cwd).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Could not set current dir", + ) + })?; if let Some(config) = stack.config.take() { // If config was updated in the stack, replace it. @@ -514,13 +521,12 @@ impl EngineState { if err.kind() == std::io::ErrorKind::NotFound { Ok(PluginRegistryFile::default()) } else { - Err(ShellError::GenericError { - error: "Failed to open plugin file".into(), - msg: "".into(), - span: None, - help: None, - inner: vec![err.into()], - }) + Err(ShellError::Io(IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + PathBuf::from(plugin_path), + "Failed to open plugin file", + ))) } } }?; @@ -531,14 +537,14 @@ impl EngineState { } // Write it to the same path - let plugin_file = - File::create(plugin_path.as_path()).map_err(|err| ShellError::GenericError { - error: "Failed to write plugin file".into(), - msg: "".into(), - span: None, - help: None, - inner: vec![err.into()], - })?; + let plugin_file = File::create(plugin_path.as_path()).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + PathBuf::from(plugin_path), + "Failed to write plugin file", + ) + })?; contents.write_to(plugin_file, None) } diff --git a/crates/nu-protocol/src/errors/labeled_error.rs b/crates/nu-protocol/src/errors/labeled_error.rs index cdd990463b..7104a1a8b1 100644 --- a/crates/nu-protocol/src/errors/labeled_error.rs +++ b/crates/nu-protocol/src/errors/labeled_error.rs @@ -143,8 +143,16 @@ impl LabeledError { /// [`ShellError`] implements `miette::Diagnostic`: /// /// ```rust - /// # use nu_protocol::{ShellError, LabeledError}; - /// let error = LabeledError::from_diagnostic(&ShellError::IOError { msg: "error".into() }); + /// # use nu_protocol::{ShellError, LabeledError, shell_error::io::IoError, Span}; + /// # + /// let error = LabeledError::from_diagnostic( + /// &ShellError::Io(IoError::new_with_additional_context( + /// std::io::ErrorKind::Other, + /// Span::test_data(), + /// None, + /// "some error" + /// )) + /// ); /// assert!(error.to_string().contains("I/O error")); /// ``` pub fn from_diagnostic(diag: &(impl miette::Diagnostic + ?Sized)) -> LabeledError { diff --git a/crates/nu-protocol/src/errors/mod.rs b/crates/nu-protocol/src/errors/mod.rs index 2e264066a9..5f385a21ee 100644 --- a/crates/nu-protocol/src/errors/mod.rs +++ b/crates/nu-protocol/src/errors/mod.rs @@ -4,7 +4,7 @@ mod config_error; mod labeled_error; mod parse_error; mod parse_warning; -mod shell_error; +pub mod shell_error; pub use cli_error::{ format_shell_error, report_parse_error, report_parse_warning, report_shell_error, @@ -15,4 +15,4 @@ pub use config_error::ConfigError; pub use labeled_error::{ErrorLabel, LabeledError}; pub use parse_error::{DidYouMean, ParseError}; pub use parse_warning::ParseWarning; -pub use shell_error::*; +pub use shell_error::ShellError; diff --git a/crates/nu-protocol/src/errors/shell_error/bridge.rs b/crates/nu-protocol/src/errors/shell_error/bridge.rs new file mode 100644 index 0000000000..eff8ba17e4 --- /dev/null +++ b/crates/nu-protocol/src/errors/shell_error/bridge.rs @@ -0,0 +1,47 @@ +use super::ShellError; +use serde::{Deserialize, Serialize}; +use thiserror::Error; + +/// A bridge for transferring a [`ShellError`] between Nushell or similar processes. +/// +/// This newtype encapsulates a [`ShellError`] to facilitate its transfer between Nushell processes +/// or processes with similar behavior. +/// By defining this type, we eliminate ambiguity about what is being transferred and avoid the +/// need to implement [`From`](From) and [`Into`](Into) directly on +/// `ShellError`. +#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)] +#[error("{0}")] +pub struct ShellErrorBridge(pub ShellError); + +impl TryFrom for ShellErrorBridge { + type Error = std::io::Error; + + fn try_from(value: std::io::Error) -> Result { + let kind = value.kind(); + value + .downcast() + .inspect(|_| debug_assert_eq!(kind, std::io::ErrorKind::Other)) + } +} + +impl From for std::io::Error { + fn from(value: ShellErrorBridge) -> Self { + std::io::Error::other(value) + } +} + +#[test] +fn test_bridge_io_error_roundtrip() { + let shell_error = ShellError::GenericError { + error: "some error".into(), + msg: "some message".into(), + span: None, + help: None, + inner: vec![], + }; + + let bridge = ShellErrorBridge(shell_error); + let io_error = std::io::Error::from(bridge.clone()); + let bridge_again = ShellErrorBridge::try_from(io_error).unwrap(); + assert_eq!(bridge.0, bridge_again.0); +} diff --git a/crates/nu-protocol/src/errors/shell_error/io.rs b/crates/nu-protocol/src/errors/shell_error/io.rs new file mode 100644 index 0000000000..87eaabc74d --- /dev/null +++ b/crates/nu-protocol/src/errors/shell_error/io.rs @@ -0,0 +1,418 @@ +use miette::{Diagnostic, LabeledSpan, SourceSpan}; +use std::{ + fmt::Display, + path::{Path, PathBuf}, +}; +use thiserror::Error; + +use crate::Span; + +use super::{location::Location, ShellError}; + +/// Represents an I/O error in the [`ShellError::Io`] variant. +/// +/// This is the central I/O error for the [`ShellError::Io`] variant. +/// It represents all I/O errors by encapsulating [`ErrorKind`], an extension of +/// [`std::io::ErrorKind`]. +/// The `span` indicates where the error occurred in user-provided code. +/// If the error is not tied to user-provided code, the `location` refers to the precise point in +/// the Rust code where the error originated. +/// The optional `path` provides the file or directory involved in the error. +/// If [`ErrorKind`] alone doesn't provide enough detail, additional context can be added to clarify +/// the issue. +/// +/// For handling user input errors (e.g., commands), prefer using [`new`](Self::new). +/// Alternatively, use the [`factory`](Self::factory) method to simplify error creation in repeated +/// contexts. +/// For internal errors, use [`new_internal`](Self::new_internal) to include the location in Rust +/// code where the error originated. +/// +/// # Examples +/// +/// ## User Input Error +/// ```rust +/// # use nu_protocol::shell_error::io::{IoError, ErrorKind}; +/// # use nu_protocol::Span; +/// use std::path::PathBuf; +/// +/// # let span = Span::test_data(); +/// let path = PathBuf::from("/some/missing/file"); +/// let error = IoError::new( +/// std::io::ErrorKind::NotFound, +/// span, +/// path +/// ); +/// println!("Error: {:?}", error); +/// ``` +/// +/// ## Internal Error +/// ```rust +/// # use nu_protocol::shell_error::io::{IoError, ErrorKind}; +// # +/// let error = IoError::new_internal( +/// std::io::ErrorKind::UnexpectedEof, +/// "Failed to read data from buffer", +/// nu_protocol::location!() +/// ); +/// println!("Error: {:?}", error); +/// ``` +/// +/// ## Using the Factory Method +/// ```rust +/// # use nu_protocol::shell_error::io::{IoError, ErrorKind}; +/// # use nu_protocol::{Span, ShellError}; +/// use std::path::PathBuf; +/// +/// # fn should_return_err() -> Result<(), ShellError> { +/// # let span = Span::new(50, 60); +/// let path = PathBuf::from("/some/file"); +/// let from_io_error = IoError::factory(span, Some(path.as_path())); +/// +/// let content = std::fs::read_to_string(&path).map_err(from_io_error)?; +/// # Ok(()) +/// # } +/// # +/// # assert!(should_return_err().is_err()); +/// ``` +/// +/// # ShellErrorBridge +/// +/// The [`ShellErrorBridge`](super::bridge::ShellErrorBridge) struct is used to contain a +/// [`ShellError`] inside a [`std::io::Error`]. +/// This allows seamless transfer of `ShellError` instances where `std::io::Error` is expected. +/// When a `ShellError` needs to be packed into an I/O context, use this bridge. +/// Similarly, when handling an I/O error that is expected to contain a `ShellError`, +/// use the bridge to unpack it. +/// +/// This approach ensures clarity about where such container transfers occur. +/// All other I/O errors should be handled using the provided constructors for `IoError`. +/// This way, the code explicitly indicates when and where a `ShellError` transfer might happen. +#[derive(Debug, Clone, Error, PartialEq)] +#[non_exhaustive] +#[error("I/O error")] +pub struct IoError { + /// The type of the underlying I/O error. + /// + /// [`std::io::ErrorKind`] provides detailed context about the type of I/O error that occurred + /// and is part of [`std::io::Error`]. + /// If a kind cannot be represented by it, consider adding a new variant to [`ErrorKind`]. + /// + /// Only in very rare cases should [`std::io::ErrorKind::Other`] be used, make sure you provide + /// `additional_context` to get useful errors in these cases. + pub kind: ErrorKind, + + /// The source location of the error. + pub span: Span, + + /// The path related to the I/O error, if applicable. + /// + /// Many I/O errors involve a file or directory path, but operating system error messages + /// often don't include the specific path. + /// Setting this to [`Some`] allows users to see which path caused the error. + pub path: Option, + + /// Additional details to provide more context about the error. + /// + /// Only set this field if it adds meaningful context. + /// If [`ErrorKind`] already contains all the necessary information, leave this as [`None`]. + pub additional_context: Option, + + /// The precise location in the Rust code where the error originated. + /// + /// This field is particularly useful for debugging errors that stem from the Rust + /// implementation rather than user-provided Nushell code. + /// The original [`Location`] is converted to a string to more easily report the error + /// attributing the location. + /// + /// This value is only used if `span` is [`Span::unknown()`] as most of the time we want to + /// refer to user code than the Rust code. + pub location: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Diagnostic)] +pub enum ErrorKind { + Std(std::io::ErrorKind), + // TODO: in Rust 1.83 this can be std::io::ErrorKind::NotADirectory + NotADirectory, + NotAFile, + // TODO: in Rust 1.83 this can be std::io::ErrorKind::IsADirectory + IsADirectory, +} + +impl IoError { + /// Creates a new [`IoError`] with the given kind, span, and optional path. + /// + /// This constructor should be used in all cases where the combination of the error kind, span, + /// and path provides enough information to describe the error clearly. + /// For example, errors like "File not found" or "Permission denied" are typically + /// self-explanatory when paired with the file path and the location in user-provided + /// Nushell code (`span`). + /// + /// # Constraints + /// If `span` is unknown, use: + /// - `new_internal` if no path is available. + /// - `new_internal_with_path` if a path is available. + pub fn new(kind: impl Into, span: Span, path: impl Into>) -> Self { + let path = path.into(); + + if span == Span::unknown() { + debug_assert!( + path.is_some(), + "for unknown spans with paths, use `new_internal_with_path`" + ); + debug_assert!( + path.is_none(), + "for unknown spans without paths, use `new_internal`" + ); + } + + Self { + kind: kind.into(), + span, + path, + additional_context: None, + location: None, + } + } + + /// Creates a new [`IoError`] with additional context. + /// + /// Use this constructor when the error kind, span, and path are not sufficient to fully + /// explain the error, and additional context can provide meaningful details. + /// Avoid redundant context (e.g., "Permission denied" for an error kind of + /// [`ErrorKind::PermissionDenied`](std::io::ErrorKind::PermissionDenied)). + /// + /// # Constraints + /// If `span` is unknown, use: + /// - `new_internal` if no path is available. + /// - `new_internal_with_path` if a path is available. + pub fn new_with_additional_context( + kind: impl Into, + span: Span, + path: impl Into>, + additional_context: impl ToString, + ) -> Self { + let path = path.into(); + + if span == Span::unknown() { + debug_assert!( + path.is_some(), + "for unknown spans with paths, use `new_internal_with_path`" + ); + debug_assert!( + path.is_none(), + "for unknown spans without paths, use `new_internal`" + ); + } + + Self { + kind: kind.into(), + span, + path, + additional_context: Some(additional_context.to_string()), + location: None, + } + } + + /// Creates a new [`IoError`] for internal I/O errors without a user-provided span or path. + /// + /// This constructor is intended for internal errors in the Rust implementation that still need + /// to be reported to the end user. + /// Since these errors are not tied to user-provided Nushell code, they generally have no + /// meaningful span or path. + /// + /// Instead, these errors provide: + /// - `additional_context`: + /// Details about what went wrong internally. + /// - `location`: + /// The location in the Rust code where the error occurred, allowing us to trace and debug + /// the issue. + /// Use the [`nu_protocol::location!`](crate::location) macro to generate the location + /// information. + /// + /// # Examples + /// ```rust + /// use nu_protocol::shell_error::io::IoError; + /// + /// let error = IoError::new_internal( + /// std::io::ErrorKind::UnexpectedEof, + /// "Failed to read from buffer", + /// nu_protocol::location!(), + /// ); + /// ``` + pub fn new_internal( + kind: impl Into, + additional_context: impl ToString, + location: Location, + ) -> Self { + Self { + kind: kind.into(), + span: Span::unknown(), + path: None, + additional_context: Some(additional_context.to_string()), + location: Some(location.to_string()), + } + } + + /// Creates a new `IoError` for internal I/O errors with a specific path. + /// + /// This constructor is similar to [`new_internal`] but also includes a file or directory + /// path relevant to the error. Use this function in rare cases where an internal error + /// involves a specific path, and the combination of path and additional context is helpful. + /// + /// # Examples + /// ```rust + /// use std::path::PathBuf; + /// use nu_protocol::shell_error::io::IoError; + /// + /// let error = IoError::new_internal_with_path( + /// std::io::ErrorKind::NotFound, + /// "Could not find special file", + /// nu_protocol::location!(), + /// PathBuf::from("/some/file"), + /// ); + /// ``` + pub fn new_internal_with_path( + kind: impl Into, + additional_context: impl ToString, + location: Location, + path: PathBuf, + ) -> Self { + Self { + kind: kind.into(), + span: Span::unknown(), + path: path.into(), + additional_context: Some(additional_context.to_string()), + location: Some(location.to_string()), + } + } + + /// Creates a factory closure for constructing [`IoError`] instances from [`std::io::Error`] values. + /// + /// This method is particularly useful when you need to handle multiple I/O errors which all + /// take the same span and path. + /// Instead of calling `.map_err(|err| IoError::new(err.kind(), span, path))` every time, you + /// can create the factory closure once and pass that into `.map_err`. + pub fn factory<'p, P>(span: Span, path: P) -> impl Fn(std::io::Error) -> Self + use<'p, P> + where + P: Into>, + { + let path = path.into(); + move |err: std::io::Error| IoError::new(err.kind(), span, path.map(PathBuf::from)) + } +} + +impl Display for ErrorKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ErrorKind::Std(error_kind) => { + let msg = error_kind.to_string(); + let (first, rest) = msg.split_at(1); + write!(f, "{}{}", first.to_uppercase(), rest) + } + ErrorKind::NotADirectory => write!(f, "Not a directory"), + ErrorKind::NotAFile => write!(f, "Not a file"), + ErrorKind::IsADirectory => write!(f, "Is a directory"), + } + } +} + +impl std::error::Error for ErrorKind {} + +impl Diagnostic for IoError { + fn code<'a>(&'a self) -> Option> { + let mut code = String::from("nu::shell::io::"); + match self.kind { + ErrorKind::Std(error_kind) => match error_kind { + std::io::ErrorKind::NotFound => code.push_str("not_found"), + std::io::ErrorKind::PermissionDenied => code.push_str("permission_denied"), + std::io::ErrorKind::ConnectionRefused => code.push_str("connection_refused"), + std::io::ErrorKind::ConnectionReset => code.push_str("connection_reset"), + std::io::ErrorKind::ConnectionAborted => code.push_str("connection_aborted"), + std::io::ErrorKind::NotConnected => code.push_str("not_connected"), + std::io::ErrorKind::AddrInUse => code.push_str("addr_in_use"), + std::io::ErrorKind::AddrNotAvailable => code.push_str("addr_not_available"), + std::io::ErrorKind::BrokenPipe => code.push_str("broken_pipe"), + std::io::ErrorKind::AlreadyExists => code.push_str("already_exists"), + std::io::ErrorKind::WouldBlock => code.push_str("would_block"), + std::io::ErrorKind::InvalidInput => code.push_str("invalid_input"), + std::io::ErrorKind::InvalidData => code.push_str("invalid_data"), + std::io::ErrorKind::TimedOut => code.push_str("timed_out"), + std::io::ErrorKind::WriteZero => code.push_str("write_zero"), + std::io::ErrorKind::Interrupted => code.push_str("interrupted"), + std::io::ErrorKind::Unsupported => code.push_str("unsupported"), + std::io::ErrorKind::UnexpectedEof => code.push_str("unexpected_eof"), + std::io::ErrorKind::OutOfMemory => code.push_str("out_of_memory"), + std::io::ErrorKind::Other => code.push_str("other"), + kind => code.push_str(&kind.to_string().to_lowercase().replace(" ", "_")), + }, + ErrorKind::NotADirectory => code.push_str("not_a_directory"), + ErrorKind::NotAFile => code.push_str("not_a_file"), + ErrorKind::IsADirectory => code.push_str("is_a_directory"), + } + + Some(Box::new(code)) + } + + fn help<'a>(&'a self) -> Option> { + self.path + .as_ref() + .map(|path| format!("The error occurred at '{}'", path.display())) + .map(|s| Box::new(s) as Box) + } + + fn labels(&self) -> Option + '_>> { + let span_is_unknown = self.span == Span::unknown(); + let span = match (span_is_unknown, self.location.as_ref()) { + (true, None) => return None, + (false, _) => SourceSpan::from(self.span), + (true, Some(location)) => SourceSpan::new(0.into(), location.len()), + }; + + let label = match self.additional_context.as_ref() { + Some(ctx) => format!("{ctx}\n{}", self.kind), + None => self.kind.to_string(), + }; + let label = LabeledSpan::new_with_span(Some(label), span); + Some(Box::new(std::iter::once(label))) + } + + fn diagnostic_source(&self) -> Option<&dyn Diagnostic> { + Some(&self.kind as &dyn Diagnostic) + } + + fn source_code(&self) -> Option<&dyn miette::SourceCode> { + let span_is_unknown = self.span == Span::unknown(); + match (span_is_unknown, self.location.as_ref()) { + (true, None) | (false, _) => None, + (true, Some(location)) => Some(location as &dyn miette::SourceCode), + } + } +} + +impl From for ShellError { + fn from(value: IoError) -> Self { + ShellError::Io(value) + } +} + +impl From for std::io::Error { + fn from(value: IoError) -> Self { + Self::new(value.kind.into(), value) + } +} + +impl From for ErrorKind { + fn from(value: std::io::ErrorKind) -> Self { + ErrorKind::Std(value) + } +} + +impl From for std::io::ErrorKind { + fn from(value: ErrorKind) -> Self { + match value { + ErrorKind::Std(error_kind) => error_kind, + _ => std::io::ErrorKind::Other, + } + } +} diff --git a/crates/nu-protocol/src/errors/shell_error/location.rs b/crates/nu-protocol/src/errors/shell_error/location.rs new file mode 100644 index 0000000000..340f9ada61 --- /dev/null +++ b/crates/nu-protocol/src/errors/shell_error/location.rs @@ -0,0 +1,56 @@ +use thiserror::Error; + +/// Represents a specific location in the Rust code. +/// +/// This data structure is used to provide detailed information about where in the Rust code +/// an error occurred. +/// While most errors in [`ShellError`](super::ShellError) are related to user-provided Nushell +/// code, some originate from the underlying Rust implementation. +/// With this type, we can pinpoint the exact location of such errors, improving debugging +/// and error reporting. +#[derive(Debug, Clone, PartialEq, Eq, Error)] +#[error("{file}:{line}:{column}")] +pub struct Location { + file: &'static str, + line: u32, + column: u32, +} + +impl Location { + /// Internal constructor for [`Location`]. + /// + /// This function is not intended to be called directly. + /// Instead, use the [`location!`] macro to create instances. + #[doc(hidden)] + #[deprecated( + note = "This function is not meant to be called directly. Use `nu_protocol::location` instead." + )] + pub fn new(file: &'static str, line: u32, column: u32) -> Self { + Location { file, line, column } + } +} + +/// Macro to create a new [`Location`] for the exact position in your code. +/// +/// This macro captures the current file, line, and column during compilation, +/// providing an easy way to associate errors with specific locations in the Rust code. +/// +/// # Note +/// This macro relies on the [`file!`], [`line!`], and [`column!`] macros to fetch the +/// compilation context. +#[macro_export] +macro_rules! location { + () => {{ + #[allow(deprecated)] + $crate::shell_error::location::Location::new(file!(), line!(), column!()) + }}; +} + +#[test] +fn test_location_macro() { + let location = crate::location!(); + let line = line!() - 1; // Adjust for the macro call being on the previous line. + let file = file!(); + assert_eq!(location.line, line); + assert_eq!(location.file, file); +} diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error/mod.rs similarity index 87% rename from crates/nu-protocol/src/errors/shell_error.rs rename to crates/nu-protocol/src/errors/shell_error/mod.rs index 208f2178f6..6f651836ca 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error/mod.rs @@ -4,9 +4,13 @@ use crate::{ }; use miette::Diagnostic; use serde::{Deserialize, Serialize}; -use std::{io, num::NonZeroI32}; +use std::num::NonZeroI32; use thiserror::Error; +pub mod bridge; +pub mod io; +pub mod location; + /// The fundamental error type for the evaluation engine. These cases represent different kinds of errors /// the evaluator might face, along with helpful spans to label. An error renderer will take this error value /// and pass it into an error viewer to display to the user. @@ -808,32 +812,6 @@ pub enum ShellError { span: Span, }, - /// Failed to find a file during a nushell operation. - /// - /// ## Resolution - /// - /// Does the file in the error message exist? Is it readable and accessible? Is the casing right? - #[error("File not found")] - #[diagnostic(code(nu::shell::file_not_found), help("{file} does not exist"))] - FileNotFound { - file: String, - #[label("file not found")] - span: Span, - }, - - /// Failed to find a file during a nushell operation. - /// - /// ## Resolution - /// - /// Does the file in the error message exist? Is it readable and accessible? Is the casing right? - #[error("File not found")] - #[diagnostic(code(nu::shell::file_not_found))] - FileNotFoundCustom { - msg: String, - #[label("{msg}")] - span: Span, - }, - /// The registered plugin data for a plugin is invalid. /// /// ## Resolution @@ -924,148 +902,14 @@ pub enum ShellError { span: Span, }, - /// I/O operation interrupted. - /// - /// ## Resolution - /// - /// This is a generic error. Refer to the specific error message for further details. - #[error("I/O interrupted")] - #[diagnostic(code(nu::shell::io_interrupted))] - IOInterrupted { - msg: String, - #[label("{msg}")] - span: Span, - }, - /// An I/O operation failed. /// /// ## Resolution /// - /// This is a generic error. Refer to the specific error message for further details. - #[error("I/O error")] - #[diagnostic(code(nu::shell::io_error), help("{msg}"))] - IOError { msg: String }, - - /// An I/O operation failed. - /// - /// ## Resolution - /// - /// This is a generic error. Refer to the specific error message for further details. - #[error("I/O error")] - #[diagnostic(code(nu::shell::io_error))] - IOErrorSpanned { - msg: String, - #[label("{msg}")] - span: Span, - }, - - /// Tried to `cd` to a path that isn't a directory. - /// - /// ## Resolution - /// - /// Make sure the path is a directory. It currently exists, but is of some other type, like a file. - #[error("Cannot change to directory")] - #[diagnostic(code(nu::shell::cannot_cd_to_directory))] - NotADirectory { - #[label("is not a directory")] - span: Span, - }, - - /// Attempted to perform an operation on a directory that doesn't exist. - /// - /// ## Resolution - /// - /// Make sure the directory in the error message actually exists before trying again. - #[error("Directory not found")] - #[diagnostic(code(nu::shell::directory_not_found), help("{dir} does not exist"))] - DirectoryNotFound { - dir: String, - #[label("directory not found")] - span: Span, - }, - - /// The requested move operation cannot be completed. This is typically because both paths exist, - /// but are of different types. For example, you might be trying to overwrite an existing file with - /// a directory. - /// - /// ## Resolution - /// - /// Make sure the destination path does not exist before moving a directory. - #[error("Move not possible")] - #[diagnostic(code(nu::shell::move_not_possible))] - MoveNotPossible { - source_message: String, - #[label("{source_message}")] - source_span: Span, - destination_message: String, - #[label("{destination_message}")] - destination_span: Span, - }, - - /// Failed to create either a file or directory. - /// - /// ## Resolution - /// - /// This is a fairly generic error. Refer to the specific error message for further details. - #[error("Create not possible")] - #[diagnostic(code(nu::shell::create_not_possible))] - CreateNotPossible { - msg: String, - #[label("{msg}")] - span: Span, - }, - - /// Changing the access time ("atime") of this file is not possible. - /// - /// ## Resolution - /// - /// This can be for various reasons, such as your platform or permission flags. Refer to the specific error message for more details. - #[error("Not possible to change the access time")] - #[diagnostic(code(nu::shell::change_access_time_not_possible))] - ChangeAccessTimeNotPossible { - msg: String, - #[label("{msg}")] - span: Span, - }, - - /// Changing the modification time ("mtime") of this file is not possible. - /// - /// ## Resolution - /// - /// This can be for various reasons, such as your platform or permission flags. Refer to the specific error message for more details. - #[error("Not possible to change the modified time")] - #[diagnostic(code(nu::shell::change_modified_time_not_possible))] - ChangeModifiedTimeNotPossible { - msg: String, - #[label("{msg}")] - span: Span, - }, - - /// Unable to remove this item. - /// - /// ## Resolution - /// - /// Removal can fail for a number of reasons, such as permissions problems. Refer to the specific error message for more details. - #[error("Remove not possible")] - #[diagnostic(code(nu::shell::remove_not_possible))] - RemoveNotPossible { - msg: String, - #[label("{msg}")] - span: Span, - }, - - /// Error while trying to read a file - /// - /// ## Resolution - /// - /// The error will show the result from a file operation - #[error("Error trying to read file")] - #[diagnostic(code(nu::shell::error_reading_file))] - ReadingFile { - msg: String, - #[label("{msg}")] - span: Span, - }, + /// This is the main I/O error, for further details check the error kind and additional context. + #[error(transparent)] + #[diagnostic(transparent)] + Io(io::IoError), /// A name was not found. Did you mean a different name? /// @@ -1531,75 +1375,26 @@ impl ShellError { } } -impl From for ShellError { - fn from(error: io::Error) -> ShellError { - if error.kind() == io::ErrorKind::Other { - match error.into_inner() { - Some(err) => match err.downcast() { - Ok(err) => *err, - Err(err) => Self::IOError { - msg: err.to_string(), - }, - }, - None => Self::IOError { - msg: "unknown error".into(), - }, - } - } else { - Self::IOError { - msg: error.to_string(), - } - } - } -} - -impl From> for ShellError { - fn from(error: Spanned) -> Self { - let Spanned { item: error, span } = error; - match error.kind() { - io::ErrorKind::Other => match error.into_inner() { - Some(err) => match err.downcast() { - Ok(err) => *err, - Err(err) => Self::IOErrorSpanned { - msg: err.to_string(), - span, - }, - }, - None => Self::IOErrorSpanned { - msg: "unknown error".into(), - span, - }, - }, - io::ErrorKind::TimedOut => Self::NetworkFailure { - msg: error.to_string(), - span, - }, - _ => Self::IOErrorSpanned { - msg: error.to_string(), - span, - }, - } - } -} - -impl From for io::Error { - fn from(error: ShellError) -> Self { - io::Error::new(io::ErrorKind::Other, error) - } -} - impl From> for ShellError { fn from(error: Box) -> ShellError { - ShellError::IOError { + ShellError::GenericError { + error: format!("{error:?}"), msg: error.to_string(), + span: None, + help: None, + inner: vec![], } } } impl From> for ShellError { fn from(error: Box) -> ShellError { - ShellError::IOError { - msg: format!("{error:?}"), + ShellError::GenericError { + error: format!("{error:?}"), + msg: error.to_string(), + span: None, + help: None, + inner: vec![], } } } @@ -1682,3 +1477,26 @@ fn shell_error_serialize_roundtrip() { deserialized.help().map(|c| c.to_string()) ); } + +#[cfg(test)] +mod test { + use super::*; + + impl From for ShellError { + fn from(_: std::io::Error) -> ShellError { + unimplemented!("This implementation is defined in the test module to ensure no other implementation exists.") + } + } + + impl From> for ShellError { + fn from(_: Spanned) -> Self { + unimplemented!("This implementation is defined in the test module to ensure no other implementation exists.") + } + } + + impl From for std::io::Error { + fn from(_: ShellError) -> Self { + unimplemented!("This implementation is defined in the test module to ensure no other implementation exists.") + } + } +} diff --git a/crates/nu-protocol/src/eval_const.rs b/crates/nu-protocol/src/eval_const.rs index 25f5d8404a..86ce4d7385 100644 --- a/crates/nu-protocol/src/eval_const.rs +++ b/crates/nu-protocol/src/eval_const.rs @@ -143,8 +143,12 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu Value::string(canon_home_path.to_string_lossy(), span) } else { Value::error( - ShellError::IOError { + ShellError::GenericError { + error: "setting $nu.home-path failed".into(), msg: "Could not get home path".into(), + span: Some(span), + help: None, + inner: vec![], }, span, ) @@ -159,8 +163,12 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu Value::string(canon_data_path.to_string_lossy(), span) } else { Value::error( - ShellError::IOError { + ShellError::GenericError { + error: "setting $nu.data-dir failed".into(), msg: "Could not get data path".into(), + span: Some(span), + help: None, + inner: vec![], }, span, ) @@ -175,8 +183,12 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu Value::string(canon_cache_path.to_string_lossy(), span) } else { Value::error( - ShellError::IOError { + ShellError::GenericError { + error: "setting $nu.cache-dir failed".into(), msg: "Could not get cache path".into(), + span: Some(span), + help: None, + inner: vec![], }, span, ) @@ -248,8 +260,12 @@ pub(crate) fn create_nu_constant(engine_state: &EngineState, span: Span) -> Valu Value::string(current_exe.to_string_lossy(), span) } else { Value::error( - ShellError::IOError { - msg: "Could not get current executable path".to_string(), + ShellError::GenericError { + error: "setting $nu.current-exe failed".into(), + msg: "Could not get current executable path".into(), + span: Some(span), + help: None, + inner: vec![], }, span, ) diff --git a/crates/nu-protocol/src/pipeline/byte_stream.rs b/crates/nu-protocol/src/pipeline/byte_stream.rs index b5cbbff237..e06a651088 100644 --- a/crates/nu-protocol/src/pipeline/byte_stream.rs +++ b/crates/nu-protocol/src/pipeline/byte_stream.rs @@ -1,7 +1,13 @@ //! Module managing the streaming of raw bytes between pipeline elements +//! +//! This module also handles conversions the [`ShellError`] <-> [`io::Error`](std::io::Error), +//! so remember the usage of [`ShellErrorBridge`] where applicable. #[cfg(feature = "os")] use crate::process::{ChildPipe, ChildProcess}; -use crate::{ErrSpan, IntRange, IntoSpanned, PipelineData, ShellError, Signals, Span, Type, Value}; +use crate::{ + shell_error::{bridge::ShellErrorBridge, io::IoError}, + IntRange, PipelineData, ShellError, Signals, Span, Type, Value, +}; use serde::{Deserialize, Serialize}; use std::ops::Bound; #[cfg(unix)] @@ -225,7 +231,8 @@ impl ByteStream { let known_size = self.known_size.map(|len| len.saturating_sub(n)); if let Some(mut reader) = self.reader() { // Copy the number of skipped bytes into the sink before proceeding - io::copy(&mut (&mut reader).take(n), &mut io::sink()).err_span(span)?; + io::copy(&mut (&mut reader).take(n), &mut io::sink()) + .map_err(|err| IoError::new(err.kind(), span, None))?; Ok( ByteStream::read(reader, span, Signals::empty(), ByteStreamType::Binary) .with_known_size(known_size), @@ -346,7 +353,7 @@ impl ByteStream { /// binary. #[cfg(feature = "os")] pub fn stdin(span: Span) -> Result { - let stdin = os_pipe::dup_stdin().err_span(span)?; + let stdin = os_pipe::dup_stdin().map_err(|err| IoError::new(err.kind(), span, None))?; let source = ByteStreamSource::File(convert_file(stdin)); Ok(Self::new( source, @@ -573,15 +580,16 @@ impl ByteStream { /// Any trailing new lines are kept in the returned [`Vec`]. pub fn into_bytes(self) -> Result, ShellError> { // todo!() ctrlc + let from_io_error = IoError::factory(self.span, None); match self.stream { ByteStreamSource::Read(mut read) => { let mut buf = Vec::new(); - read.read_to_end(&mut buf).err_span(self.span)?; + read.read_to_end(&mut buf).map_err(&from_io_error)?; Ok(buf) } ByteStreamSource::File(mut file) => { let mut buf = Vec::new(); - file.read_to_end(&mut buf).err_span(self.span)?; + file.read_to_end(&mut buf).map_err(&from_io_error)?; Ok(buf) } #[cfg(feature = "os")] @@ -759,7 +767,12 @@ where while let Some(cursor) = self.cursor.as_mut() { let read = cursor.read(buf)?; if read == 0 { - self.cursor = self.iter.next().transpose()?.map(Cursor::new); + self.cursor = self + .iter + .next() + .transpose() + .map_err(ShellErrorBridge)? + .map(Cursor::new); } else { return Ok(read); } @@ -782,7 +795,7 @@ impl Reader { impl Read for Reader { fn read(&mut self, buf: &mut [u8]) -> io::Result { - self.signals.check(self.span)?; + self.signals.check(self.span).map_err(ShellErrorBridge)?; self.reader.read(buf) } } @@ -826,7 +839,7 @@ impl Iterator for Lines { trim_end_newline(&mut string); Some(Ok(string)) } - Err(e) => Some(Err(e.into_spanned(self.span).into())), + Err(e) => Some(Err(IoError::new(e.kind(), self.span, None).into())), } } } @@ -1022,7 +1035,15 @@ impl Iterator for SplitRead { if self.signals.interrupted() { return None; } - self.internal.next().map(|r| r.map_err(|e| e.into())) + self.internal.next().map(|r| { + r.map_err(|err| { + ShellError::Io(IoError::new_internal( + err.kind(), + "Could not get next value for SplitRead", + crate::location!(), + )) + }) + }) } } @@ -1057,12 +1078,17 @@ impl Chunks { } fn next_string(&mut self) -> Result, (Vec, ShellError)> { + let from_io_error = |err: std::io::Error| match ShellErrorBridge::try_from(err) { + Ok(err) => err.0, + Err(err) => IoError::new(err.kind(), self.span, None).into(), + }; + // Get some data from the reader let buf = self .reader .fill_buf() - .err_span(self.span) - .map_err(|err| (vec![], ShellError::from(err)))?; + .map_err(from_io_error) + .map_err(|err| (vec![], err))?; // If empty, this is EOF if buf.is_empty() { @@ -1076,9 +1102,9 @@ impl Chunks { if buf.len() < 4 { consumed += buf.len(); self.reader.consume(buf.len()); - match self.reader.fill_buf().err_span(self.span) { + match self.reader.fill_buf() { Ok(more_bytes) => buf.extend_from_slice(more_bytes), - Err(err) => return Err((buf, err.into())), + Err(err) => return Err((buf, from_io_error(err))), } } @@ -1133,11 +1159,15 @@ impl Iterator for Chunks { match self.type_ { // Binary should always be binary ByteStreamType::Binary => { - let buf = match self.reader.fill_buf().err_span(self.span) { + let buf = match self.reader.fill_buf() { Ok(buf) => buf, Err(err) => { self.error = true; - return Some(Err(err.into())); + return Some(Err(ShellError::Io(IoError::new( + err.kind(), + self.span, + None, + )))); } }; if !buf.is_empty() { @@ -1206,15 +1236,19 @@ pub fn copy_with_signals( span: Span, signals: &Signals, ) -> Result { + let from_io_error = IoError::factory(span, None); if signals.is_empty() { match io::copy(&mut reader, &mut writer) { Ok(n) => { - writer.flush().err_span(span)?; + writer.flush().map_err(&from_io_error)?; Ok(n) } Err(err) => { let _ = writer.flush(); - Err(err.into_spanned(span).into()) + match ShellErrorBridge::try_from(err) { + Ok(ShellErrorBridge(shell_error)) => Err(shell_error), + Err(err) => Err(from_io_error(err).into()), + } } } } else { @@ -1224,7 +1258,7 @@ pub fn copy_with_signals( // } match generic_copy(&mut reader, &mut writer, span, signals) { Ok(len) => { - writer.flush().err_span(span)?; + writer.flush().map_err(&from_io_error)?; Ok(len) } Err(err) => { @@ -1242,6 +1276,7 @@ fn generic_copy( span: Span, signals: &Signals, ) -> Result { + let from_io_error = IoError::factory(span, None); let buf = &mut [0; DEFAULT_BUF_SIZE]; let mut len = 0; loop { @@ -1250,10 +1285,13 @@ fn generic_copy( Ok(0) => break, Ok(n) => n, Err(e) if e.kind() == ErrorKind::Interrupted => continue, - Err(e) => return Err(e.into_spanned(span).into()), + Err(e) => match ShellErrorBridge::try_from(e) { + Ok(ShellErrorBridge(e)) => return Err(e), + Err(e) => return Err(from_io_error(e).into()), + }, }; len += n; - writer.write_all(&buf[..n]).err_span(span)?; + writer.write_all(&buf[..n]).map_err(&from_io_error)?; } Ok(len as u64) } @@ -1278,7 +1316,7 @@ where self.buffer.set_position(0); self.buffer.get_mut().clear(); // Ask the generator to generate data - if !(self.generator)(self.buffer.get_mut())? { + if !(self.generator)(self.buffer.get_mut()).map_err(ShellErrorBridge)? { // End of stream break; } diff --git a/crates/nu-protocol/src/pipeline/pipeline_data.rs b/crates/nu-protocol/src/pipeline/pipeline_data.rs index b9d523b3da..b4a1e9ca01 100644 --- a/crates/nu-protocol/src/pipeline/pipeline_data.rs +++ b/crates/nu-protocol/src/pipeline/pipeline_data.rs @@ -1,6 +1,7 @@ use crate::{ ast::{Call, PathMember}, engine::{EngineState, Stack}, + shell_error::io::IoError, ByteStream, ByteStreamType, Config, ListStream, OutDest, PipelineMetadata, Range, ShellError, Signals, Span, Type, Value, }; @@ -219,17 +220,47 @@ impl PipelineData { PipelineData::Empty => Ok(()), PipelineData::Value(value, ..) => { let bytes = value_to_bytes(value)?; - dest.write_all(&bytes)?; - dest.flush()?; + dest.write_all(&bytes).map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not write PipelineData to dest", + crate::location!(), + ) + })?; + dest.flush().map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not flush PipelineData to dest", + crate::location!(), + ) + })?; Ok(()) } PipelineData::ListStream(stream, ..) => { for value in stream { let bytes = value_to_bytes(value)?; - dest.write_all(&bytes)?; - dest.write_all(b"\n")?; + dest.write_all(&bytes).map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not write PipelineData to dest", + crate::location!(), + ) + })?; + dest.write_all(b"\n").map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not write linebreak after PipelineData to dest", + crate::location!(), + ) + })?; } - dest.flush()?; + dest.flush().map_err(|err| { + IoError::new_internal( + err.kind(), + "Could not flush PipelineData to dest", + crate::location!(), + ) + })?; Ok(()) } PipelineData::ByteStream(stream, ..) => stream.write_to(dest), @@ -633,9 +664,23 @@ impl PipelineData { ) -> Result<(), ShellError> { if let PipelineData::Value(Value::Binary { val: bytes, .. }, _) = self { if to_stderr { - stderr_write_all_and_flush(bytes)?; + stderr_write_all_and_flush(bytes).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Writing to stderr failed", + ) + })? } else { - stdout_write_all_and_flush(bytes)?; + stdout_write_all_and_flush(bytes).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Writing to stdout failed", + ) + })? } Ok(()) } else { @@ -666,9 +711,23 @@ impl PipelineData { } if to_stderr { - stderr_write_all_and_flush(out)? + stderr_write_all_and_flush(out).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Writing to stderr failed", + ) + })? } else { - stdout_write_all_and_flush(out)? + stdout_write_all_and_flush(out).map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + None, + "Writing to stdout failed", + ) + })? } } diff --git a/crates/nu-protocol/src/process/child.rs b/crates/nu-protocol/src/process/child.rs index 8c4fb55c0d..1c0ef513ed 100644 --- a/crates/nu-protocol/src/process/child.rs +++ b/crates/nu-protocol/src/process/child.rs @@ -1,4 +1,4 @@ -use crate::{byte_stream::convert_file, ErrSpan, IntoSpanned, ShellError, Span}; +use crate::{byte_stream::convert_file, shell_error::io::IoError, ShellError, Span}; use nu_system::{ExitStatus, ForegroundChild}; use os_pipe::PipeReader; use std::{ @@ -74,13 +74,18 @@ impl ExitStatusFuture { Ok(status) } Ok(Ok(status)) => Ok(status), - Ok(Err(err)) => Err(ShellError::IOErrorSpanned { - msg: format!("failed to get exit code: {err:?}"), + Ok(Err(err)) => Err(ShellError::Io(IoError::new_with_additional_context( + err.kind(), span, - }), - Err(RecvError) => Err(ShellError::IOErrorSpanned { + None, + "failed to get exit code", + ))), + Err(err @ RecvError) => Err(ShellError::GenericError { + error: err.to_string(), msg: "failed to get exit code".into(), - span, + span: span.into(), + help: None, + inner: vec![], }), }; @@ -98,13 +103,19 @@ impl ExitStatusFuture { ExitStatusFuture::Running(receiver) => { let code = match receiver.try_recv() { Ok(Ok(status)) => Ok(Some(status)), - Ok(Err(err)) => Err(ShellError::IOErrorSpanned { - msg: format!("failed to get exit code: {err:?}"), - span, + Ok(Err(err)) => Err(ShellError::GenericError { + error: err.to_string(), + msg: "failed to get exit code".to_string(), + span: span.into(), + help: None, + inner: vec![], }), - Err(TryRecvError::Disconnected) => Err(ShellError::IOErrorSpanned { + Err(TryRecvError::Disconnected) => Err(ShellError::GenericError { + error: "receiver disconnected".to_string(), msg: "failed to get exit code".into(), - span, + span: span.into(), + help: None, + inner: vec![], }), Err(TryRecvError::Empty) => Ok(None), }; @@ -180,7 +191,14 @@ impl ChildProcess { thread::Builder::new() .name("exit status waiter".into()) .spawn(move || exit_status_sender.send(child.wait())) - .err_span(span)?; + .map_err(|err| { + IoError::new_with_additional_context( + err.kind(), + span, + None, + "Could now spawn exit status waiter", + ) + })?; Ok(Self::from_raw(stdout, stderr, Some(exit_status), span)) } @@ -214,14 +232,17 @@ impl ChildProcess { pub fn into_bytes(mut self) -> Result, ShellError> { if self.stderr.is_some() { debug_assert!(false, "stderr should not exist"); - return Err(ShellError::IOErrorSpanned { - msg: "internal error".into(), - span: self.span, + return Err(ShellError::GenericError { + error: "internal error".into(), + msg: "stderr should not exist".into(), + span: self.span.into(), + help: None, + inner: vec![], }); } let bytes = if let Some(stdout) = self.stdout { - collect_bytes(stdout).err_span(self.span)? + collect_bytes(stdout).map_err(|err| IoError::new(err.kind(), self.span, None))? } else { Vec::new() }; @@ -236,6 +257,7 @@ impl ChildProcess { } pub fn wait(mut self) -> Result<(), ShellError> { + let from_io_error = IoError::factory(self.span, None); if let Some(stdout) = self.stdout.take() { let stderr = self .stderr @@ -246,7 +268,7 @@ impl ChildProcess { .spawn(move || consume_pipe(stderr)) }) .transpose() - .err_span(self.span)?; + .map_err(&from_io_error)?; let res = consume_pipe(stdout); @@ -254,7 +276,7 @@ impl ChildProcess { handle .join() .map_err(|e| match e.downcast::() { - Ok(io) => ShellError::from((*io).into_spanned(self.span)), + Ok(io) => from_io_error(*io).into(), Err(err) => ShellError::GenericError { error: "Unknown error".into(), msg: format!("{err:?}"), @@ -263,12 +285,12 @@ impl ChildProcess { inner: Vec::new(), }, })? - .err_span(self.span)?; + .map_err(&from_io_error)?; } - res.err_span(self.span)?; + res.map_err(&from_io_error)?; } else if let Some(stderr) = self.stderr.take() { - consume_pipe(stderr).err_span(self.span)?; + consume_pipe(stderr).map_err(&from_io_error)?; } check_ok( @@ -283,19 +305,20 @@ impl ChildProcess { } pub fn wait_with_output(mut self) -> Result { + let from_io_error = IoError::factory(self.span, None); let (stdout, stderr) = if let Some(stdout) = self.stdout { let stderr = self .stderr .map(|stderr| thread::Builder::new().spawn(move || collect_bytes(stderr))) .transpose() - .err_span(self.span)?; + .map_err(&from_io_error)?; - let stdout = collect_bytes(stdout).err_span(self.span)?; + let stdout = collect_bytes(stdout).map_err(&from_io_error)?; let stderr = stderr .map(|handle| { handle.join().map_err(|e| match e.downcast::() { - Ok(io) => ShellError::from((*io).into_spanned(self.span)), + Ok(io) => from_io_error(*io).into(), Err(err) => ShellError::GenericError { error: "Unknown error".into(), msg: format!("{err:?}"), @@ -307,7 +330,7 @@ impl ChildProcess { }) .transpose()? .transpose() - .err_span(self.span)?; + .map_err(&from_io_error)?; (Some(stdout), stderr) } else { @@ -315,7 +338,7 @@ impl ChildProcess { .stderr .map(collect_bytes) .transpose() - .err_span(self.span)?; + .map_err(&from_io_error)?; (None, stderr) }; diff --git a/crates/nu-protocol/src/span.rs b/crates/nu-protocol/src/span.rs index 9fe42c2530..dd465b5a63 100644 --- a/crates/nu-protocol/src/span.rs +++ b/crates/nu-protocol/src/span.rs @@ -115,10 +115,17 @@ impl Span { Self { start: 0, end: 0 } } + /// Span for testing purposes. + /// + /// The provided span does not point into any known source but is unequal to [`Span::unknown()`]. + /// /// Note: Only use this for test data, *not* live data, as it will point into unknown source - /// when used in errors. + /// when used in errors pub const fn test_data() -> Self { - Self::unknown() + Self { + start: usize::MAX / 2, + end: usize::MAX / 2, + } } pub fn offset(&self, offset: usize) -> Self { @@ -215,26 +222,14 @@ impl From for SourceSpan { } } -/// An extension trait for `Result`, which adds a span to the error type. +/// An extension trait for [`Result`], which adds a span to the error type. +/// +/// This trait might be removed later, since the old [`Spanned`] to [`ShellError`] +/// conversion was replaced by [`IoError`](io_error::IoError). pub trait ErrSpan { type Result; - /// Add the given span to the error type `E`, turning it into a `Spanned`. - /// - /// Some auto-conversion methods to `ShellError` from other error types are available on spanned - /// errors, to give users better information about where an error came from. For example, it is - /// preferred when working with `std::io::Error`: - /// - /// ```no_run - /// use nu_protocol::{ErrSpan, ShellError, Span}; - /// use std::io::Read; - /// - /// fn read_from(mut reader: impl Read, span: Span) -> Result, ShellError> { - /// let mut vec = vec![]; - /// reader.read_to_end(&mut vec).err_span(span)?; - /// Ok(vec) - /// } - /// ``` + /// Adds the given span to the error type, turning it into a [`Spanned`]. fn err_span(self, span: Span) -> Self::Result; } diff --git a/crates/nu_plugin_polars/src/dataframe/command/core/open.rs b/crates/nu_plugin_polars/src/dataframe/command/core/open.rs index 97a44d8f4f..b218530e64 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/core/open.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/core/open.rs @@ -11,8 +11,8 @@ use url::Url; use nu_plugin::PluginCommand; use nu_protocol::{ - Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Spanned, - SyntaxShape, Type, Value, + shell_error::io::IoError, Category, Example, LabeledError, PipelineData, ShellError, Signature, + Span, Spanned, SyntaxShape, Type, Value, }; use std::{fmt::Debug, fs::File, io::BufReader, num::NonZeroUsize, path::PathBuf, sync::Arc}; @@ -221,10 +221,12 @@ fn command( blamed, )), }, - None => Err(ShellError::FileNotFoundCustom { - msg: "File without extension".into(), - span: spanned_file.span, - }), + None => Err(ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, + spanned_file.span, + PathBuf::from(spanned_file.item), + "File without extension", + ))), } .map(|value| PipelineData::Value(value, None)) } diff --git a/crates/nu_plugin_polars/src/dataframe/command/core/save/mod.rs b/crates/nu_plugin_polars/src/dataframe/command/core/save/mod.rs index 8619ec9d42..775baca990 100644 --- a/crates/nu_plugin_polars/src/dataframe/command/core/save/mod.rs +++ b/crates/nu_plugin_polars/src/dataframe/command/core/save/mod.rs @@ -14,8 +14,8 @@ use crate::{ use nu_path::expand_path_with; use nu_plugin::{EngineInterface, EvaluatedCall, PluginCommand}; use nu_protocol::{ - Category, Example, LabeledError, PipelineData, ShellError, Signature, Span, Spanned, - SyntaxShape, Type, + shell_error::io::IoError, Category, Example, LabeledError, PipelineData, ShellError, Signature, + Span, Spanned, SyntaxShape, Type, }; use polars::error::PolarsError; @@ -204,10 +204,12 @@ fn command( blamed, )), }, - None => Err(ShellError::FileNotFoundCustom { - msg: "File without extension".into(), - span: spanned_file.span, - }), + None => Err(ShellError::Io(IoError::new_with_additional_context( + std::io::ErrorKind::NotFound, + spanned_file.span, + spanned_file.item, + "File without extension", + ))), }?; Ok(PipelineData::empty()) diff --git a/src/ide.rs b/src/ide.rs index 85023745fa..6ce7e69c6f 100644 --- a/src/ide.rs +++ b/src/ide.rs @@ -3,7 +3,9 @@ use nu_cli::NuCompleter; use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, - report_shell_error, DeclId, ShellError, Span, Value, VarId, + report_shell_error, + shell_error::io::IoError, + DeclId, ShellError, Span, Value, VarId, }; use reedline::Completer; use serde_json::{json, Value as JsonValue}; @@ -54,15 +56,16 @@ fn read_in_file<'a>( file_path: &str, ) -> (Vec, StateWorkingSet<'a>) { let file = std::fs::read(file_path) - .into_diagnostic() - .unwrap_or_else(|e| { - report_shell_error( - engine_state, - &ShellError::FileNotFoundCustom { - msg: format!("Could not read file '{}': {:?}", file_path, e.to_string()), - span: Span::unknown(), - }, - ); + .map_err(|err| { + ShellError::Io(IoError::new_with_additional_context( + err.kind(), + Span::unknown(), + PathBuf::from(file_path), + "Could not read file", + )) + }) + .unwrap_or_else(|err| { + report_shell_error(engine_state, &err); std::process::exit(1); }); diff --git a/src/main.rs b/src/main.rs index 0e7c656fc7..6beae6de77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -428,7 +428,13 @@ fn main() -> Result<()> { for plugin_filename in plugins { // Make sure the plugin filenames are canonicalized let filename = canonicalize_with(&plugin_filename.item, &init_cwd) - .err_span(plugin_filename.span) + .map_err(|err| { + nu_protocol::shell_error::io::IoError::new( + err.kind(), + plugin_filename.span, + PathBuf::from(&plugin_filename.item), + ) + }) .map_err(ShellError::from)?; let identity = PluginIdentity::new(&filename, None)