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