forked from extern/nushell
explain command (#7957)
# Description The purpose of this PR is to introduce the `inspect` command. A command that is used to inspect, but not run, a pipeline to ensure everything looks right. This is meant as a debugging tool. This is some hackery, so don't laugh. :) 
This commit is contained in:
parent
16b99ed0ba
commit
c31225fdcf
@ -174,6 +174,7 @@ pub fn create_default_context() -> EngineState {
|
|||||||
bind_command! {
|
bind_command! {
|
||||||
Benchmark,
|
Benchmark,
|
||||||
Complete,
|
Complete,
|
||||||
|
Explain,
|
||||||
External,
|
External,
|
||||||
NuCheck,
|
NuCheck,
|
||||||
Sys,
|
Sys,
|
||||||
|
330
crates/nu-command/src/system/explain.rs
Normal file
330
crates/nu-command/src/system/explain.rs
Normal file
@ -0,0 +1,330 @@
|
|||||||
|
use nu_engine::{eval_expression, CallExt};
|
||||||
|
use nu_protocol::ast::{Argument, Block, Call, Expr, Expression};
|
||||||
|
use nu_protocol::engine::{Closure, Command, EngineState, Stack};
|
||||||
|
use nu_protocol::{
|
||||||
|
Category, Example, IntoInterruptiblePipelineData, PipelineData, ShellError, Signature, Span,
|
||||||
|
SyntaxShape, Type, Value,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub struct Explain;
|
||||||
|
|
||||||
|
impl Command for Explain {
|
||||||
|
fn name(&self) -> &str {
|
||||||
|
"explain"
|
||||||
|
}
|
||||||
|
|
||||||
|
fn usage(&self) -> &str {
|
||||||
|
"Explain closure contents."
|
||||||
|
}
|
||||||
|
|
||||||
|
fn signature(&self) -> nu_protocol::Signature {
|
||||||
|
Signature::build("explain")
|
||||||
|
.required(
|
||||||
|
"closure",
|
||||||
|
SyntaxShape::Closure(Some(vec![SyntaxShape::Any])),
|
||||||
|
"the closure to run",
|
||||||
|
)
|
||||||
|
.input_output_types(vec![(Type::Any, Type::Any), (Type::Nothing, Type::Any)])
|
||||||
|
.allow_variants_without_examples(true)
|
||||||
|
.category(Category::Debug)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(
|
||||||
|
&self,
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
call: &Call,
|
||||||
|
_input: PipelineData,
|
||||||
|
) -> Result<nu_protocol::PipelineData, nu_protocol::ShellError> {
|
||||||
|
// This was all delightfully stolen from benchmark :)
|
||||||
|
let capture_block: Closure = call.req(engine_state, stack, 0)?;
|
||||||
|
let block = engine_state.get_block(capture_block.block_id);
|
||||||
|
let ctrlc = engine_state.ctrlc.clone();
|
||||||
|
let mut stack = stack.captures_to_stack(&capture_block.captures);
|
||||||
|
|
||||||
|
let elements = get_pipeline_elements(engine_state, &mut stack, block)?;
|
||||||
|
|
||||||
|
Ok(elements.into_pipeline_data(ctrlc))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn examples(&self) -> Vec<Example> {
|
||||||
|
vec![Example {
|
||||||
|
description: "Explain a command within a closure",
|
||||||
|
example: "explain { ls | sort-by name type -i | get name } | table -e",
|
||||||
|
result: None,
|
||||||
|
}]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_pipeline_elements(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
block: &Block,
|
||||||
|
) -> Result<Vec<Value>, ShellError> {
|
||||||
|
let mut element_values = vec![];
|
||||||
|
let span = Span::test_data();
|
||||||
|
|
||||||
|
for (pipeline_idx, pipeline) in block.pipelines.iter().enumerate() {
|
||||||
|
let mut i = 0;
|
||||||
|
while i < pipeline.elements.len() {
|
||||||
|
let pipeline_element = &pipeline.elements[i];
|
||||||
|
let pipeline_expression = pipeline_element.expression().clone();
|
||||||
|
let pipeline_span = &pipeline_element.span();
|
||||||
|
let element_str =
|
||||||
|
String::from_utf8_lossy(engine_state.get_span_contents(pipeline_span));
|
||||||
|
let value = Value::string(element_str.to_string(), *pipeline_span);
|
||||||
|
let expr = pipeline_expression.expr.clone();
|
||||||
|
let (command_name, command_args_value) = if let Expr::Call(call) = expr {
|
||||||
|
let command = engine_state.get_decl(call.decl_id);
|
||||||
|
(
|
||||||
|
command.name().to_string(),
|
||||||
|
get_arguments(engine_state, stack, *call),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
("no-op".to_string(), vec![])
|
||||||
|
};
|
||||||
|
let index = format!("{pipeline_idx}_{i}");
|
||||||
|
let value_type = value.get_type();
|
||||||
|
let value_span = value.span()?;
|
||||||
|
let value_span_start = value_span.start as i64;
|
||||||
|
let value_span_end = value_span.end as i64;
|
||||||
|
let command_name = command_name;
|
||||||
|
|
||||||
|
let rec = Value::Record {
|
||||||
|
cols: vec![
|
||||||
|
"cmd_index".to_string(),
|
||||||
|
"cmd_name".to_string(),
|
||||||
|
"type".to_string(),
|
||||||
|
"cmd_args".to_string(),
|
||||||
|
"span_start".to_string(),
|
||||||
|
"span_end".to_string(),
|
||||||
|
],
|
||||||
|
vals: vec![
|
||||||
|
Value::string(index, span),
|
||||||
|
Value::string(command_name, value_span),
|
||||||
|
Value::string(value_type.to_string(), span),
|
||||||
|
Value::List {
|
||||||
|
vals: command_args_value,
|
||||||
|
span: value_span,
|
||||||
|
},
|
||||||
|
Value::int(value_span_start, span),
|
||||||
|
Value::int(value_span_end, span),
|
||||||
|
],
|
||||||
|
span: value_span,
|
||||||
|
};
|
||||||
|
element_values.push(rec);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(element_values)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_arguments(engine_state: &EngineState, stack: &mut Stack, call: Call) -> Vec<Value> {
|
||||||
|
let mut arg_value = vec![];
|
||||||
|
let span = Span::test_data();
|
||||||
|
for arg in &call.arguments {
|
||||||
|
match arg {
|
||||||
|
// I think the second argument to Argument::Named is the short name, but I'm not really sure.
|
||||||
|
// Please fix it if it's wrong. :)
|
||||||
|
Argument::Named((name, short, opt_expr)) => {
|
||||||
|
let arg_type = "named";
|
||||||
|
let arg_value_name = name.item.clone();
|
||||||
|
let arg_value_name_span_start = name.span.start as i64;
|
||||||
|
let arg_value_name_span_end = name.span.end as i64;
|
||||||
|
|
||||||
|
let rec = Value::Record {
|
||||||
|
cols: vec![
|
||||||
|
"arg_type".to_string(),
|
||||||
|
"name".to_string(),
|
||||||
|
"type".to_string(),
|
||||||
|
"span_start".to_string(),
|
||||||
|
"span_end".to_string(),
|
||||||
|
],
|
||||||
|
vals: vec![
|
||||||
|
Value::string(arg_type, span),
|
||||||
|
Value::string(arg_value_name, name.span),
|
||||||
|
Value::string("string".to_string(), span),
|
||||||
|
Value::int(arg_value_name_span_start, span),
|
||||||
|
Value::int(arg_value_name_span_end, span),
|
||||||
|
],
|
||||||
|
span: name.span,
|
||||||
|
};
|
||||||
|
arg_value.push(rec);
|
||||||
|
|
||||||
|
if let Some(shortcut) = short {
|
||||||
|
let arg_type = "short";
|
||||||
|
let arg_value_name = shortcut.item.clone();
|
||||||
|
let arg_value_name_span_start = shortcut.span.start as i64;
|
||||||
|
let arg_value_name_span_end = shortcut.span.end as i64;
|
||||||
|
|
||||||
|
let rec = Value::Record {
|
||||||
|
cols: vec![
|
||||||
|
"arg_type".to_string(),
|
||||||
|
"name".to_string(),
|
||||||
|
"type".to_string(),
|
||||||
|
"span_start".to_string(),
|
||||||
|
"span_end".to_string(),
|
||||||
|
],
|
||||||
|
vals: vec![
|
||||||
|
Value::string(arg_type, span),
|
||||||
|
Value::string(arg_value_name, shortcut.span),
|
||||||
|
Value::string("string".to_string(), span),
|
||||||
|
Value::int(arg_value_name_span_start, span),
|
||||||
|
Value::int(arg_value_name_span_end, span),
|
||||||
|
],
|
||||||
|
span: name.span,
|
||||||
|
};
|
||||||
|
arg_value.push(rec);
|
||||||
|
} else {
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(expression) = opt_expr {
|
||||||
|
let evaluated_expression =
|
||||||
|
get_expression_as_value(engine_state, stack, expression);
|
||||||
|
let arg_type = "expr";
|
||||||
|
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||||
|
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||||
|
let evaled_span = evaluated_expression.expect_span();
|
||||||
|
let arg_value_name_span_start = evaled_span.start as i64;
|
||||||
|
let arg_value_name_span_end = evaled_span.end as i64;
|
||||||
|
|
||||||
|
let rec = Value::Record {
|
||||||
|
cols: vec![
|
||||||
|
"arg_type".to_string(),
|
||||||
|
"name".to_string(),
|
||||||
|
"type".to_string(),
|
||||||
|
"span_start".to_string(),
|
||||||
|
"span_end".to_string(),
|
||||||
|
],
|
||||||
|
vals: vec![
|
||||||
|
Value::string(arg_type, span),
|
||||||
|
Value::string(arg_value_name, expression.span),
|
||||||
|
Value::string(arg_value_type, span),
|
||||||
|
Value::int(arg_value_name_span_start, span),
|
||||||
|
Value::int(arg_value_name_span_end, span),
|
||||||
|
],
|
||||||
|
span: expression.span,
|
||||||
|
};
|
||||||
|
arg_value.push(rec);
|
||||||
|
} else {
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Argument::Positional(inner_expr) => {
|
||||||
|
let arg_type = "positional";
|
||||||
|
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
|
||||||
|
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||||
|
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||||
|
let evaled_span = evaluated_expression.expect_span();
|
||||||
|
let arg_value_name_span_start = evaled_span.start as i64;
|
||||||
|
let arg_value_name_span_end = evaled_span.end as i64;
|
||||||
|
|
||||||
|
let rec = Value::Record {
|
||||||
|
cols: vec![
|
||||||
|
"arg_type".to_string(),
|
||||||
|
"name".to_string(),
|
||||||
|
"type".to_string(),
|
||||||
|
"span_start".to_string(),
|
||||||
|
"span_end".to_string(),
|
||||||
|
],
|
||||||
|
vals: vec![
|
||||||
|
Value::string(arg_type, span),
|
||||||
|
Value::string(arg_value_name, inner_expr.span),
|
||||||
|
Value::string(arg_value_type, span),
|
||||||
|
Value::int(arg_value_name_span_start, span),
|
||||||
|
Value::int(arg_value_name_span_end, span),
|
||||||
|
],
|
||||||
|
span: inner_expr.span,
|
||||||
|
};
|
||||||
|
arg_value.push(rec);
|
||||||
|
}
|
||||||
|
Argument::Unknown(inner_expr) => {
|
||||||
|
let arg_type = "unknown";
|
||||||
|
let evaluated_expression = get_expression_as_value(engine_state, stack, inner_expr);
|
||||||
|
let arg_value_name = debug_string_without_formatting(&evaluated_expression);
|
||||||
|
let arg_value_type = &evaluated_expression.get_type().to_string();
|
||||||
|
let evaled_span = evaluated_expression.expect_span();
|
||||||
|
let arg_value_name_span_start = evaled_span.start as i64;
|
||||||
|
let arg_value_name_span_end = evaled_span.end as i64;
|
||||||
|
|
||||||
|
let rec = Value::Record {
|
||||||
|
cols: vec![
|
||||||
|
"arg_type".to_string(),
|
||||||
|
"name".to_string(),
|
||||||
|
"type".to_string(),
|
||||||
|
"span_start".to_string(),
|
||||||
|
"span_end".to_string(),
|
||||||
|
],
|
||||||
|
vals: vec![
|
||||||
|
Value::string(arg_type, span),
|
||||||
|
Value::string(arg_value_name, inner_expr.span),
|
||||||
|
Value::string(arg_value_type, span),
|
||||||
|
Value::int(arg_value_name_span_start, span),
|
||||||
|
Value::int(arg_value_name_span_end, span),
|
||||||
|
],
|
||||||
|
span: inner_expr.span,
|
||||||
|
};
|
||||||
|
arg_value.push(rec);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
arg_value
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_expression_as_value(
|
||||||
|
engine_state: &EngineState,
|
||||||
|
stack: &mut Stack,
|
||||||
|
inner_expr: &Expression,
|
||||||
|
) -> Value {
|
||||||
|
match eval_expression(engine_state, stack, inner_expr) {
|
||||||
|
Ok(v) => v,
|
||||||
|
Err(error) => Value::Error { error },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn debug_string_without_formatting(value: &Value) -> String {
|
||||||
|
match value {
|
||||||
|
Value::Bool { val, .. } => val.to_string(),
|
||||||
|
Value::Int { val, .. } => val.to_string(),
|
||||||
|
Value::Float { val, .. } => val.to_string(),
|
||||||
|
Value::Filesize { val, .. } => val.to_string(),
|
||||||
|
Value::Duration { val, .. } => val.to_string(),
|
||||||
|
Value::Date { val, .. } => format!("{val:?}"),
|
||||||
|
Value::Range { val, .. } => {
|
||||||
|
format!(
|
||||||
|
"{}..{}",
|
||||||
|
debug_string_without_formatting(&val.from),
|
||||||
|
debug_string_without_formatting(&val.to)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Value::String { val, .. } => val.clone(),
|
||||||
|
Value::List { vals: val, .. } => format!(
|
||||||
|
"[{}]",
|
||||||
|
val.iter()
|
||||||
|
.map(debug_string_without_formatting)
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
),
|
||||||
|
Value::Record { cols, vals, .. } => format!(
|
||||||
|
"{{{}}}",
|
||||||
|
cols.iter()
|
||||||
|
.zip(vals.iter())
|
||||||
|
.map(|(x, y)| format!("{}: {}", x, debug_string_without_formatting(y)))
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
.join(" ")
|
||||||
|
),
|
||||||
|
Value::LazyRecord { val, .. } => match val.collect() {
|
||||||
|
Ok(val) => debug_string_without_formatting(&val),
|
||||||
|
Err(error) => format!("{error:?}"),
|
||||||
|
},
|
||||||
|
//TODO: It would be good to drill in deeper to blocks and closures.
|
||||||
|
Value::Block { val, .. } => format!("<Block {val}>"),
|
||||||
|
Value::Closure { val, .. } => format!("<Closure {val}>"),
|
||||||
|
Value::Nothing { .. } => String::new(),
|
||||||
|
Value::Error { error } => format!("{error:?}"),
|
||||||
|
Value::Binary { val, .. } => format!("{val:?}"),
|
||||||
|
Value::CellPath { val, .. } => val.into_string(),
|
||||||
|
Value::CustomValue { val, .. } => val.value_string(),
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ mod benchmark;
|
|||||||
mod complete;
|
mod complete;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
mod exec;
|
mod exec;
|
||||||
|
mod explain;
|
||||||
mod nu_check;
|
mod nu_check;
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
@ -20,6 +21,7 @@ pub use benchmark::Benchmark;
|
|||||||
pub use complete::Complete;
|
pub use complete::Complete;
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
pub use exec::Exec;
|
pub use exec::Exec;
|
||||||
|
pub use explain::Explain;
|
||||||
pub use nu_check::NuCheck;
|
pub use nu_check::NuCheck;
|
||||||
#[cfg(any(
|
#[cfg(any(
|
||||||
target_os = "android",
|
target_os = "android",
|
||||||
|
@ -23,6 +23,19 @@ pub enum PipelineElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl PipelineElement {
|
impl PipelineElement {
|
||||||
|
pub fn expression(&self) -> &Expression {
|
||||||
|
match self {
|
||||||
|
PipelineElement::Expression(_, expression) => expression,
|
||||||
|
PipelineElement::Redirection(_, _, expression) => expression,
|
||||||
|
PipelineElement::SeparateRedirection {
|
||||||
|
out: (_, expression),
|
||||||
|
..
|
||||||
|
} => expression,
|
||||||
|
PipelineElement::And(_, expression) => expression,
|
||||||
|
PipelineElement::Or(_, expression) => expression,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn span(&self) -> Span {
|
pub fn span(&self) -> Span {
|
||||||
match self {
|
match self {
|
||||||
PipelineElement::Expression(None, expression) => expression.span,
|
PipelineElement::Expression(None, expression) => expression.span,
|
||||||
|
Loading…
Reference in New Issue
Block a user