nu_command/debug/
ast.rs

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