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