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