use crate::util::{eval_source, print_pipeline};
use log::{info, trace};
use nu_engine::eval_block;
use nu_parser::{escape_for_script_arg, parse};
use nu_path::absolute_with;
use nu_protocol::{
PipelineData, ShellError, Span, Value,
debugger::WithoutDebug,
engine::{EngineState, Stack, StateWorkingSet},
report_error::report_compile_error,
report_parse_error, report_parse_warning,
shell_error::io::*,
};
use std::{path::PathBuf, sync::Arc};
pub fn evaluate_file(
path: String,
args: &[String],
engine_state: &mut EngineState,
stack: &mut Stack,
input: PipelineData,
) -> Result<(), ShellError> {
let cwd = engine_state.cwd_as_string(Some(stack))?;
let file_path = {
match absolute_with(&path, cwd) {
Ok(t) => Ok(t),
Err(err) => Err(IoError::new_internal_with_path(
err,
"Invalid path",
PathBuf::from(&path),
)),
}
}?;
let file_path_str = file_path
.to_str()
.ok_or_else(|| ShellError::NonUtf8Custom {
msg: format!(
"Input file name '{}' is not valid UTF8",
file_path.to_string_lossy()
),
span: Span::unknown(),
})?;
let file = std::fs::read(&file_path).map_err(|err| {
let cmdline = format!("nu {path} {}", args.join(" "));
let mut working_set = StateWorkingSet::new(engine_state);
let file_id = working_set.add_file("<commandline>".into(), cmdline.as_bytes());
let span = working_set
.get_span_for_file(file_id)
.subspan(3, path.len() + 3)
.expect("<commandline> to contain script path");
if let Err(err) = engine_state.merge_delta(working_set.render()) {
err
} else {
IoError::new(err.not_found_as(NotFound::File), span, PathBuf::from(&path)).into()
}
})?;
engine_state.file = Some(file_path.clone());
let parent = file_path.parent().ok_or_else(|| {
IoError::new_internal_with_path(
ErrorKind::DirectoryNotFound,
"The file path does not have a parent",
file_path.clone(),
)
})?;
stack.add_env_var(
"FILE_PWD".to_string(),
Value::string(parent.to_string_lossy(), Span::unknown()),
);
stack.add_env_var(
"CURRENT_FILE".to_string(),
Value::string(file_path.to_string_lossy(), Span::unknown()),
);
stack.add_env_var(
"PROCESS_PATH".to_string(),
Value::string(path, Span::unknown()),
);
let source_filename = file_path
.file_name()
.expect("internal error: missing filename");
let script_name = source_filename.to_string_lossy().to_string();
let script_name_bytes = script_name.as_bytes().to_vec();
let mut working_set = StateWorkingSet::new(engine_state);
trace!("parsing file: {file_path_str}");
let block = parse(&mut working_set, Some(file_path_str), &file, false);
if let Some(warning) = working_set.parse_warnings.first() {
report_parse_warning(None, &working_set, warning);
}
if let Some(err) = working_set.parse_errors.first() {
report_parse_error(None, &working_set, err);
std::process::exit(1);
}
if let Some(err) = working_set.compile_errors.first() {
report_compile_error(None, &working_set, err);
std::process::exit(1);
}
let mut file_has_main = false;
for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) {
if block.signature.name == "main" {
file_has_main = true;
block.signature.name = script_name.clone();
} else if block.signature.name.starts_with("main ") {
file_has_main = true;
block.signature.name = script_name.clone() + " " + &block.signature.name[5..];
}
}
if file_has_main && let Some(overlay) = working_set.delta.last_overlay_mut() {
let mut new_decls = Vec::new();
for (name, &decl_id) in &overlay.decls {
if name == b"main" || name.starts_with(b"main ") {
let mut new_name = script_name_bytes.clone();
if name.len() > 4 {
new_name.extend_from_slice(&name[4..]);
}
new_decls.push((new_name, decl_id));
}
}
for (n, id) in new_decls {
overlay.decls.insert(n, id);
}
let mut new_predecls = Vec::new();
for (name, &decl_id) in &overlay.predecls {
if name == b"main" || name.starts_with(b"main ") {
let mut new_name = script_name_bytes.clone();
if name.len() > 4 {
new_name.extend_from_slice(&name[4..]);
}
new_predecls.push((new_name, decl_id));
}
}
for (n, id) in new_predecls {
overlay.predecls.insert(n, id);
}
}
engine_state.merge_delta(working_set.delta)?;
let exit_code = if file_has_main && engine_state.find_decl(&script_name_bytes, &[]).is_some() {
let pipeline =
match eval_block::<WithoutDebug>(engine_state, stack, &block, PipelineData::empty())
.map(|p| p.body)
{
Ok(data) => data,
Err(ShellError::Return { .. }) => {
return Ok(());
}
Err(err) => return Err(err),
};
print_pipeline(engine_state, stack, pipeline, true)?;
let args = format!(
"main {}",
args.iter()
.map(|arg| escape_for_script_arg(arg))
.collect::<Vec<_>>()
.join(" ")
);
eval_source(
engine_state,
stack,
args.as_bytes(),
"<commandline>",
input,
true,
)
} else {
eval_source(engine_state, stack, &file, file_path_str, input, true)
};
if exit_code != 0 {
std::process::exit(exit_code);
}
info!("evaluate {}:{}:{}", file!(), line!(), column!());
Ok(())
}
#[cfg(test)]
mod tests {
use nu_test_support::fs::Stub::FileWithContent;
use nu_test_support::playground::Playground;
use nu_test_support::prelude::*;
#[test]
fn evaluate_file_arg_with_newline_does_not_split_commands() -> Result {
Playground::setup("evaluate_file_newline_arg", |dirs, sandbox| {
sandbox.with_files(&[FileWithContent(
"test.nu",
"def main [...args: string] { print ...($args) }",
)]);
test()
.cwd(dirs.test())
.add_nu_to_path()
.run("nu test.nu a b \"c\\nd\"; 'ok'")
.expect_value_eq("ok")
})
}
}