From c31225fdcf8300380212e2e6dc681d4bc0f0866b Mon Sep 17 00:00:00 2001 From: Darren Schroeder <343840+fdncred@users.noreply.github.com> Date: Thu, 9 Feb 2023 13:59:38 -0600 Subject: [PATCH] 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. :) ![image](https://user-images.githubusercontent.com/343840/217896776-99c6bece-172c-4d3d-8cec-dda85d37cada.png) --- crates/nu-command/src/default_context.rs | 1 + crates/nu-command/src/system/explain.rs | 330 +++++++++++++++++++++++ crates/nu-command/src/system/mod.rs | 2 + crates/nu-protocol/src/ast/pipeline.rs | 13 + 4 files changed, 346 insertions(+) create mode 100644 crates/nu-command/src/system/explain.rs diff --git a/crates/nu-command/src/default_context.rs b/crates/nu-command/src/default_context.rs index a27a462bb..ced738c17 100644 --- a/crates/nu-command/src/default_context.rs +++ b/crates/nu-command/src/default_context.rs @@ -174,6 +174,7 @@ pub fn create_default_context() -> EngineState { bind_command! { Benchmark, Complete, + Explain, External, NuCheck, Sys, diff --git a/crates/nu-command/src/system/explain.rs b/crates/nu-command/src/system/explain.rs new file mode 100644 index 000000000..36aa850f3 --- /dev/null +++ b/crates/nu-command/src/system/explain.rs @@ -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 { + // 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 { + 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, 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 { + 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::>() + .join(" ") + ), + Value::Record { cols, vals, .. } => format!( + "{{{}}}", + cols.iter() + .zip(vals.iter()) + .map(|(x, y)| format!("{}: {}", x, debug_string_without_formatting(y))) + .collect::>() + .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!(""), + Value::Closure { val, .. } => format!(""), + 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(), + } +} diff --git a/crates/nu-command/src/system/mod.rs b/crates/nu-command/src/system/mod.rs index 15bcf13fa..e1fe95ac6 100644 --- a/crates/nu-command/src/system/mod.rs +++ b/crates/nu-command/src/system/mod.rs @@ -2,6 +2,7 @@ mod benchmark; mod complete; #[cfg(unix)] mod exec; +mod explain; mod nu_check; #[cfg(any( target_os = "android", @@ -20,6 +21,7 @@ pub use benchmark::Benchmark; pub use complete::Complete; #[cfg(unix)] pub use exec::Exec; +pub use explain::Explain; pub use nu_check::NuCheck; #[cfg(any( target_os = "android", diff --git a/crates/nu-protocol/src/ast/pipeline.rs b/crates/nu-protocol/src/ast/pipeline.rs index ee6194615..be3cf7abf 100644 --- a/crates/nu-protocol/src/ast/pipeline.rs +++ b/crates/nu-protocol/src/ast/pipeline.rs @@ -23,6 +23,19 @@ pub enum 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 { match self { PipelineElement::Expression(None, expression) => expression.span,