Refactor I/O Errors (#14927)

<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

As mentioned in #10698, we have too many `ShellError` variants, with
some even overlapping in meaning. This PR simplifies and improves I/O
error handling by restructuring `ShellError` related to I/O issues.
Previously, `ShellError::IOError` only contained a message string,
making it convenient but overly generic. It was widely used without
providing spans (#4323).

This PR introduces a new `ShellError::Io` variant that consolidates
multiple I/O-related errors (except for `ShellError::NetworkFailure`,
which remains distinct for now). The new `ShellError::Io` variant
replaces the following:

- `FileNotFound`
- `FileNotFoundCustom`
- `IOInterrupted`
- `IOError`
- `IOErrorSpanned`
- `NotADirectory`
- `DirectoryNotFound`
- `MoveNotPossible`
- `CreateNotPossible`
- `ChangeAccessTimeNotPossible`
- `ChangeModifiedTimeNotPossible`
- `RemoveNotPossible`
- `ReadingFile`

## The `IoError`
`IoError` includes the following fields:

1. **`kind`**: Extends `std::io::ErrorKind` to specify the type of I/O
error without needing new `ShellError` variants. This aligns with the
approach used in `std::io::Error`. This adds a second dimension to error
reporting by combining the `kind` field with `ShellError` variants,
making it easier to describe errors in more detail. As proposed by
@kubouch in [#design-discussion on
Discord](https://discord.com/channels/601130461678272522/615329862395101194/1323699197165178930),
this helps reduce the number of `ShellError` variants. In the error
report, the `kind` field is displayed as the "source" of the error,
e.g., "I/O error," followed by the specific kind of I/O error.
2. **`span`**: A non-optional field to encourage providing spans for
better error reporting (#4323).
3. **`path`**: Optional `PathBuf` to give context about the file or
directory involved in the error (#7695). If provided, it’s shown as a
help entry in error reports.
4. **`additional_context`**: Allows adding custom messages when the
span, kind, and path are insufficient. This is rendered in the error
report at the labeled span.
5. **`location`**: Sometimes, I/O errors occur in the engine itself and
are not caused directly by user input. In such cases, if we don’t have a
span and must set it to `Span::unknown()`, we need another way to
reference the error. For this, the `location` field uses the new
`Location` struct, which records the Rust file and line number where the
error occurred. This ensures that we at least know the Rust code
location that failed, helping with debugging. To make this work, a new
`location!` macro was added, which retrieves `file!`, `line!`, and
`column!` values accurately. If `Location::new` is used directly, it
issues a warning to remind developers to use the macro instead, ensuring
consistent and correct usage.

### Constructor Behavior
`IoError` provides five constructor methods:
- `new` and `new_with_additional_context`: Used for errors caused by
user input and require a valid (non-unknown) span to ensure precise
error reporting.
- `new_internal` and `new_internal_with_path`: Used for internal errors
where a span is not available. These methods require additional context
and the `Location` struct to pinpoint the source of the error in the
engine code.
- `factory`: Returns a closure that maps an `std::io::Error` to an
`IoError`. This is useful for handling multiple I/O errors that share
the same span and path, streamlining error handling in such cases.

## New Report Look
This is simulation how the I/O errors look like (the `open crates` is
simulated to show how internal errors are referenced now):
![Screenshot 2025-01-25
190426](https://github.com/user-attachments/assets/a41b6aa6-a440-497d-bbcc-3ac0121c9226)

## `Span::test_data()`
To enable better testing, `Span::test_data()` now returns a value
distinct from `Span::unknown()`. Both `Span::test_data()` and
`Span::unknown()` refer to invalid source code, but having a separate
value for test data helps identify issues during testing while keeping
spans unique.

## Cursed Sneaky Error Transfers
I removed the conversions between `std::io::Error` and `ShellError` as
they often removed important information and were used too broadly to
handle I/O errors. This also removed the problematic implementation
found here:

7ea4895513/crates/nu-protocol/src/errors/shell_error.rs (L1534-L1583)

which hid some downcasting from I/O errors and made it hard to trace
where `ShellError` was converted into `std::io::Error`. To address this,
I introduced a new struct called `ShellErrorBridge`, which explicitly
defines this transfer behavior. With `ShellErrorBridge`, we can now
easily grep the codebase to locate and manage such conversions.

## Miscellaneous
- Removed the OS error added in #14640, as it’s no longer needed.
- Improved error messages in `glob_from` (#14679).
- Trying to open a directory with `open` caused a permissions denied
error (it's just what the OS provides). I added a `is_dir` check to
provide a better error in that case.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->

- Error outputs now include more detailed information and are formatted
differently, including updated error codes.
- The structure of `ShellError` has changed, requiring plugin authors
and embedders to update their implementations.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

I updated tests to account for the new I/O error structure and
formatting changes.

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->

This PR closes #7695 and closes #14892 and partially addresses #4323 and
#10698.

---------

Co-authored-by: Darren Schroeder <343840+fdncred@users.noreply.github.com>
This commit is contained in:
Piepmatz 2025-01-28 23:03:31 +01:00 committed by GitHub
parent ec1f7deb23
commit 66bc0542e0
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
105 changed files with 1944 additions and 1052 deletions

View File

@ -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)),
}
}

View File

@ -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<PipelineData, ShellError> {
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(&current_history_path)? {
if let Some(bak_path) = backup(&current_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<HistoryItem, ShellErr
hostname: get(rec, fields::HOSTNAME, |v| Ok(v.as_str()?.to_owned()))?,
cwd: get(rec, fields::CWD, |v| Ok(v.as_str()?.to_owned()))?,
exit_status: get(rec, fields::EXIT_STATUS, |v| v.as_int())?,
duration: get(rec, fields::DURATION, duration_from_value)?,
duration: get(rec, fields::DURATION, |v| duration_from_value(v, span))?,
more_info: None,
// TODO: Currently reedline doesn't let you create session IDs.
session_id: None,
@ -232,19 +234,21 @@ fn item_from_record(mut rec: Record, span: Span) -> Result<HistoryItem, ShellErr
Ok(item)
}
fn duration_from_value(v: Value) -> Result<std::time::Duration, ShellError> {
fn duration_from_value(v: Value, span: Span) -> Result<std::time::Duration, ShellError> {
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<PathBuf, ShellError> {
fn find_backup_path(path: &Path, span: Span) -> Result<PathBuf, ShellError> {
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<PathBuf, ShellError> {
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<Option<PathBuf>, ShellError> {
fn backup(path: &Path, span: Span) -> Result<Option<PathBuf>, 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());
}
}

View File

@ -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<Value, ShellError> {
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<Value, ShellError> {
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<Value, ShellError> {
_ => "".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<Value, ShellError> {
);
}
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()))
}

View File

@ -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<Spanned<String>>) {
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<Span
} else {
report_shell_error(
engine_state,
&ShellError::GenericError {
error: format!(
"Error while opening plugin registry file: {}",
plugin_path.display()
),
msg: "plugin path defined here".into(),
span,
help: None,
inner: vec![err.into()],
},
&ShellError::Io(IoError::new_internal_with_path(
err.kind(),
"Could not open plugin registry file",
nu_protocol::location!(),
plugin_path,
)),
);
return;
}
@ -234,8 +230,8 @@ pub fn eval_config_contents(
#[cfg(feature = "plugin")]
pub fn migrate_old_plugin_file(engine_state: &EngineState) -> 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(

View File

@ -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(),

View File

@ -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;
}

View File

@ -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;

View File

@ -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
}
};

View File

@ -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

View File

@ -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),
)?;

View File

@ -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 {

View File

@ -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 {

View File

@ -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;

View File

@ -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<Self, ShellError> {
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![],
})
}
})

View File

@ -60,6 +60,8 @@ pub(super) fn start_editor(
call: &Call,
) -> Result<PipelineData, ShellError> {
// 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)?;

View File

@ -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",
)));
}
}
}

