Skip to main content

nu_command/system/
nu_check.rs

1use nu_engine::{command_prelude::*, find_in_dirs_env, get_dirs_var_from_call};
2use nu_parser::{parse, parse_module_block, parse_module_file_or_dir, unescape_unquote_string};
3use nu_protocol::{
4    engine::{FileStack, StateWorkingSet},
5    report_parse_error,
6    shell_error::generic::GenericError,
7    shell_error::io::IoError,
8};
9use std::path::{Path, PathBuf};
10
11#[derive(Clone)]
12pub struct NuCheck;
13
14impl Command for NuCheck {
15    fn name(&self) -> &str {
16        "nu-check"
17    }
18
19    fn signature(&self) -> Signature {
20        Signature::build("nu-check")
21            .input_output_types(vec![
22                (Type::Nothing, Type::Bool),
23                (Type::String, Type::Bool),
24                (Type::List(Box::new(Type::Any)), Type::Bool),
25                // FIXME Type::Any input added to disable pipeline input type checking, as run-time checks can raise undesirable type errors
26                // which aren't caught by the parser. see https://github.com/nushell/nushell/pull/14922 for more details
27                (Type::Any, Type::Bool),
28            ])
29            // type is string to avoid automatically canonicalizing the path
30            .optional("path", SyntaxShape::String, "File path to parse.")
31            .switch("as-module", "Parse content as module.", Some('m'))
32            .switch("debug", "Show error messages.", Some('d'))
33            .category(Category::Strings)
34    }
35
36    fn description(&self) -> &str {
37        "Validate and parse Nushell input content."
38    }
39
40    fn search_terms(&self) -> Vec<&str> {
41        vec!["syntax", "parse", "debug"]
42    }
43
44    fn run(
45        &self,
46        engine_state: &EngineState,
47        stack: &mut Stack,
48        call: &Call,
49        input: PipelineData,
50    ) -> Result<PipelineData, ShellError> {
51        let path_arg: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
52        let as_module = call.has_flag(engine_state, stack, "as-module")?;
53        let is_debug = call.has_flag(engine_state, stack, "debug")?;
54
55        // DO NOT ever try to merge the working_set in this command
56        let mut working_set = StateWorkingSet::new(engine_state);
57
58        let input_span = input.span().unwrap_or(call.head);
59
60        match input {
61            PipelineData::Value(Value::String { val, .. }, ..) => {
62                let contents = Vec::from(val);
63                if as_module {
64                    parse_module(&mut working_set, None, &contents, is_debug, input_span)
65                } else {
66                    parse_script(&mut working_set, None, &contents, is_debug, input_span)
67                }
68            }
69            PipelineData::ListStream(stream, ..) => {
70                let config = stack.get_config(engine_state);
71                let list_stream = stream.into_string("\n", &config);
72                let contents = Vec::from(list_stream);
73
74                if as_module {
75                    parse_module(&mut working_set, None, &contents, is_debug, call.head)
76                } else {
77                    parse_script(&mut working_set, None, &contents, is_debug, call.head)
78                }
79            }
80            PipelineData::ByteStream(stream, ..) => {
81                let contents = stream.into_bytes()?;
82
83                if as_module {
84                    parse_module(&mut working_set, None, &contents, is_debug, call.head)
85                } else {
86                    parse_script(&mut working_set, None, &contents, is_debug, call.head)
87                }
88            }
89            _ => {
90                if let Some(path_str) = path_arg {
91                    let path_span = path_str.span;
92
93                    // look up the path as relative to FILE_PWD or inside NU_LIB_DIRS (same process as source-env)
94                    let path = match find_in_dirs_env(
95                        &path_str.item,
96                        engine_state,
97                        stack,
98                        get_dirs_var_from_call(stack, call),
99                    ) {
100                        Ok(Some(path)) => path,
101                        Ok(None) => {
102                            return Err(ShellError::Io(IoError::new(
103                                ErrorKind::FileNotFound,
104                                path_span,
105                                PathBuf::from(path_str.item),
106                            )));
107                        }
108                        Err(err) => return Err(err),
109                    };
110
111                    if as_module || path.is_dir() {
112                        parse_file_or_dir_module(
113                            path.to_string_lossy().as_bytes(),
114                            &mut working_set,
115                            is_debug,
116                            path_span,
117                            call.head,
118                        )
119                    } else {
120                        // Unlike `parse_file_or_dir_module`, `parse_file_script` parses the content directly,
121                        // without adding the file to the stack. Therefore we need to handle this manually.
122                        working_set.files = FileStack::with_file(path.clone());
123                        parse_file_script(&path, &mut working_set, is_debug, path_span, call.head)
124                        // The working set is not merged, so no need to pop the file from the stack.
125                    }
126                } else {
127                    Err(ShellError::Generic(
128                        GenericError::new(
129                            "Failed to execute command",
130                            "Requires path argument if ran without pipeline input",
131                            call.head,
132                        )
133                        .with_help("Please run 'nu-check --help' for more details"),
134                    ))
135                }
136            }
137        }
138    }
139
140    fn examples(&self) -> Vec<Example<'_>> {
141        vec![
142            Example {
143                description: "Parse a input file as script(Default)",
144                example: "nu-check script.nu",
145                result: None,
146            },
147            Example {
148                description: "Parse a input file as module",
149                example: "nu-check --as-module module.nu",
150                result: None,
151            },
152            Example {
153                description: "Parse a input file by showing error message",
154                example: "nu-check --debug script.nu",
155                result: None,
156            },
157            Example {
158                description: "Parse a byte stream as script by showing error message",
159                example: "open foo.nu | nu-check --debug script.nu",
160                result: None,
161            },
162            Example {
163                description: "Parse an internal stream as module by showing error message",
164                example: "open module.nu | lines | nu-check --debug --as-module module.nu",
165                result: None,
166            },
167            Example {
168                description: "Parse a string as script",
169                example: "$'two(char nl)lines' | nu-check ",
170                result: None,
171            },
172        ]
173    }
174}
175
176fn parse_module(
177    working_set: &mut StateWorkingSet,
178    filename: Option<String>,
179    contents: &[u8],
180    is_debug: bool,
181    call_head: Span,
182) -> Result<PipelineData, ShellError> {
183    let filename = filename.unwrap_or_else(|| "empty".to_string());
184
185    let file_id = working_set.add_file(&filename, contents);
186    let new_span = working_set.get_span_for_file(file_id);
187
188    let starting_error_count = working_set.parse_errors.len();
189    parse_module_block(working_set, new_span, filename.as_bytes());
190
191    check_parse(
192        starting_error_count,
193        working_set,
194        is_debug,
195        Some(
196            "If the content is intended to be a script, please try to remove `--as-module` flag "
197                .to_string(),
198        ),
199        call_head,
200    )
201}
202
203fn parse_script(
204    working_set: &mut StateWorkingSet,
205    filename: Option<&str>,
206    contents: &[u8],
207    is_debug: bool,
208    call_head: Span,
209) -> Result<PipelineData, ShellError> {
210    let starting_error_count = working_set.parse_errors.len();
211    parse(working_set, filename, contents, false);
212    check_parse(starting_error_count, working_set, is_debug, None, call_head)
213}
214
215fn check_parse(
216    starting_error_count: usize,
217    working_set: &StateWorkingSet,
218    is_debug: bool,
219    help: Option<String>,
220    call_head: Span,
221) -> Result<PipelineData, ShellError> {
222    if starting_error_count != working_set.parse_errors.len() {
223        let parse_err = working_set
224            .parse_errors
225            .first()
226            .expect("Missing parser error");
227
228        if is_debug {
229            // Print the real miette diagnostic (with file contents / labels) first.
230            report_parse_error(None, working_set, parse_err);
231
232            let msg = format!("Found : {parse_err}");
233            let mut err = GenericError::new("Failed to parse content", msg, call_head);
234            if let Some(help) = help {
235                err = err.with_help(help);
236            }
237            Err(ShellError::Generic(err))
238        } else {
239            Ok(PipelineData::value(Value::bool(false, call_head), None))
240        }
241    } else {
242        Ok(PipelineData::value(Value::bool(true, call_head), None))
243    }
244}
245
246fn parse_file_script(
247    path: &Path,
248    working_set: &mut StateWorkingSet,
249    is_debug: bool,
250    path_span: Span,
251    call_head: Span,
252) -> Result<PipelineData, ShellError> {
253    let filename = check_path(working_set, path_span, call_head)?;
254
255    match std::fs::read(path) {
256        Ok(contents) => parse_script(working_set, Some(&filename), &contents, is_debug, call_head),
257        Err(err) => Err(ShellError::Io(IoError::new(
258            err.not_found_as(NotFound::File),
259            path_span,
260            PathBuf::from(path),
261        ))),
262    }
263}
264
265fn parse_file_or_dir_module(
266    path_bytes: &[u8],
267    working_set: &mut StateWorkingSet,
268    is_debug: bool,
269    path_span: Span,
270    call_head: Span,
271) -> Result<PipelineData, ShellError> {
272    let _ = check_path(working_set, path_span, call_head)?;
273
274    let starting_error_count = working_set.parse_errors.len();
275    let _ = parse_module_file_or_dir(working_set, path_bytes, path_span, None);
276
277    if starting_error_count != working_set.parse_errors.len() {
278        if is_debug {
279            let parse_err = working_set
280                .parse_errors
281                .first()
282                .expect("Missing parser error");
283            report_parse_error(None, working_set, parse_err);
284            let msg = format!("Found : {parse_err}");
285            Err(ShellError::Generic(
286                GenericError::new("Failed to parse content", msg, path_span).with_help(
287                    "If the content is intended to be a script, please try to remove `--as-module` flag ",
288                ),
289            ))
290        } else {
291            Ok(PipelineData::value(Value::bool(false, call_head), None))
292        }
293    } else {
294        Ok(PipelineData::value(Value::bool(true, call_head), None))
295    }
296}
297
298fn check_path(
299    working_set: &mut StateWorkingSet,
300    path_span: Span,
301    call_head: Span,
302) -> Result<String, ShellError> {
303    let bytes = working_set.get_span_contents(path_span);
304    let (filename, err) = unescape_unquote_string(bytes, path_span);
305    if let Some(e) = err {
306        Err(ShellError::Generic(
307            GenericError::new(
308                "Could not escape filename",
309                "could not escape filename",
310                call_head,
311            )
312            .with_help(format!("Returned error: {e}")),
313        ))
314    } else {
315        Ok(filename)
316    }
317}