diff --git a/crates/nu-command/src/debug/ast.rs b/crates/nu-command/src/debug/ast.rs index c6e405d533..45e7738c32 100644 --- a/crates/nu-command/src/debug/ast.rs +++ b/crates/nu-command/src/debug/ast.rs @@ -1,6 +1,7 @@ use nu_engine::command_prelude::*; -use nu_parser::parse; -use nu_protocol::engine::StateWorkingSet; +use nu_parser::{flatten_block, parse}; +use nu_protocol::{engine::StateWorkingSet, record}; +use serde_json::{json, Value as JsonValue}; #[derive(Clone)] pub struct Ast; @@ -16,109 +17,23 @@ impl Command for Ast { fn signature(&self) -> Signature { Signature::build("ast") - .input_output_types(vec![(Type::String, Type::record())]) + .input_output_types(vec![ + (Type::Nothing, Type::table()), + (Type::Nothing, Type::record()), + (Type::Nothing, Type::String), + ]) .required( "pipeline", SyntaxShape::String, "The pipeline to print the ast for.", ) - .switch("json", "serialize to json", Some('j')) - .switch("minify", "minify the nuon or json output", Some('m')) + .switch("json", "Serialize to json", Some('j')) + .switch("minify", "Minify the nuon or json output", Some('m')) + .switch("flatten", "An easier to read version of the ast", Some('f')) .allow_variants_without_examples(true) .category(Category::Debug) } - fn run( - &self, - engine_state: &EngineState, - stack: &mut Stack, - call: &Call, - _input: PipelineData, - ) -> Result { - let pipeline: Spanned = call.req(engine_state, stack, 0)?; - let to_json = call.has_flag(engine_state, stack, "json")?; - let minify = call.has_flag(engine_state, stack, "minify")?; - let mut working_set = StateWorkingSet::new(engine_state); - let block_output = parse(&mut working_set, None, pipeline.item.as_bytes(), false); - let error_output = working_set.parse_errors.first(); - let block_span = match &block_output.span { - Some(span) => span, - None => &pipeline.span, - }; - if to_json { - // Get the block as json - let serde_block_str = if minify { - serde_json::to_string(&*block_output) - } else { - serde_json::to_string_pretty(&*block_output) - }; - let block_json = match serde_block_str { - Ok(json) => json, - Err(e) => Err(ShellError::CantConvert { - to_type: "string".to_string(), - from_type: "block".to_string(), - span: *block_span, - help: Some(format!( - "Error: {e}\nCan't convert {block_output:?} to string" - )), - })?, - }; - // Get the error as json - let serde_error_str = if minify { - serde_json::to_string(&error_output) - } else { - serde_json::to_string_pretty(&error_output) - }; - - let error_json = match serde_error_str { - Ok(json) => json, - Err(e) => Err(ShellError::CantConvert { - to_type: "string".to_string(), - from_type: "error".to_string(), - span: *block_span, - help: Some(format!( - "Error: {e}\nCan't convert {error_output:?} to string" - )), - })?, - }; - - // Create a new output record, merging the block and error - let output_record = Value::record( - record! { - "block" => Value::string(block_json, *block_span), - "error" => Value::string(error_json, Span::test_data()), - }, - pipeline.span, - ); - Ok(output_record.into_pipeline_data()) - } else { - let block_value = Value::string( - if minify { - format!("{block_output:?}") - } else { - format!("{block_output:#?}") - }, - pipeline.span, - ); - let error_value = Value::string( - if minify { - format!("{error_output:?}") - } else { - format!("{error_output:#?}") - }, - pipeline.span, - ); - let output_record = Value::record( - record! { - "block" => block_value, - "error" => error_value - }, - pipeline.span, - ); - Ok(output_record.into_pipeline_data()) - } - } - fn examples(&self) -> Vec { vec![ Example { @@ -147,8 +62,247 @@ impl Command for Ast { example: "ast 'for x in 1..10 { echo $x ' --json --minify", result: None, }, + Example { + description: "Print the ast of a string flattened", + example: r#"ast "'hello'" --flatten"#, + result: Some(Value::test_list(vec![Value::test_record(record! { + "content" => Value::test_string("'hello'"), + "shape" => Value::test_string("shape_string"), + "span" => Value::test_record(record! { + "start" => Value::test_int(0), + "end" => Value::test_int(7),}), + })])), + }, + Example { + description: "Print the ast of a string flattened, as json, minified", + example: r#"ast "'hello'" --flatten --json --minify"#, + result: Some(Value::test_string( + r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#, + )), + }, + Example { + description: "Print the ast of a pipeline flattened", + example: r#"ast 'ls | sort-by type name -i' --flatten"#, + result: Some(Value::test_list(vec![ + Value::test_record(record! { + "content" => Value::test_string("ls"), + "shape" => Value::test_string("shape_external"), + "span" => Value::test_record(record! { + "start" => Value::test_int(0), + "end" => Value::test_int(2),}), + }), + Value::test_record(record! { + "content" => Value::test_string("|"), + "shape" => Value::test_string("shape_pipe"), + "span" => Value::test_record(record! { + "start" => Value::test_int(3), + "end" => Value::test_int(4),}), + }), + Value::test_record(record! { + "content" => Value::test_string("sort-by"), + "shape" => Value::test_string("shape_internalcall"), + "span" => Value::test_record(record! { + "start" => Value::test_int(5), + "end" => Value::test_int(12),}), + }), + Value::test_record(record! { + "content" => Value::test_string("type"), + "shape" => Value::test_string("shape_string"), + "span" => Value::test_record(record! { + "start" => Value::test_int(13), + "end" => Value::test_int(17),}), + }), + Value::test_record(record! { + "content" => Value::test_string("name"), + "shape" => Value::test_string("shape_string"), + "span" => Value::test_record(record! { + "start" => Value::test_int(18), + "end" => Value::test_int(22),}), + }), + Value::test_record(record! { + "content" => Value::test_string("-i"), + "shape" => Value::test_string("shape_flag"), + "span" => Value::test_record(record! { + "start" => Value::test_int(23), + "end" => Value::test_int(25),}), + }), + ])), + }, ] } + + fn run( + &self, + engine_state: &EngineState, + stack: &mut Stack, + call: &Call, + _input: PipelineData, + ) -> Result { + let pipeline: Spanned = call.req(engine_state, stack, 0)?; + let to_json = call.has_flag(engine_state, stack, "json")?; + let minify = call.has_flag(engine_state, stack, "minify")?; + let flatten = call.has_flag(engine_state, stack, "flatten")?; + + let mut working_set = StateWorkingSet::new(engine_state); + let offset = working_set.next_span_start(); + let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false); + + if flatten { + let flat = flatten_block(&working_set, &parsed_block); + if to_json { + let mut json_val: JsonValue = json!([]); + for (span, shape) in flat { + let content = + String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(); + + let json = json!( + { + "content": content, + "shape": shape.to_string(), + "span": { + "start": span.start.checked_sub(offset), + "end": span.end.checked_sub(offset), + }, + } + ); + json_merge(&mut json_val, &json); + } + let json_string = if minify { + if let Ok(json_str) = serde_json::to_string(&json_val) { + json_str + } else { + "{}".to_string() + } + } else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) { + json_str + } else { + "{}".to_string() + }; + + Ok(Value::string(json_string, pipeline.span).into_pipeline_data()) + } else { + // let mut rec: Record = Record::new(); + let mut rec = vec![]; + for (span, shape) in flat { + let content = + String::from_utf8_lossy(working_set.get_span_contents(span)).to_string(); + let each_rec = record! { + "content" => Value::test_string(content), + "shape" => Value::test_string(shape.to_string()), + "span" => Value::test_record(record!{ + "start" => Value::test_int(match span.start.checked_sub(offset) { + Some(start) => start as i64, + None => 0 + }), + "end" => Value::test_int(match span.end.checked_sub(offset) { + Some(end) => end as i64, + None => 0 + }), + }), + }; + rec.push(Value::test_record(each_rec)); + } + Ok(Value::list(rec, pipeline.span).into_pipeline_data()) + } + } else { + let error_output = working_set.parse_errors.first(); + let block_span = match &parsed_block.span { + Some(span) => span, + None => &pipeline.span, + }; + if to_json { + // Get the block as json + let serde_block_str = if minify { + serde_json::to_string(&*parsed_block) + } else { + serde_json::to_string_pretty(&*parsed_block) + }; + let block_json = match serde_block_str { + Ok(json) => json, + Err(e) => Err(ShellError::CantConvert { + to_type: "string".to_string(), + from_type: "block".to_string(), + span: *block_span, + help: Some(format!( + "Error: {e}\nCan't convert {parsed_block:?} to string" + )), + })?, + }; + // Get the error as json + let serde_error_str = if minify { + serde_json::to_string(&error_output) + } else { + serde_json::to_string_pretty(&error_output) + }; + + let error_json = match serde_error_str { + Ok(json) => json, + Err(e) => Err(ShellError::CantConvert { + to_type: "string".to_string(), + from_type: "error".to_string(), + span: *block_span, + help: Some(format!( + "Error: {e}\nCan't convert {error_output:?} to string" + )), + })?, + }; + + // Create a new output record, merging the block and error + let output_record = Value::record( + record! { + "block" => Value::string(block_json, *block_span), + "error" => Value::string(error_json, Span::test_data()), + }, + pipeline.span, + ); + Ok(output_record.into_pipeline_data()) + } else { + let block_value = Value::string( + if minify { + format!("{parsed_block:?}") + } else { + format!("{parsed_block:#?}") + }, + pipeline.span, + ); + let error_value = Value::string( + if minify { + format!("{error_output:?}") + } else { + format!("{error_output:#?}") + }, + pipeline.span, + ); + let output_record = Value::record( + record! { + "block" => block_value, + "error" => error_value + }, + pipeline.span, + ); + Ok(output_record.into_pipeline_data()) + } + } + } +} + +fn json_merge(a: &mut JsonValue, b: &JsonValue) { + match (a, b) { + (JsonValue::Object(ref mut a), JsonValue::Object(b)) => { + for (k, v) in b { + json_merge(a.entry(k).or_insert(JsonValue::Null), v); + } + } + (JsonValue::Array(ref mut a), JsonValue::Array(b)) => { + a.extend(b.clone()); + } + (JsonValue::Array(ref mut a), JsonValue::Object(b)) => { + a.extend([JsonValue::Object(b.clone())]); + } + (a, b) => { + *a = b.clone(); + } + } } #[cfg(test)]