View File

@ -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() {

View File

@ -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())
}
}
}

View File

@ -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<Box<dyn Iterator<Item = Result<PathBuf, ShellError>> + 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::<Vec<_>>();

View File

@ -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),

View File

@ -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(

View File

@ -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"

View File

@ -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<File, ShellError> {
let file = match (append, path.exists()) {
(true, true) => std::fs::OpenOptions::new().append(true).open(path),
let file: Result<File, nu_protocol::shell_error::io::ErrorKind> = 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 {

View File

@ -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<PathBuf> = Vec::new();
for v in exp_files {

View File

@ -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<PathBuf> = Vec::new();
for v in exp_files {

View File

@ -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(),

View File

@ -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) => {}

View File

@ -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<PipelineData, ShellError> {
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,

View File

@ -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 {

View File

@ -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 })

View File

@ -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())
})
})?;

View File

@ -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));
}

View File

@ -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<PipelineData, ShellError> {
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<Closure> = 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();

View File

@ -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 {

View File

@ -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"),

View File

@ -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<WriteError> 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,
}
}

View File

@ -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())

View File

@ -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)
},
)

View File

@ -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.

View File

@ -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::<std::io::Error>() else {
break 'io generic_network_failure();
};
ShellError::Io(IoError::new(io_error.kind(), span, None))
}
_ => generic_network_failure()
}
}
}
}

View File

@ -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<PipelineData, ShellError> {
let from_io_error = IoError::factory(call.head, None);
let start_port: Option<Spanned<usize>> = call.opt(engine_state, stack, 0)?;
let end_port: Option<Spanned<usize>> = 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())
}

