diff --git a/crates/nu-cli/src/eval_file.rs b/crates/nu-cli/src/eval_file.rs index 8a761df0a9..f302111b93 100644 --- a/crates/nu-cli/src/eval_file.rs +++ b/crates/nu-cli/src/eval_file.rs @@ -8,7 +8,7 @@ use nu_protocol::{ debugger::WithoutDebug, engine::{EngineState, Stack, StateWorkingSet}, report_parse_error, report_parse_warning, - shell_error::io::IoError, + shell_error::io::*, PipelineData, ShellError, Span, Value, }; use std::{path::PathBuf, sync::Arc}; @@ -28,7 +28,7 @@ pub fn evaluate_file( let file_path = canonicalize_with(&path, cwd).map_err(|err| { IoError::new_internal_with_path( - err.kind(), + err.kind().not_found_as(NotFound::File), "Could not access file", nu_protocol::location!(), PathBuf::from(&path), @@ -47,7 +47,7 @@ pub fn evaluate_file( let file = std::fs::read(&file_path).map_err(|err| { IoError::new_internal_with_path( - err.kind(), + err.kind().not_found_as(NotFound::File), "Could not read file", nu_protocol::location!(), file_path.clone(), @@ -57,7 +57,7 @@ pub fn evaluate_file( let parent = file_path.parent().ok_or_else(|| { IoError::new_internal_with_path( - std::io::ErrorKind::NotFound, + ErrorKind::DirectoryNotFound, "The file path does not have a parent", nu_protocol::location!(), file_path.clone(), diff --git a/crates/nu-command/src/env/config/config_reset.rs b/crates/nu-command/src/env/config/config_reset.rs index 5215ac6a1d..eeed056812 100644 --- a/crates/nu-command/src/env/config/config_reset.rs +++ b/crates/nu-command/src/env/config/config_reset.rs @@ -1,7 +1,5 @@ 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, path::PathBuf}; @@ -61,7 +59,7 @@ impl Command for ConfigReset { )); if let Err(err) = std::fs::rename(nu_config.clone(), &backup_path) { return Err(ShellError::Io(IoError::new_with_additional_context( - err.kind(), + err.kind().not_found_as(NotFound::Directory), span, PathBuf::from(backup_path), "config.nu could not be backed up", @@ -71,7 +69,7 @@ impl Command for ConfigReset { 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(), + err.kind().not_found_as(NotFound::File), span, PathBuf::from(nu_config), "config.nu could not be written to", @@ -88,7 +86,7 @@ impl Command for ConfigReset { backup_path.push(format!("oldenv-{}.nu", Local::now().format("%F-%H-%M-%S"),)); if let Err(err) = std::fs::rename(env_config.clone(), &backup_path) { return Err(ShellError::Io(IoError::new_with_additional_context( - err.kind(), + err.kind().not_found_as(NotFound::Directory), span, PathBuf::from(backup_path), "env.nu could not be backed up", @@ -98,7 +96,7 @@ impl Command for ConfigReset { 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(), + err.kind().not_found_as(NotFound::File), span, PathBuf::from(env_config), "env.nu could not be written to", diff --git a/crates/nu-command/src/filesystem/cd.rs b/crates/nu-command/src/filesystem/cd.rs index d8e6713199..5ef3e4eb5e 100644 --- a/crates/nu-command/src/filesystem/cd.rs +++ b/crates/nu-command/src/filesystem/cd.rs @@ -88,7 +88,7 @@ impl Command for Cd { path } else { return Err(shell_error::io::IoError::new( - std::io::ErrorKind::NotFound, + ErrorKind::DirectoryNotFound, v.span, PathBuf::from(path_no_whitespace), ) @@ -98,7 +98,7 @@ impl Command for Cd { let path = nu_path::expand_path_with(path_no_whitespace, &cwd, true); if !path.exists() { return Err(shell_error::io::IoError::new( - std::io::ErrorKind::NotFound, + ErrorKind::DirectoryNotFound, v.span, PathBuf::from(path_no_whitespace), ) diff --git a/crates/nu-command/src/filesystem/open.rs b/crates/nu-command/src/filesystem/open.rs index 2be2314835..27dbf5723a 100644 --- a/crates/nu-command/src/filesystem/open.rs +++ b/crates/nu-command/src/filesystem/open.rs @@ -98,6 +98,7 @@ impl Command for Open { for path in nu_engine::glob_from(&path, &cwd, call_span, None) .map_err(|err| match err { ShellError::Io(mut err) => { + err.kind = err.kind.not_found_as(NotFound::File); err.span = arg_span; err.into() } diff --git a/crates/nu-command/src/misc/source.rs b/crates/nu-command/src/misc/source.rs index de692c557c..d507345f6e 100644 --- a/crates/nu-command/src/misc/source.rs +++ b/crates/nu-command/src/misc/source.rs @@ -55,8 +55,13 @@ 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| IoError::new(err.kind(), call.head, pb.clone()))?; + let file_path = canonicalize_with(pb.as_path(), cwd).map_err(|err| { + IoError::new( + err.kind().not_found_as(NotFound::File), + 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/system/nu_check.rs b/crates/nu-command/src/system/nu_check.rs index d988974589..8280232c1a 100644 --- a/crates/nu-command/src/system/nu_check.rs +++ b/crates/nu-command/src/system/nu_check.rs @@ -98,7 +98,7 @@ impl Command for NuCheck { Ok(Some(path)) => path, Ok(None) => { return Err(ShellError::Io(IoError::new( - std::io::ErrorKind::NotFound, + ErrorKind::FileNotFound, path_span, PathBuf::from(path_str.item), ))) @@ -255,7 +255,7 @@ fn parse_file_script( 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(), + err.kind().not_found_as(NotFound::File), path_span, PathBuf::from(path), ))), diff --git a/crates/nu-command/tests/commands/cd.rs b/crates/nu-command/tests/commands/cd.rs index 7dd648d135..2ad25f5aa6 100644 --- a/crates/nu-command/tests/commands/cd.rs +++ b/crates/nu-command/tests/commands/cd.rs @@ -210,7 +210,7 @@ fn filesystem_directory_not_found() { actual.err ); assert!( - actual.err.contains("nu::shell::io::not_found"), + actual.err.contains("nu::shell::io::directory_not_found"), "actual={:?}", actual.err ); diff --git a/crates/nu-command/tests/commands/nu_check.rs b/crates/nu-command/tests/commands/nu_check.rs index 971349778e..ed082aeedc 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("nu::shell::io::not_found")); + assert!(actual.err.contains("nu::shell::io::file_not_found")); }) } diff --git a/crates/nu-command/tests/commands/open.rs b/crates/nu-command/tests/commands/open.rs index 09df04b97a..529da6c28c 100644 --- a/crates/nu-command/tests/commands/open.rs +++ b/crates/nu-command/tests/commands/open.rs @@ -251,7 +251,7 @@ fn errors_if_file_not_found() { // This seems to be not directly affected by localization compared to the OS // provided error message - assert!(actual.err.contains("nu::shell::io::not_found")); + assert!(actual.err.contains("nu::shell::io::file_not_found")); assert!(actual.err.contains( &PathBuf::from_iter(["tests", "fixtures", "formats", "i_dont_exist.txt"]) .display() diff --git a/crates/nu-engine/src/command_prelude.rs b/crates/nu-engine/src/command_prelude.rs index 9b9ed40477..39124af1d4 100644 --- a/crates/nu-engine/src/command_prelude.rs +++ b/crates/nu-engine/src/command_prelude.rs @@ -3,7 +3,7 @@ pub use nu_protocol::{ ast::CellPath, engine::{Call, Command, EngineState, Stack, StateWorkingSet}, record, - shell_error::io::IoError, + shell_error::io::*, 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 2c4aec9fb3..5a2a201911 100644 --- a/crates/nu-engine/src/env.rs +++ b/crates/nu-engine/src/env.rs @@ -3,7 +3,7 @@ use nu_path::canonicalize_with; use nu_protocol::{ ast::Expr, engine::{Call, EngineState, Stack, StateWorkingSet}, - shell_error::io::IoError, + shell_error::io::{ErrorKindExt, IoError, NotFound}, ShellError, Span, Type, Value, VarId, }; use std::{ @@ -221,7 +221,7 @@ pub fn current_dir(engine_state: &EngineState, stack: &Stack) -> Result Result) -> fmt::Result { + match self.kind { + ErrorKind::Std(std::io::ErrorKind::NotFound) => write!(f, "Not found"), + ErrorKind::FileNotFound => write!(f, "File not found"), + ErrorKind::DirectoryNotFound => write!(f, "Directory not found"), + _ => write!(f, "I/O error"), + } + } +} + impl Display for ErrorKind { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result { match self { + ErrorKind::Std(std::io::ErrorKind::NotFound) => write!(f, "Not found"), ErrorKind::Std(error_kind) => { let msg = error_kind.to_string(); let (first, rest) = msg.split_at(1); write!(f, "{}{}", first.to_uppercase(), rest) } ErrorKind::NotAFile => write!(f, "Not a file"), + ErrorKind::FileNotFound => write!(f, "File not found"), + ErrorKind::DirectoryNotFound => write!(f, "Directory not found"), } } } @@ -352,15 +366,28 @@ impl Diagnostic for IoError { kind => code.push_str(&kind.to_string().to_lowercase().replace(" ", "_")), }, ErrorKind::NotAFile => code.push_str("not_a_file"), + ErrorKind::FileNotFound => code.push_str("file_not_found"), + ErrorKind::DirectoryNotFound => code.push_str("directory_not_found"), } Some(Box::new(code)) } fn help<'a>(&'a self) -> Option> { + let make_msg = |path: &Path| { + let path = format!("'{}'", path.display()); + match self.kind { + ErrorKind::NotAFile => format!("{path} is not a file"), + ErrorKind::Std(std::io::ErrorKind::NotFound) + | ErrorKind::FileNotFound + | ErrorKind::DirectoryNotFound => format!("{path} does not exist"), + _ => format!("The error occurred at {path}"), + } + }; + self.path .as_ref() - .map(|path| format!("The error occurred at '{}'", path.display())) + .map(|path| make_msg(path)) .map(|s| Box::new(s) as Box) } @@ -417,3 +444,75 @@ impl From for std::io::ErrorKind { } } } + +/// More specific variants of [`NotFound`](std::io::ErrorKind). +/// +/// Use these to define how a `NotFound` error maps to our custom [`ErrorKind`]. +pub enum NotFound { + /// Map into [`FileNotFound`](ErrorKind::FileNotFound). + File, + /// Map into [`DirectoryNotFound`](ErrorKind::DirectoryNotFound). + Directory, +} + +/// Extension trait for working with [`std::io::ErrorKind`]. +pub trait ErrorKindExt { + /// Map [`NotFound`](std::io::ErrorKind) variants into more precise variants. + /// + /// The OS doesn't know when an entity was not found whether it was meant to be a file or a + /// directory or something else. + /// But sometimes we, the application, know what we expect and with this method, we can further + /// specify it. + /// + /// # Examples + /// Reading a file. + /// If the file isn't found, return [`FileNotFound`](ErrorKind::FileNotFound). + /// ```rust + /// # use nu_protocol::{ + /// # shell_error::io::{ErrorKind, ErrorKindExt, IoError, NotFound}, + /// # ShellError, Span, + /// # }; + /// # use std::{fs, path::PathBuf}; + /// # + /// # fn example() -> Result<(), ShellError> { + /// # let span = Span::test_data(); + /// let a_file = PathBuf::from("scripts/ellie.nu"); + /// let ellie = fs::read_to_string(&a_file).map_err(|err| { + /// ShellError::Io(IoError::new( + /// err.kind().not_found_as(NotFound::File), + /// span, + /// a_file, + /// )) + /// })?; + /// # Ok(()) + /// # } + /// # + /// # assert!(matches!( + /// # example(), + /// # Err(ShellError::Io(IoError { + /// # kind: ErrorKind::FileNotFound, + /// # .. + /// # })) + /// # )); + /// ``` + fn not_found_as(self, kind: NotFound) -> ErrorKind; +} + +impl ErrorKindExt for std::io::ErrorKind { + fn not_found_as(self, kind: NotFound) -> ErrorKind { + match (kind, self) { + (NotFound::File, Self::NotFound) => ErrorKind::FileNotFound, + (NotFound::Directory, Self::NotFound) => ErrorKind::DirectoryNotFound, + _ => ErrorKind::Std(self), + } + } +} + +impl ErrorKindExt for ErrorKind { + fn not_found_as(self, kind: NotFound) -> ErrorKind { + match self { + Self::Std(std_kind) => std_kind.not_found_as(kind), + _ => self, + } + } +} diff --git a/src/ide.rs b/src/ide.rs index 6ce7e69c6f..8d64fdd487 100644 --- a/src/ide.rs +++ b/src/ide.rs @@ -4,7 +4,7 @@ use nu_parser::{flatten_block, parse, FlatShape}; use nu_protocol::{ engine::{EngineState, Stack, StateWorkingSet}, report_shell_error, - shell_error::io::IoError, + shell_error::io::{ErrorKindExt, IoError, NotFound}, DeclId, ShellError, Span, Value, VarId, }; use reedline::Completer; @@ -58,7 +58,7 @@ fn read_in_file<'a>( let file = std::fs::read(file_path) .map_err(|err| { ShellError::Io(IoError::new_with_additional_context( - err.kind(), + err.kind().not_found_as(NotFound::File), Span::unknown(), PathBuf::from(file_path), "Could not read file",