nu_cli/
eval_file.rs

1use crate::util::{eval_source, print_pipeline};
2use log::{info, trace};
3use nu_engine::eval_block;
4use nu_parser::parse;
5use nu_path::canonicalize_with;
6use nu_protocol::{
7    PipelineData, ShellError, Span, Value,
8    cli_error::report_compile_error,
9    debugger::WithoutDebug,
10    engine::{EngineState, Stack, StateWorkingSet},
11    report_parse_error, report_parse_warning,
12    shell_error::io::*,
13};
14use std::{path::PathBuf, sync::Arc};
15
16/// Entry point for evaluating a file.
17///
18/// If the file contains a main command, it is invoked with `args` and the pipeline data from `input`;
19/// otherwise, the pipeline data is forwarded to the first command in the file, and `args` are ignored.
20pub fn evaluate_file(
21    path: String,
22    args: &[String],
23    engine_state: &mut EngineState,
24    stack: &mut Stack,
25    input: PipelineData,
26) -> Result<(), ShellError> {
27    let cwd = engine_state.cwd_as_string(Some(stack))?;
28
29    let file_path = canonicalize_with(&path, cwd).map_err(|err| {
30        IoError::new_internal_with_path(
31            err.not_found_as(NotFound::File),
32            "Could not access file",
33            nu_protocol::location!(),
34            PathBuf::from(&path),
35        )
36    })?;
37
38    let file_path_str = file_path
39        .to_str()
40        .ok_or_else(|| ShellError::NonUtf8Custom {
41            msg: format!(
42                "Input file name '{}' is not valid UTF8",
43                file_path.to_string_lossy()
44            ),
45            span: Span::unknown(),
46        })?;
47
48    let file = std::fs::read(&file_path).map_err(|err| {
49        IoError::new_internal_with_path(
50            err.not_found_as(NotFound::File),
51            "Could not read file",
52            nu_protocol::location!(),
53            file_path.clone(),
54        )
55    })?;
56    engine_state.file = Some(file_path.clone());
57
58    let parent = file_path.parent().ok_or_else(|| {
59        IoError::new_internal_with_path(
60            ErrorKind::DirectoryNotFound,
61            "The file path does not have a parent",
62            nu_protocol::location!(),
63            file_path.clone(),
64        )
65    })?;
66
67    stack.add_env_var(
68        "FILE_PWD".to_string(),
69        Value::string(parent.to_string_lossy(), Span::unknown()),
70    );
71    stack.add_env_var(
72        "CURRENT_FILE".to_string(),
73        Value::string(file_path.to_string_lossy(), Span::unknown()),
74    );
75    stack.add_env_var(
76        "PROCESS_PATH".to_string(),
77        Value::string(path, Span::unknown()),
78    );
79
80    let source_filename = file_path
81        .file_name()
82        .expect("internal error: missing filename");
83
84    let mut working_set = StateWorkingSet::new(engine_state);
85    trace!("parsing file: {}", file_path_str);
86    let block = parse(&mut working_set, Some(file_path_str), &file, false);
87
88    if let Some(warning) = working_set.parse_warnings.first() {
89        report_parse_warning(&working_set, warning);
90    }
91
92    // If any parse errors were found, report the first error and exit.
93    if let Some(err) = working_set.parse_errors.first() {
94        report_parse_error(&working_set, err);
95        std::process::exit(1);
96    }
97
98    if let Some(err) = working_set.compile_errors.first() {
99        report_compile_error(&working_set, err);
100        std::process::exit(1);
101    }
102
103    // Look for blocks whose name starts with "main" and replace it with the filename.
104    for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) {
105        if block.signature.name == "main" {
106            block.signature.name = source_filename.to_string_lossy().to_string();
107        } else if block.signature.name.starts_with("main ") {
108            block.signature.name =
109                source_filename.to_string_lossy().to_string() + " " + &block.signature.name[5..];
110        }
111    }
112
113    // Merge the changes into the engine state.
114    engine_state.merge_delta(working_set.delta)?;
115
116    // Check if the file contains a main command.
117    let exit_code = if engine_state.find_decl(b"main", &[]).is_some() {
118        // Evaluate the file, but don't run main yet.
119        let pipeline =
120            match eval_block::<WithoutDebug>(engine_state, stack, &block, PipelineData::empty()) {
121                Ok(data) => data,
122                Err(ShellError::Return { .. }) => {
123                    // Allow early return before main is run.
124                    return Ok(());
125                }
126                Err(err) => return Err(err),
127            };
128
129        // Print the pipeline output of the last command of the file.
130        print_pipeline(engine_state, stack, pipeline, true)?;
131
132        // Invoke the main command with arguments.
133        // Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
134        let args = format!("main {}", args.join(" "));
135        eval_source(
136            engine_state,
137            stack,
138            args.as_bytes(),
139            "<commandline>",
140            input,
141            true,
142        )
143    } else {
144        eval_source(engine_state, stack, &file, file_path_str, input, true)
145    };
146
147    if exit_code != 0 {
148        std::process::exit(exit_code);
149    }
150
151    info!("evaluate {}:{}:{}", file!(), line!(), column!());
152
153    Ok(())
154}