mirror of
https://github.com/nushell/nushell.git
synced 2025-06-30 22:50:14 +02:00
Enable nushell error with backtrace (#14945)
# 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
This commit is contained in:
@ -187,6 +187,7 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
|
||||
// Program counter, starts at zero.
|
||||
let mut pc = 0;
|
||||
let need_backtrace = ctx.engine_state.get_env_var("NU_BACKTRACE").is_some();
|
||||
|
||||
while pc < ir_block.instructions.len() {
|
||||
let instruction = &ir_block.instructions[pc];
|
||||
@ -195,7 +196,7 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
|
||||
D::enter_instruction(ctx.engine_state, ir_block, pc, ctx.registers);
|
||||
|
||||
let result = eval_instruction::<D>(ctx, instruction, span, ast);
|
||||
let result = eval_instruction::<D>(ctx, instruction, span, ast, need_backtrace);
|
||||
|
||||
D::leave_instruction(
|
||||
ctx.engine_state,
|
||||
@ -228,8 +229,10 @@ fn eval_ir_block_impl<D: DebugContext>(
|
||||
// If an error handler is set, branch there
|
||||
prepare_error_handler(ctx, error_handler, Some(err.into_spanned(*span)));
|
||||
pc = error_handler.handler_index;
|
||||
} else if need_backtrace {
|
||||
let err = ShellError::into_chainned(err, *span);
|
||||
return Err(err);
|
||||
} else {
|
||||
// If not, exit the block with the error
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
@ -285,6 +288,7 @@ fn eval_instruction<D: DebugContext>(
|
||||
instruction: &Instruction,
|
||||
span: &Span,
|
||||
ast: &Option<IrAstRef>,
|
||||
need_backtrace: bool,
|
||||
) -> Result<InstructionResult, ShellError> {
|
||||
use self::InstructionResult::*;
|
||||
|
||||
@ -548,7 +552,14 @@ fn eval_instruction<D: DebugContext>(
|
||||
}
|
||||
Instruction::Call { decl_id, src_dst } => {
|
||||
let input = ctx.take_reg(*src_dst);
|
||||
let result = eval_call::<D>(ctx, *decl_id, *span, input)?;
|
||||
let mut result = eval_call::<D>(ctx, *decl_id, *span, input)?;
|
||||
if need_backtrace {
|
||||
match &mut result {
|
||||
PipelineData::ByteStream(s, ..) => s.push_caller_span(*span),
|
||||
PipelineData::ListStream(s, ..) => s.push_caller_span(*span),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
ctx.put_reg(*src_dst, result);
|
||||
Ok(Continue)
|
||||
}
|
||||
@ -1457,14 +1468,40 @@ fn drain(ctx: &mut EvalContext<'_>, data: PipelineData) -> Result<InstructionRes
|
||||
match data {
|
||||
PipelineData::ByteStream(stream, ..) => {
|
||||
let span = stream.span();
|
||||
if let Err(err) = stream.drain() {
|
||||
let callback_spans = stream.get_caller_spans().clone();
|
||||
if let Err(mut err) = stream.drain() {
|
||||
ctx.stack.set_last_error(&err);
|
||||
return Err(err);
|
||||
if callback_spans.is_empty() {
|
||||
return Err(err);
|
||||
} else {
|
||||
for s in callback_spans {
|
||||
err = ShellError::EvalBlockWithInput {
|
||||
span: s,
|
||||
sources: vec![err],
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
} else {
|
||||
ctx.stack.set_last_exit_code(0, span);
|
||||
}
|
||||
}
|
||||
PipelineData::ListStream(stream, ..) => stream.drain()?,
|
||||
PipelineData::ListStream(stream, ..) => {
|
||||
let callback_spans = stream.get_caller_spans().clone();
|
||||
if let Err(mut err) = stream.drain() {
|
||||
if callback_spans.is_empty() {
|
||||
return Err(err);
|
||||
} else {
|
||||
for s in callback_spans {
|
||||
err = ShellError::EvalBlockWithInput {
|
||||
span: s,
|
||||
sources: vec![err],
|
||||
}
|
||||
}
|
||||
return Err(err);
|
||||
}
|
||||
}
|
||||
}
|
||||
PipelineData::Value(..) | PipelineData::Empty => {}
|
||||
}
|
||||
Ok(Continue)
|
||||
|
Reference in New Issue
Block a user