View File

@ -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,
)

View File

@ -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<PipelineData, ShellError> {
let path: Option<String> = 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,
);

View File

@ -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),
}
}

View File

@ -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<PipelineData, ShellError> {
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)?;
}
};

View File

@ -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<Self, ShellError> {
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)
}

View File

@ -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()),

View File

@ -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<DeferredConsoleRestore, ShellError> {
fn enable_events(&self, span: Span) -> Result<DeferredConsoleRestore, ShellError> {
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 {

View File

@ -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,
)
})?,
)
};

View File

@ -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<Vec<u8>> = 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;

View File

@ -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 {

View File

@ -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<PipelineData, ShellError> {
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),
))),
}
}

View File

@ -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<Spanned<String>> = 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![];

View File

@ -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(())

View File

@ -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))

View File

@ -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(),
"

View File

@ -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.

View File

@ -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.

View File

@ -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"));
})
}

View File

@ -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"));
}

View File

@ -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"));
}

View File

@ -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)";

View File

@ -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"));
}

View File

@ -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"));
}

View File

@ -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"));
}

View File

@ -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"));
}

View File

@ -172,7 +172,7 @@ fn file_not_exist() {
"
));
assert!(actual.err.contains("file not found"));
assert!(actual.err.contains("nu::shell::io::not_found"));
})
}

View File

@ -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]

View File

@ -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"));
}

View File

@ -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));
}
});
}

View File

@ -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"));
}

View File

@ -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,
};

View File

@ -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<PathBuf,
// We're using `canonicalize_with` instead of `fs::canonicalize()` because
// we still need to simplify Windows paths. "." is safe because `cwd` should
// be an absolute path already.
canonicalize_with(&cwd, ".").map_err(|_| ShellError::DirectoryNotFound {
dir: cwd.to_string_lossy().to_string(),
span: Span::unknown(),
canonicalize_with(&cwd, ".").map_err(|err| {
ShellError::Io(IoError::new(
err.kind(),
Span::unknown(),
PathBuf::from(cwd),
))
})
}
@ -234,9 +238,12 @@ pub fn current_dir_const(working_set: &StateWorkingSet) -> Result<PathBuf, Shell
// We're using `canonicalize_with` instead of `fs::canonicalize()` because
// we still need to simplify Windows paths. "." is safe because `cwd` should
// be an absolute path already.
canonicalize_with(&cwd, ".").map_err(|_| ShellError::DirectoryNotFound {
dir: cwd.to_string_lossy().to_string(),
span: Span::unknown(),
canonicalize_with(&cwd, ".").map_err(|err| {
ShellError::Io(IoError::new(
err.kind(),
Span::unknown(),
PathBuf::from(cwd),
))
})
}

View File

@ -8,9 +8,10 @@ use nu_protocol::{
Argument, Closure, EngineState, ErrorHandler, Matcher, Redirection, Stack, StateWorkingSet,
},
ir::{Call, DataSlice, Instruction, IrAstRef, IrBlock, Literal, RedirectMode},
DataSource, DeclId, ErrSpan, Flag, IntoPipelineData, IntoSpanned, ListStream, OutDest,
PipelineData, PipelineMetadata, PositionalArg, Range, Record, RegId, ShellError, Signals,
Signature, Span, Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
shell_error::io::IoError,
DataSource, DeclId, Flag, IntoPipelineData, IntoSpanned, ListStream, OutDest, PipelineData,
PipelineMetadata, PositionalArg, Range, Record, RegId, ShellError, Signals, Signature, Span,
Spanned, Type, Value, VarId, ENV_VARIABLE_ID,
};
use nu_utils::IgnoreCaseExt;
@ -1489,8 +1490,8 @@ fn open_file(ctx: &EvalContext<'_>, path: &Value, append: bool) -> Result<Arc<Fi
}
let file = options
.create(true)
.open(path_expanded)
.err_span(path.span())?;
.open(&path_expanded)
.map_err(|err| IoError::new(err.kind(), path.span(), path_expanded))?;
Ok(Arc::new(file))
}

View File

@ -1,9 +1,8 @@
use nu_glob::MatchOptions;
use nu_path::{canonicalize_with, expand_path_with};
use nu_protocol::{NuGlob, ShellError, Span, Spanned};
use nu_protocol::{shell_error::io::IoError, NuGlob, ShellError, Span, Spanned};
use std::{
fs,
io::ErrorKind,
path::{Component, Path, PathBuf},
};
@ -28,6 +27,7 @@ pub fn glob_from(
ShellError,
> {
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)

View File

@ -14,16 +14,24 @@ pub fn run_command_with_value(
stack: &mut Stack,
) -> Result<PipelineData, ShellError> {
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],
});
}

View File

@ -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,
)));
}
}
}

