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
105 changed files with 1944 additions and 1052 deletions

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

View File

@ -115,10 +115,17 @@ impl Span {
Self { start: 0, end: 0 }
}
/// Span for testing purposes.
///
/// The provided span does not point into any known source but is unequal to [`Span::unknown()`].
///
/// Note: Only use this for test data, *not* live data, as it will point into unknown source
/// when used in errors.
/// when used in errors
pub const fn test_data() -> Self {
Self::unknown()
Self {
start: usize::MAX / 2,
end: usize::MAX / 2,
}
}
pub fn offset(&self, offset: usize) -> Self {
@ -215,26 +222,14 @@ impl From<Span> for SourceSpan {
}
}
/// An extension trait for `Result`, which adds a span to the error type.
/// An extension trait for [`Result`], which adds a span to the error type.
///
/// This trait might be removed later, since the old [`Spanned<std::io::Error>`] to [`ShellError`]
/// conversion was replaced by [`IoError`](io_error::IoError).
pub trait ErrSpan {
type Result;
/// Add the given span to the error type `E`, turning it into a `Spanned<E>`.
///
/// Some auto-conversion methods to `ShellError` from other error types are available on spanned
/// errors, to give users better information about where an error came from. For example, it is
/// preferred when working with `std::io::Error`:
///
/// ```no_run
/// use nu_protocol::{ErrSpan, ShellError, Span};
/// use std::io::Read;
///
/// fn read_from(mut reader: impl Read, span: Span) -> Result<Vec<u8>, ShellError> {
/// let mut vec = vec![];
/// reader.read_to_end(&mut vec).err_span(span)?;
/// Ok(vec)
/// }
/// ```
/// Adds the given span to the error type, turning it into a [`Spanned<E>`].
fn err_span(self, span: Span) -> Self::Result;
}