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