1use nu_engine::command_prelude::*;
2use nu_parser::{flatten_block, parse};
3use nu_protocol::{engine::StateWorkingSet, record};
4use serde_json::{Value as JsonValue, json};
5
6const FIELD_START: &str = "start";
8const FIELD_END: &str = "end";
9const FIELD_SPAN_SOURCE: &str = "span_source";
10
11#[derive(Clone)]
12pub struct Ast;
13
14impl Command for Ast {
15 fn name(&self) -> &str {
16 "ast"
17 }
18
19 fn description(&self) -> &str {
20 "Print the abstract syntax tree (ast) for a pipeline."
21 }
22
23 fn signature(&self) -> Signature {
24 Signature::build("ast")
25 .input_output_types(vec![
26 (Type::Nothing, Type::table()),
27 (Type::Nothing, Type::record()),
28 (Type::Nothing, Type::String),
29 ])
30 .required(
31 "pipeline",
32 SyntaxShape::String,
33 "The pipeline to print the ast for.",
34 )
35 .switch("json", "Serialize to json.", Some('j'))
36 .switch("minify", "Minify the nuon or json output.", Some('m'))
37 .switch(
38 "flatten",
39 "An easier to read version of the ast.",
40 Some('f'),
41 )
42 .allow_variants_without_examples(true)
43 .category(Category::Debug)
44 }
45
46 fn examples(&self) -> Vec<Example<'_>> {
47 vec![
48 Example {
49 description: "Print the ast of a string.",
50 example: "ast 'hello'",
51 result: None,
52 },
53 Example {
54 description: "Print the ast of a pipeline.",
55 example: "ast 'ls | where name =~ README'",
56 result: None,
57 },
58 Example {
59 description: "Print the ast of a pipeline with an error.",
60 example: "ast 'for x in 1..10 { echo $x '",
61 result: None,
62 },
63 Example {
64 description: "Print the ast of a pipeline with an error, as json, in a nushell table.",
65 example: "ast 'for x in 1..10 { echo $x ' --json | get block | from json",
66 result: None,
67 },
68 Example {
69 description: "Print the ast of a pipeline with an error, as json, minified.",
70 example: "ast 'for x in 1..10 { echo $x ' --json --minify",
71 result: None,
72 },
73 Example {
74 description: "Print the ast of a string flattened.",
75 example: r#"ast "'hello'" --flatten"#,
76 result: Some(Value::test_list(vec![Value::test_record(record! {
77 "content" => Value::test_string("'hello'"),
78 "shape" => Value::test_string("shape_string"),
79 "span" => Value::test_record(record! {
80 "start" => Value::test_int(0),
81 "end" => Value::test_int(7),}),
82 })])),
83 },
84 Example {
85 description: "Print the ast of a string flattened, as json, minified.",
86 example: r#"ast "'hello'" --flatten --json --minify"#,
87 result: Some(Value::test_string(
88 r#"[{"content":"'hello'","shape":"shape_string","span":{"start":0,"end":7}}]"#,
89 )),
90 },
91 Example {
92 description: "Print the ast of a pipeline flattened.",
93 example: r#"ast 'ls | sort-by type name -i' --flatten"#,
94 result: Some(Value::test_list(vec![
95 Value::test_record(record! {
96 "content" => Value::test_string("ls"),
97 "shape" => Value::test_string("shape_external"),
98 "span" => Value::test_record(record! {
99 "start" => Value::test_int(0),
100 "end" => Value::test_int(2),}),
101 }),
102 Value::test_record(record! {
103 "content" => Value::test_string("|"),
104 "shape" => Value::test_string("shape_pipe"),
105 "span" => Value::test_record(record! {
106 "start" => Value::test_int(3),
107 "end" => Value::test_int(4),}),
108 }),
109 Value::test_record(record! {
110 "content" => Value::test_string("sort-by"),
111 "shape" => Value::test_string("shape_internalcall"),
112 "span" => Value::test_record(record! {
113 "start" => Value::test_int(5),
114 "end" => Value::test_int(12),}),
115 }),
116 Value::test_record(record! {
117 "content" => Value::test_string("type"),
118 "shape" => Value::test_string("shape_string"),
119 "span" => Value::test_record(record! {
120 "start" => Value::test_int(13),
121 "end" => Value::test_int(17),}),
122 }),
123 Value::test_record(record! {
124 "content" => Value::test_string("name"),
125 "shape" => Value::test_string("shape_string"),
126 "span" => Value::test_record(record! {
127 "start" => Value::test_int(18),
128 "end" => Value::test_int(22),}),
129 }),
130 Value::test_record(record! {
131 "content" => Value::test_string("-i"),
132 "shape" => Value::test_string("shape_flag"),
133 "span" => Value::test_record(record! {
134 "start" => Value::test_int(23),
135 "end" => Value::test_int(25),}),
136 }),
137 ])),
138 },
139 ]
140 }
141
142 fn run(
143 &self,
144 engine_state: &EngineState,
145 stack: &mut Stack,
146 call: &Call,
147 _input: PipelineData,
148 ) -> Result<PipelineData, ShellError> {
149 let pipeline: Spanned<String> = call.req(engine_state, stack, 0)?;
151 let to_json = call.has_flag(engine_state, stack, "json")?;
152 let minify = call.has_flag(engine_state, stack, "minify")?;
153 let flatten = call.has_flag(engine_state, stack, "flatten")?;
154
155 let mut working_set = StateWorkingSet::new(engine_state);
157 let offset = working_set.next_span_start();
158 let parsed_block = parse(&mut working_set, None, pipeline.item.as_bytes(), false);
159
160 if flatten {
162 let flat = flatten_block(&working_set, &parsed_block);
163 if to_json {
164 let mut json_val: JsonValue = json!([]);
165 for (span, shape) in flat {
166 let content =
167 String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
168
169 let json = json!(
170 {
171 "content": content,
172 "shape": shape.to_string(),
173 "span": {
174 "start": span.start.checked_sub(offset),
175 "end": span.end.checked_sub(offset),
176 },
177 }
178 );
179 json_merge(&mut json_val, &json);
180 }
181 let json_string = if minify {
182 if let Ok(json_str) = serde_json::to_string(&json_val) {
183 json_str
184 } else {
185 "{}".to_string()
186 }
187 } else if let Ok(json_str) = serde_json::to_string_pretty(&json_val) {
188 json_str
189 } else {
190 "{}".to_string()
191 };
192
193 Ok(Value::string(json_string, pipeline.span).into_pipeline_data())
194 } else {
195 let mut rec = vec![];
197 for (span, shape) in flat {
198 let content =
199 String::from_utf8_lossy(working_set.get_span_contents(span)).to_string();
200 let each_rec = record! {
201 "content" => Value::test_string(content),
202 "shape" => Value::test_string(shape.to_string()),
203 "span" => Value::test_record(record!{
204 "start" => Value::test_int(match span.start.checked_sub(offset) {
205 Some(start) => start as i64,
206 None => 0
207 }),
208 "end" => Value::test_int(match span.end.checked_sub(offset) {
209 Some(end) => end as i64,
210 None => 0
211 }),
212 }),
213 };
214 rec.push(Value::test_record(each_rec));
215 }
216 Ok(Value::list(rec, pipeline.span).into_pipeline_data())
217 }
218 } else {
219 let error_output = working_set.parse_errors.first();
220 let block_span = match &parsed_block.span {
221 Some(span) => span,
222 None => &pipeline.span,
223 };
224 if to_json {
225 let serde_block_str =
227 serde_json::to_string(&*parsed_block).map_err(|e| ShellError::CantConvert {
228 to_type: "string".to_string(),
229 from_type: "block".to_string(),
230 span: *block_span,
231 help: Some(format!(
232 "Error: {e}\nCan't convert {parsed_block:?} to string"
233 )),
234 })?;
235 let json_val: serde_json::Value =
236 serde_json::from_str(&serde_block_str).map_err(|e| {
237 ShellError::CantConvert {
238 to_type: "string".to_string(),
239 from_type: "block".to_string(),
240 span: *block_span,
241 help: Some(format!(
242 "Error: {e}\nCan't convert block JSON to serde_json: {e}"
243 )),
244 }
245 })?;
246 let mut json_val = json_val;
247
248 embed_span_sources(&mut json_val, &working_set);
250
251 let block_json = if minify {
252 json_val.to_string()
253 } else {
254 serde_json::to_string_pretty(&json_val).unwrap_or_else(|_| json_val.to_string())
255 };
256 let serde_error_str = if minify {
258 serde_json::to_string(&error_output)
259 } else {
260 serde_json::to_string_pretty(&error_output)
261 };
262
263 let error_json = match serde_error_str {
264 Ok(json) => json,
265 Err(e) => Err(ShellError::CantConvert {
266 to_type: "string".to_string(),
267 from_type: "error".to_string(),
268 span: *block_span,
269 help: Some(format!(
270 "Error: {e}\nCan't convert {error_output:?} to string"
271 )),
272 })?,
273 };
274
275 let output_record = Value::record(
277 record! {
278 "block" => Value::string(block_json, *block_span),
279 "error" => Value::string(error_json, Span::test_data()),
280 },
281 pipeline.span,
282 );
283 Ok(output_record.into_pipeline_data())
284 } else {
285 let block_value = Value::string(
286 if minify {
287 format!("{parsed_block:?}")
288 } else {
289 format!("{parsed_block:#?}")
290 },
291 pipeline.span,
292 );
293 let error_value = Value::string(
294 if minify {
295 format!("{error_output:?}")
296 } else {
297 format!("{error_output:#?}")
298 },
299 pipeline.span,
300 );
301 let output_record = Value::record(
302 record! {
303 "block" => block_value,
304 "error" => error_value,
305 },
306 pipeline.span,
307 );
308 Ok(output_record.into_pipeline_data())
309 }
310 }
311 }
312}
313
314fn json_merge(a: &mut JsonValue, b: &JsonValue) {
315 match (a, b) {
316 (JsonValue::Object(a), JsonValue::Object(b)) => {
317 for (k, v) in b {
318 json_merge(a.entry(k).or_insert(JsonValue::Null), v);
319 }
320 }
321 (JsonValue::Array(a), JsonValue::Array(b)) => {
322 a.extend(b.clone());
323 }
324 (JsonValue::Array(a), JsonValue::Object(b)) => {
325 a.extend([JsonValue::Object(b.clone())]);
326 }
327 (a, b) => {
328 *a = b.clone();
329 }
330 }
331}
332
333fn embed_span_sources(value: &mut serde_json::Value, working_set: &StateWorkingSet) {
343 match value {
344 serde_json::Value::Object(obj) => {
345 if let Some(span) = extract_span_from_json(obj) {
347 let contents = working_set.get_span_contents(span);
349 let source = String::from_utf8_lossy(contents).to_string();
350
351 obj.insert(
353 FIELD_SPAN_SOURCE.to_string(),
354 serde_json::Value::String(source),
355 );
356 } else {
357 for (_, v) in obj.iter_mut() {
359 embed_span_sources(v, working_set);
360 }
361 }
362 }
363 serde_json::Value::Array(arr) => {
364 for v in arr {
366 embed_span_sources(v, working_set);
367 }
368 }
369 _ => {
370 }
372 }
373}
374
375fn extract_span_from_json(obj: &serde_json::Map<String, serde_json::Value>) -> Option<Span> {
380 let start_value = obj.get(FIELD_START)?;
381 let end_value = obj.get(FIELD_END)?;
382
383 let start_num = match start_value {
385 serde_json::Value::Number(n) => n.as_i64()?,
386 _ => return None,
387 };
388 let end_num = match end_value {
389 serde_json::Value::Number(n) => n.as_i64()?,
390 _ => return None,
391 };
392
393 if start_num < 0 || end_num < 0 || start_num >= end_num {
395 return None;
396 }
397
398 Some(Span::new(start_num as usize, end_num as usize))
399}
400
401#[cfg(test)]
402mod test {
403 #[test]
404 fn test_examples() {
405 use super::Ast;
406 use crate::test_examples;
407 test_examples(Ast {})
408 }
409}