use nu_engine::{command_prelude::*, env::get_config, find_in_dirs_env, get_dirs_var_from_call};
use nu_parser::{parse, parse_module_block, parse_module_file_or_dir, unescape_unquote_string};
use nu_protocol::engine::StateWorkingSet;
use std::path::Path;
#[derive(Clone)]
pub struct NuCheck;
impl Command for NuCheck {
fn name(&self) -> &str {
"nu-check"
}
fn signature(&self) -> Signature {
Signature::build("nu-check")
.input_output_types(vec![
(Type::String, Type::Bool),
(Type::ListStream, Type::Bool),
(Type::List(Box::new(Type::Any)), Type::Bool),
])
.optional("path", SyntaxShape::String, "File path to parse.")
.switch("as-module", "Parse content as module", Some('m'))
.switch("debug", "Show error messages", Some('d'))
.category(Category::Strings)
}
fn usage(&self) -> &str {
"Validate and parse input content."
}
fn search_terms(&self) -> Vec<&str> {
vec!["syntax", "parse", "debug"]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let path_arg: Option<Spanned<String>> = call.opt(engine_state, stack, 0)?;
let as_module = call.has_flag(engine_state, stack, "as-module")?;
let is_debug = call.has_flag(engine_state, stack, "debug")?;
let mut working_set = StateWorkingSet::new(engine_state);
let input_span = input.span().unwrap_or(call.head);
match input {
PipelineData::Value(Value::String { val, .. }, ..) => {
let contents = Vec::from(val);
if as_module {
parse_module(&mut working_set, None, &contents, is_debug, input_span)
} else {
parse_script(&mut working_set, None, &contents, is_debug, input_span)
}
}
PipelineData::ListStream(stream, ..) => {
let config = get_config(engine_state, stack);
let list_stream = stream.into_string("\n", &config);
let contents = Vec::from(list_stream);
if as_module {
parse_module(&mut working_set, None, &contents, is_debug, call.head)
} else {
parse_script(&mut working_set, None, &contents, is_debug, call.head)
}
}
PipelineData::ExternalStream {
stdout: Some(stream),
..
} => {
let mut contents = vec![];
let raw_stream: Vec<_> = stream.stream.collect();
for r in raw_stream {
match r {
Ok(v) => contents.extend(v),
Err(error) => return Err(error),
};
}
if as_module {
parse_module(&mut working_set, None, &contents, is_debug, call.head)
} else {
parse_script(&mut working_set, None, &contents, is_debug, call.head)
}
}
_ => {
if let Some(path_str) = path_arg {
let path_span = path_str.span;
let path = match find_in_dirs_env(
&path_str.item,
engine_state,
stack,
get_dirs_var_from_call(call),
) {
Ok(path) => {
if let Some(path) = path {
path
} else {
return Err(ShellError::FileNotFound {
file: path_str.item,
span: path_span,
});
}
}
Err(error) => return Err(error),
};
let prev_currently_parsed_cwd = if let Some(parent) = path.parent() {
let prev = working_set.currently_parsed_cwd.clone();
working_set.currently_parsed_cwd = Some(parent.into());
prev
} else {
working_set.currently_parsed_cwd.clone()
};
let result = if as_module || path.is_dir() {
parse_file_or_dir_module(
path.to_string_lossy().as_bytes(),
&mut working_set,
is_debug,
path_span,
call.head,
)
} else {
parse_file_script(&path, &mut working_set, is_debug, path_span, call.head)
};
working_set.currently_parsed_cwd = prev_currently_parsed_cwd;
result
} else {
Err(ShellError::GenericError {
error: "Failed to execute command".into(),
msg: "Requires path argument if ran without pipeline input".into(),
span: Some(call.head),
help: Some("Please run 'nu-check --help' for more details".into()),
inner: vec![],
})
}
}
}
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Parse a input file as script(Default)",
example: "nu-check script.nu",
result: None,
},
Example {
description: "Parse a input file as module",
example: "nu-check --as-module module.nu",
result: None,
},
Example {
description: "Parse a input file by showing error message",
example: "nu-check --debug script.nu",
result: None,
},
Example {
description: "Parse an external stream as script by showing error message",
example: "open foo.nu | nu-check --debug script.nu",
result: None,
},
Example {
description: "Parse an internal stream as module by showing error message",
example: "open module.nu | lines | nu-check --debug --as-module module.nu",
result: None,
},
Example {
description: "Parse a string as script",
example: "$'two(char nl)lines' | nu-check ",
result: None,
},
Example {
description: "Heuristically parse which begins with script first, if it sees a failure, try module afterwards",
example: "nu-check -a script.nu",
result: None,
},
Example {
description: "Heuristically parse by showing error message",
example: "open foo.nu | lines | nu-check --all --debug",
result: None,
},
]
}
}
fn parse_module(
working_set: &mut StateWorkingSet,
filename: Option<String>,
contents: &[u8],
is_debug: bool,
call_head: Span,
) -> Result<PipelineData, ShellError> {
let filename = filename.unwrap_or_else(|| "empty".to_string());
let file_id = working_set.add_file(filename.clone(), contents);
let new_span = working_set.get_span_for_file(file_id);
let starting_error_count = working_set.parse_errors.len();
parse_module_block(working_set, new_span, filename.as_bytes());
check_parse(
starting_error_count,
working_set,
is_debug,
Some(
"If the content is intended to be a script, please try to remove `--as-module` flag "
.to_string(),
),
call_head,
)
}
fn parse_script(
working_set: &mut StateWorkingSet,
filename: Option<&str>,
contents: &[u8],
is_debug: bool,
call_head: Span,
) -> Result<PipelineData, ShellError> {
let starting_error_count = working_set.parse_errors.len();
parse(working_set, filename, contents, false);
check_parse(starting_error_count, working_set, is_debug, None, call_head)
}
fn check_parse(
starting_error_count: usize,
working_set: &StateWorkingSet,
is_debug: bool,
help: Option<String>,
call_head: Span,
) -> Result<PipelineData, ShellError> {
if starting_error_count != working_set.parse_errors.len() {
let msg = format!(
r#"Found : {}"#,
working_set
.parse_errors
.first()
.expect("Missing parser error")
);
if is_debug {
Err(ShellError::GenericError {
error: "Failed to parse content".into(),
msg,
span: Some(call_head),
help,
inner: vec![],
})
} else {
Ok(PipelineData::Value(Value::bool(false, call_head), None))
}
} else {
Ok(PipelineData::Value(Value::bool(true, call_head), None))
}
}
fn parse_file_script(
path: &Path,
working_set: &mut StateWorkingSet,
is_debug: bool,
path_span: Span,
call_head: Span,
) -> Result<PipelineData, ShellError> {
let filename = check_path(working_set, path_span, call_head)?;
if let Ok(contents) = std::fs::read(path) {
parse_script(working_set, Some(&filename), &contents, is_debug, call_head)
} else {
Err(ShellError::IOErrorSpanned {
msg: "Could not read path".to_string(),
span: path_span,
})
}
}
fn parse_file_or_dir_module(
path_bytes: &[u8],
working_set: &mut StateWorkingSet,
is_debug: bool,
path_span: Span,
call_head: Span,
) -> Result<PipelineData, ShellError> {
let _ = check_path(working_set, path_span, call_head)?;
let starting_error_count = working_set.parse_errors.len();
let _ = parse_module_file_or_dir(working_set, path_bytes, path_span, None);
if starting_error_count != working_set.parse_errors.len() {
if is_debug {
let msg = format!(
r#"Found : {}"#,
working_set
.parse_errors
.first()
.expect("Missing parser error")
);
Err(ShellError::GenericError {
error: "Failed to parse content".into(),
msg,
span: Some(path_span),
help: Some("If the content is intended to be a script, please try to remove `--as-module` flag ".into()),
inner: vec![],
})
} else {
Ok(PipelineData::Value(Value::bool(false, call_head), None))
}
} else {
Ok(PipelineData::Value(Value::bool(true, call_head), None))
}
}
fn check_path(
working_set: &mut StateWorkingSet,
path_span: Span,
call_head: Span,
) -> Result<String, ShellError> {
let bytes = working_set.get_span_contents(path_span);
let (filename, err) = unescape_unquote_string(bytes, path_span);
if let Some(e) = err {
Err(ShellError::GenericError {
error: "Could not escape filename".to_string(),
msg: "could not escape filename".to_string(),
span: Some(call_head),
help: Some(format!("Returned error: {e}")),
inner: vec![],
})
} else {
Ok(filename)
}
}