View File

@ -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",
)
})?,
)),
}

View File

@ -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 {

View File

@ -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<PluginInput> 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<PluginOutput> 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<PluginOutput> 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<T>(err: serde_json::Error) -> Result<Option<T>, 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(),

View File

@ -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<T>(err: rmp_serde::decode::Error) -> Result<Option<T>, 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!(),
)))
}
}
_ => {

View File

@ -36,18 +36,18 @@ macro_rules! generate_tests {
let mut buffered = std::io::BufReader::new(ErrorProducer);
match Encoder::<PluginInput>::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::<PluginOutput>::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);

View File

@ -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()

View File

@ -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) {

View File

@ -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(

View File

@ -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<T: Clone + Send> PluginWrite<T> for FakePluginWrite<T> {
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)

View File

@ -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()?;

View File

@ -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) {

View File

@ -387,7 +387,7 @@ pub enum ServePluginError {
impl From<ShellError> 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),
}

View File

@ -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)
}

View File

@ -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 {

View File

@ -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;

View File

@ -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<io::Error>`](From) and [`Into<io::Error>`](Into) directly on
/// `ShellError`.
#[derive(Debug, Clone, PartialEq, Error, Serialize, Deserialize)]
#[error("{0}")]
pub struct ShellErrorBridge(pub ShellError);
impl TryFrom<std::io::Error> for ShellErrorBridge {
type Error = std::io::Error;
fn try_from(value: std::io::Error) -> Result<Self, Self::Error> {
let kind = value.kind();
value
.downcast()
.inspect(|_| debug_assert_eq!(kind, std::io::ErrorKind::Other))
}
}
impl From<ShellErrorBridge> 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);
}

View File

@ -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<PathBuf>,
/// 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<String>,
/// 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<String>,
}
#[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<ErrorKind>, span: Span, path: impl Into<Option<PathBuf>>) -> 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<ErrorKind>,
span: Span,
path: impl Into<Option<PathBuf>>,
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<ErrorKind>,
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<ErrorKind>,
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<Option<&'p Path>>,
{
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<Box<dyn std::fmt::Display + 'a>> {
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<Box<dyn std::fmt::Display + 'a>> {
self.path
.as_ref()
.map(|path| format!("The error occurred at '{}'", path.display()))
.map(|s| Box::new(s) as Box<dyn std::fmt::Display>)
}
fn labels(&self) -> Option<Box<dyn Iterator<Item = miette::LabeledSpan> + '_>> {
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<IoError> for ShellError {
fn from(value: IoError) -> Self {
ShellError::Io(value)
}
}
impl From<IoError> for std::io::Error {
fn from(value: IoError) -> Self {
Self::new(value.kind.into(), value)
}
}
impl From<std::io::ErrorKind> for ErrorKind {
fn from(value: std::io::ErrorKind) -> Self {
ErrorKind::Std(value)
}
}
impl From<ErrorKind> for std::io::ErrorKind {
fn from(value: ErrorKind) -> Self {
match value {
ErrorKind::Std(error_kind) => error_kind,
_ => std::io::ErrorKind::Other,
}
}
}

View File

@ -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);
}

View File

@ -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<io::Error> 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<Spanned<io::Error>> for ShellError {
fn from(error: Spanned<io::Error>) -> 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<ShellError> for io::Error {
fn from(error: ShellError) -> Self {
io::Error::new(io::ErrorKind::Other, error)
}
}
impl From<Box<dyn std::error::Error>> for ShellError {
fn from(error: Box<dyn std::error::Error>) -> ShellError {
ShellError::IOError {
ShellError::GenericError {
error: format!("{error:?}"),
msg: error.to_string(),
span: None,
help: None,
inner: vec![],
}
}
}
impl From<Box<dyn std::error::Error + Send + Sync>> for ShellError {
fn from(error: Box<dyn std::error::Error + Send + Sync>) -> 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<std::io::Error> 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<Spanned<std::io::Error>> for ShellError {
fn from(_: Spanned<std::io::Error>) -> Self {
unimplemented!("This implementation is defined in the test module to ensure no other implementation exists.")
}
}
impl From<ShellError> for std::io::Error {
fn from(_: ShellError) -> Self {
unimplemented!("This implementation is defined in the test module to ensure no other implementation exists.")
}
}
}

View File

@ -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,
)

View File

@ -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<Self, ShellError> {
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<Vec<u8>, 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<usize> {
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<Option<String>, (Vec<u8>, 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<u64, ShellError> {
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<u64, ShellError> {
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;
}

View File

@ -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",
)
})?
}
}

View File

@ -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<Vec<u8>, 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::<io::Error>() {
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<ProcessOutput, ShellError> {
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::<io::Error>() {
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)
};

Some files were not shown because too many files have changed in this diff Show More