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