Skip to main content

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::absolute_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 absolute_with(&path, cwd) {
31            Ok(t) => Ok(t),
32            Err(err) => Err(IoError::new_internal_with_path(
33                err,
34                "Invalid path",
35                nu_protocol::location!(),
36                PathBuf::from(&path),
37            )),
38        }
39    }?;
40
41    let file_path_str = file_path
42        .to_str()
43        .ok_or_else(|| ShellError::NonUtf8Custom {
44            msg: format!(
45                "Input file name '{}' is not valid UTF8",
46                file_path.to_string_lossy()
47            ),
48            span: Span::unknown(),
49        })?;
50
51    let file = std::fs::read(&file_path).map_err(|err| {
52        let cmdline = format!("nu {path} {}", args.join(" "));
53        let mut working_set = StateWorkingSet::new(engine_state);
54        let file_id = working_set.add_file("<commandline>".into(), cmdline.as_bytes());
55        let span = working_set
56            .get_span_for_file(file_id)
57            .subspan(3, path.len() + 3)
58            .expect("<commandline> to contain script path");
59        if let Err(err) = engine_state.merge_delta(working_set.render()) {
60            err
61        } else {
62            IoError::new(err.not_found_as(NotFound::File), span, PathBuf::from(&path)).into()
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    // we'll need the script name repeatedly; keep both String and bytes
94    let script_name = source_filename.to_string_lossy().to_string();
95    let script_name_bytes = script_name.as_bytes().to_vec();
96
97    let mut working_set = StateWorkingSet::new(engine_state);
98    trace!("parsing file: {file_path_str}");
99    let block = parse(&mut working_set, Some(file_path_str), &file, false);
100
101    if let Some(warning) = working_set.parse_warnings.first() {
102        report_parse_warning(None, &working_set, warning);
103    }
104
105    // If any parse errors were found, report the first error and exit.
106    if let Some(err) = working_set.parse_errors.first() {
107        report_parse_error(None, &working_set, err);
108        std::process::exit(1);
109    }
110
111    if let Some(err) = working_set.compile_errors.first() {
112        report_compile_error(None, &working_set, err);
113        std::process::exit(1);
114    }
115
116    // Look for blocks whose name is `main` or begins with `main `; if any are
117    // found we:
118    // 1. rewrite the signature to use the script's filename,
119    // 2. remember that the file contained a `main` command, and
120    // 3. later add an alias in the overlay so users can still call `main`.
121    let mut file_has_main = false;
122    for block in working_set.delta.blocks.iter_mut().map(Arc::make_mut) {
123        if block.signature.name == "main" {
124            file_has_main = true;
125            block.signature.name = script_name.clone();
126        } else if block.signature.name.starts_with("main ") {
127            file_has_main = true;
128            block.signature.name = script_name.clone() + " " + &block.signature.name[5..];
129        }
130    }
131
132    // If we found a main declaration, alias the overlay entries so that
133    // `script.nu` (and `script.nu foo`) resolve just like `main`.
134    if file_has_main && let Some(overlay) = working_set.delta.last_overlay_mut() {
135        // Collect new entries to avoid mutating while iterating.
136        // For "main" → new_name is just the script filename.
137        // For "main foo" → name[4..] is " foo" (space included), giving "script.nu foo".
138        let mut new_decls = Vec::new();
139        for (name, &decl_id) in &overlay.decls {
140            if name == b"main" || name.starts_with(b"main ") {
141                let mut new_name = script_name_bytes.clone();
142                if name.len() > 4 {
143                    new_name.extend_from_slice(&name[4..]);
144                }
145                new_decls.push((new_name, decl_id));
146            }
147        }
148        for (n, id) in new_decls {
149            overlay.decls.insert(n, id);
150        }
151
152        let mut new_predecls = Vec::new();
153        for (name, &decl_id) in &overlay.predecls {
154            if name == b"main" || name.starts_with(b"main ") {
155                let mut new_name = script_name_bytes.clone();
156                if name.len() > 4 {
157                    new_name.extend_from_slice(&name[4..]);
158                }
159                new_predecls.push((new_name, decl_id));
160            }
161        }
162        for (n, id) in new_predecls {
163            overlay.predecls.insert(n, id);
164        }
165    }
166
167    // Merge the changes into the engine state.
168    engine_state.merge_delta(working_set.delta)?;
169
170    // Check if the file contains a main command.  We use the script name instead
171    // of the literal `main` because the delta (above) may have rewritten the
172    // declaration and added an alias.
173    let exit_code = if file_has_main && engine_state.find_decl(&script_name_bytes, &[]).is_some() {
174        // Evaluate the file, but don't run main yet.
175        let pipeline =
176            match eval_block::<WithoutDebug>(engine_state, stack, &block, PipelineData::empty())
177                .map(|p| p.body)
178            {
179                Ok(data) => data,
180                Err(ShellError::Return { .. }) => {
181                    // Allow early return before main is run.
182                    return Ok(());
183                }
184                Err(err) => return Err(err),
185            };
186
187        // Print the pipeline output of the last command of the file.
188        print_pipeline(engine_state, stack, pipeline, true)?;
189
190        // Invoke the main command with arguments.  Keep using `main` as the
191        // internal command name so the parser reliably resolves it; the block's
192        // signature was already rewritten to the script filename above, so help
193        // messages will show the correct `script.nu`-qualified name.
194        // Arguments with whitespaces are quoted, thus can be safely concatenated by whitespace.
195        let args = format!("main {}", args.join(" "));
196        eval_source(
197            engine_state,
198            stack,
199            args.as_bytes(),
200            "<commandline>",
201            input,
202            true,
203        )
204    } else {
205        eval_source(engine_state, stack, &file, file_path_str, input, true)
206    };
207
208    if exit_code != 0 {
209        std::process::exit(exit_code);
210    }
211
212    info!("evaluate {}:{}:{}", file!(), line!(), column!());
213
214    Ok(())
215}