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