mirror of
https://github.com/nushell/nushell.git
synced 2025-05-18 00:40:48 +02:00
# Description After this pr, nushell is able to raise errors with a backtrace, which should make users easier to debug. To enable the feature, users need to set env variable via `$env.NU_BACKTRACE = 1`. But yeah it might not work perfectly, there are some corner cases which might not be handled. I think it should close #13379 in another way. ### About the change The implementation mostly contained with 2 parts: 1. introduce a new `ChainedError` struct as well as a new `ShellError::ChainedError` variant. If `eval_instruction` returned an error, it converts the error to `ShellError::ChainedError`. `ChainedError` struct is responsable to display errors properly. It needs to handle the following 2 cases: - if we run a function which runs `error make` internally, it needs to display the error itself along with caller span. - if we run a `error make` directly, or some commands directly returns an error, we just want nushell raise an error about `error make`. 2. Attach caller spans to `ListStream` and `ByteStream`, because they are lazy streams, and *only* contains the span that runs it directly(like `^false`, for example), so nushell needs to add all caller spans to the stream. For example: in `def a [] { ^false }; def b [] { a; 33 }; b`, when we run `b`, which runs `a`, which runs `^false`, the `ByteStream` only contains the span of `^false`, we need to make it contains the span of `a`, so nushell is able to get all spans if something bad happened. This behavior is happened after running `Instruction::Call`, if it returns a `ByteStream` and `ListStream`, it will call `push_caller_span` method to attach call spans. # User-Facing Changes It's better to demostrate how it works by examples, given the following definition: ```nushell > $env.NU_BACKTRACE = 1 > def a [x] { if $x == 3 { error make {msg: 'a custom error'}}} > def a_2 [x] { if $x == 3 { ^false } else { $x } } > def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } > def b [--list-stream --external] { if $external == true { # error with non-zero exit code, which is generated from external command. a_2 1; a_2 3; a_2 2 } else if $list_stream == true { # error generated by list-stream a_3 1; a_3 3; a_3 2 } else { # error generated by command directly a 1; a 2; a 3 } } ``` Run `b` directly shows the following error: <details> ```nushell Error: chained_error × oops ╭─[entry #27:1:1] 1 │ b · ┬ · ╰── error happened when running this ╰──── Error: chained_error × oops ╭─[entry #26:10:19] 9 │ # error generated by command directly 10 │ a 1; a 2; a 3 · ┬ · ╰── error happened when running this 11 │ } ╰──── Error: × a custom error ╭─[entry #6:1:26] 1 │ def a [x] { if $x == 3 { error make {msg: 'a custom error'}}} · ─────┬──── · ╰── originates from here ╰──── ``` </details> Run `b --list-stream` shows the following error <details> ```nushell Error: chained_error × oops ╭─[entry #28:1:1] 1 │ b --list-stream · ┬ · ╰── error happened when running this ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #26:7:16] 6 │ # error generated by list-stream 7 │ a_3 1; a_3 3; a_3 2 · ─┬─ · ╰── source value 8 │ } else { ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #23:1:29] 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } · ┬ · ╰── source value ╰──── Error: × a custom error inside list stream ╭─[entry #23:1:44] 1 │ def a_3 [x] { if $x == 3 { [1 2 3] | each {error make {msg: 'a custom error inside list stream'} } } } · ─────┬──── · ╰── originates from here ╰──── ``` </details> Run `b --external` shows the following error: <details> ```nushell Error: chained_error × oops ╭─[entry #29:1:1] 1 │ b --external · ┬ · ╰── error happened when running this ╰──── Error: nu:🐚:eval_block_with_input × Eval block failed with pipeline input ╭─[entry #26:4:16] 3 │ # error with non-zero exit code, which is generated from external command. 4 │ a_2 1; a_2 3; a_2 2 · ─┬─ · ╰── source value 5 │ } else if $list_stream == true { ╰──── Error: nu:🐚:non_zero_exit_code × External command had a non-zero exit code ╭─[entry #7:1:29] 1 │ def a_2 [x] { if $x == 3 { ^false } else { $x } } · ──┬── · ╰── exited with code 1 ╰──── ``` </details> It also added a message to guide the usage of NU_BACKTRACE, see the last line in the following example: ```shell ls asdfasd Error: nu:🐚:io::not_found × I/O error ╰─▶ × Entity not found ╭─[entry #17:1:4] 1 │ ls asdfasd · ───┬─── · ╰── Entity not found ╰──── help: The error occurred at '/home/windsoilder/projects/nushell/asdfasd' set the `NU_BACKTRACE=1` environment variable to display a backtrace. ``` # Tests + Formatting Added some tests for the behavior. # After Submitting
184 lines
5.2 KiB
Rust
184 lines
5.2 KiB
Rust
//! Module managing the streaming of individual [`Value`]s as a [`ListStream`] between pipeline
|
|
//! elements
|
|
//!
|
|
//! For more general infos regarding our pipelining model refer to [`PipelineData`]
|
|
use crate::{Config, PipelineData, ShellError, Signals, Span, Value};
|
|
use std::fmt::Debug;
|
|
|
|
pub type ValueIterator = Box<dyn Iterator<Item = Value> + Send + 'static>;
|
|
|
|
/// A potentially infinite, interruptible stream of [`Value`]s.
|
|
///
|
|
/// In practice, a "stream" here means anything which can be iterated and produces Values.
|
|
/// Like other iterators in Rust, observing values from this stream will drain the items
|
|
/// as you view them and the stream cannot be replayed.
|
|
pub struct ListStream {
|
|
stream: ValueIterator,
|
|
span: Span,
|
|
caller_spans: Vec<Span>,
|
|
}
|
|
|
|
impl ListStream {
|
|
/// Create a new [`ListStream`] from a [`Value`] `Iterator`.
|
|
pub fn new(
|
|
iter: impl Iterator<Item = Value> + Send + 'static,
|
|
span: Span,
|
|
signals: Signals,
|
|
) -> Self {
|
|
Self {
|
|
stream: Box::new(InterruptIter::new(iter, signals)),
|
|
span,
|
|
caller_spans: vec![],
|
|
}
|
|
}
|
|
|
|
/// Returns the [`Span`] associated with this [`ListStream`].
|
|
pub fn span(&self) -> Span {
|
|
self.span
|
|
}
|
|
|
|
/// Push a caller [`Span`] to the bytestream, it's useful to construct a backtrace.
|
|
pub fn push_caller_span(&mut self, span: Span) {
|
|
if span != self.span {
|
|
self.caller_spans.push(span)
|
|
}
|
|
}
|
|
|
|
/// Get all caller [`Span`], it's useful to construct a backtrace.
|
|
pub fn get_caller_spans(&self) -> &Vec<Span> {
|
|
&self.caller_spans
|
|
}
|
|
|
|
/// Changes the [`Span`] associated with this [`ListStream`].
|
|
pub fn with_span(mut self, span: Span) -> Self {
|
|
self.span = span;
|
|
self
|
|
}
|
|
|
|
/// Convert a [`ListStream`] into its inner [`Value`] `Iterator`.
|
|
pub fn into_inner(self) -> ValueIterator {
|
|
self.stream
|
|
}
|
|
|
|
/// Take a single value from the inner `Iterator`, modifying the stream.
|
|
pub fn next_value(&mut self) -> Option<Value> {
|
|
self.stream.next()
|
|
}
|
|
|
|
/// Converts each value in a [`ListStream`] into a string and then joins the strings together
|
|
/// using the given separator.
|
|
pub fn into_string(self, separator: &str, config: &Config) -> String {
|
|
self.into_iter()
|
|
.map(|val| val.to_expanded_string(", ", config))
|
|
.collect::<Vec<String>>()
|
|
.join(separator)
|
|
}
|
|
|
|
/// Collect the values of a [`ListStream`] into a list [`Value`].
|
|
pub fn into_value(self) -> Value {
|
|
Value::list(self.stream.collect(), self.span)
|
|
}
|
|
|
|
/// Consume all values in the stream, returning an error if any of the values is a `Value::Error`.
|
|
pub fn drain(self) -> Result<(), ShellError> {
|
|
for next in self {
|
|
if let Value::Error { error, .. } = next {
|
|
return Err(*error);
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Modify the inner iterator of a [`ListStream`] using a function.
|
|
///
|
|
/// This can be used to call any number of standard iterator functions on the [`ListStream`].
|
|
/// E.g., `take`, `filter`, `step_by`, and more.
|
|
///
|
|
/// ```
|
|
/// use nu_protocol::{ListStream, Signals, Span, Value};
|
|
///
|
|
/// let span = Span::unknown();
|
|
/// let stream = ListStream::new(std::iter::repeat(Value::int(0, span)), span, Signals::empty());
|
|
/// let new_stream = stream.modify(|iter| iter.take(100));
|
|
/// ```
|
|
pub fn modify<I>(self, f: impl FnOnce(ValueIterator) -> I) -> Self
|
|
where
|
|
I: Iterator<Item = Value> + Send + 'static,
|
|
{
|
|
Self {
|
|
stream: Box::new(f(self.stream)),
|
|
span: self.span,
|
|
caller_spans: self.caller_spans,
|
|
}
|
|
}
|
|
|
|
/// Create a new [`ListStream`] whose values are the results of applying the given function
|
|
/// to each of the values in the original [`ListStream`].
|
|
pub fn map(self, mapping: impl FnMut(Value) -> Value + Send + 'static) -> Self {
|
|
self.modify(|iter| iter.map(mapping))
|
|
}
|
|
}
|
|
|
|
impl Debug for ListStream {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("ListStream").finish()
|
|
}
|
|
}
|
|
|
|
impl IntoIterator for ListStream {
|
|
type Item = Value;
|
|
|
|
type IntoIter = IntoIter;
|
|
|
|
fn into_iter(self) -> Self::IntoIter {
|
|
IntoIter {
|
|
stream: self.into_inner(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<ListStream> for PipelineData {
|
|
fn from(stream: ListStream) -> Self {
|
|
Self::ListStream(stream, None)
|
|
}
|
|
}
|
|
|
|
pub struct IntoIter {
|
|
stream: ValueIterator,
|
|
}
|
|
|
|
impl Iterator for IntoIter {
|
|
type Item = Value;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
self.stream.next()
|
|
}
|
|
}
|
|
|
|
struct InterruptIter<I: Iterator> {
|
|
iter: I,
|
|
signals: Signals,
|
|
}
|
|
|
|
impl<I: Iterator> InterruptIter<I> {
|
|
fn new(iter: I, signals: Signals) -> Self {
|
|
Self { iter, signals }
|
|
}
|
|
}
|
|
|
|
impl<I: Iterator> Iterator for InterruptIter<I> {
|
|
type Item = <I as Iterator>::Item;
|
|
|
|
fn next(&mut self) -> Option<Self::Item> {
|
|
if self.signals.interrupted() {
|
|
None
|
|
} else {
|
|
self.iter.next()
|
|
}
|
|
}
|
|
|
|
fn size_hint(&self) -> (usize, Option<usize>) {
|
|
self.iter.size_hint()
|
|
}
|
|
}
|