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 (Type::Any, Type::Bool),
28 ])
29 .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 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 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 working_set.files = FileStack::with_file(path.clone());
123 parse_file_script(&path, &mut working_set, is_debug, path_span, call.head)
124 }
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 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}