use nu_engine::command_prelude::*; use nu_parser::{flatten_block, parse}; use nu_protocol::{engine::StateWorkingSet, record}; use serde_json::{json, Value as JsonValue}; #[derive(Clone)] pub struct Ast; impl Command for Ast { fn name(&self) -> &str { "ast" } fn description(&self) -> &str { "Print the abstract syntax tree (ast) for a pipeline." } fn signature(&self) -> Signature { Signature::build("ast") .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("flatten", "An easier to read version of the ast", Some('f')) .allow_variants_without_examples(true) .category(Category::Debug) } fn examples(&self) -> Vec { vec![ Example { description: "Print the ast of a string", example: "ast 'hello'", result: None, }, Example { description: "Print the ast of a pipeline", example: "ast 'ls | where name =~ README'", result: None, }, Example { description: "Print the ast of a pipeline with an error", example: "ast 'for x in 1..10 { echo $x '", result: None, }, Example { description: "Print the ast of a pipeline with an error, as json, in a nushell table", example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json", result: None, }, Example { description: "Print the ast of a pipeline with an error, as json, minified", 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)] mod test { #[test] fn test_examples() { use super::Ast; use crate::test_examples; test_examples(Ast {}) } }