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                    let result = 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
125                    result
126                } else {
127                    Err(ShellError::GenericError {
128                        error: "Failed to execute command".into(),
129                        msg: "Requires path argument if ran without pipeline input".into(),
130                        span: Some(call.head),
131                        help: Some("Please run 'nu-check --help' for more details".into()),
132                        inner: vec![],
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            r#"Found : {}"#,
224            working_set
225                .parse_errors
226                .first()
227                .expect("Missing parser error")
228        );
229
230        if is_debug {
231            Err(ShellError::GenericError {
232                error: "Failed to parse content".into(),
233                msg,
234                span: Some(call_head),
235                help,
236                inner: vec![],
237            })
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 msg = format!(
280                r#"Found : {}"#,
281                working_set
282                    .parse_errors
283                    .first()
284                    .expect("Missing parser error")
285            );
286            Err(ShellError::GenericError {
287                error: "Failed to parse content".into(),
288                msg,
289                span: Some(path_span),
290                help: Some("If the content is intended to be a script, please try to remove `--as-module` flag ".into()),
291                inner: vec![],
292            })
293        } else {
294            Ok(PipelineData::Value(Value::bool(false, call_head), None))
295        }
296    } else {
297        Ok(PipelineData::Value(Value::bool(true, call_head), None))
298    }
299}
300
301fn check_path(
302    working_set: &mut StateWorkingSet,
303    path_span: Span,
304    call_head: Span,
305) -> Result<String, ShellError> {
306    let bytes = working_set.get_span_contents(path_span);
307    let (filename, err) = unescape_unquote_string(bytes, path_span);
308    if let Some(e) = err {
309        Err(ShellError::GenericError {
310            error: "Could not escape filename".to_string(),
311            msg: "could not escape filename".to_string(),
312            span: Some(call_head),
313            help: Some(format!("Returned error: {e}")),
314            inner: vec![],
315        })
316    } else {
317        Ok(filename)
318    }
319}