Skip to main content

nu_command/misc/
run.rs

1use nu_engine::{
2    CallEval, command_prelude::*, get_eval_block_with_early_return, get_eval_expression,
3};
4use nu_parser::{find_main_block_id_in_script, parse};
5use nu_path::{absolute_with, is_windows_device_path};
6use nu_protocol::{
7    BlockId, Value,
8    ast::Block,
9    engine::{CommandType, StateWorkingSet},
10    parser_path::{MAX_RUN_SCRIPT_BYTES, ScriptLoadError, read_run_script_file},
11    shell_error::{generic::GenericError, io::IoError},
12};
13use std::sync::Arc;
14
15/// Run a script file in an isolated scope as part of a pipeline.
16#[derive(Clone)]
17pub struct Run;
18
19impl Command for Run {
20    fn name(&self) -> &str {
21        "run"
22    }
23
24    fn signature(&self) -> Signature {
25        Signature::build("run")
26            .input_output_types(vec![(Type::Any, Type::Any)])
27            .required(
28                "filename",
29                SyntaxShape::OneOf(vec![SyntaxShape::Filepath, SyntaxShape::Nothing]),
30                "The filepath to the script file to run (`null` for no-op).",
31            )
32            .rest(
33                "arguments",
34                SyntaxShape::Any,
35                "Arguments to pass to the script's `def main` if it exists.",
36            )
37            .switch(
38                "full-reparse",
39                "Reload and reparse the script on every invocation instead of using parser-cached blocks.",
40                Some('f'),
41            )
42            .allows_unknown_args()
43            .category(Category::Core)
44    }
45
46    fn description(&self) -> &str {
47        "Runs a script file in an isolated scope as part of a pipeline."
48    }
49
50    fn extra_description(&self) -> &str {
51        "This command is a parser keyword. For details, check:
52   https://www.nushell.sh/book/thinking_in_nu.html"
53    }
54
55    fn command_type(&self) -> CommandType {
56        CommandType::Keyword
57    }
58
59    fn run(
60        &self,
61        engine_state: &EngineState,
62        stack: &mut Stack,
63        call: &Call,
64        input: PipelineData,
65    ) -> Result<PipelineData, ShellError> {
66        // `run null` is parsed as a no-op so pipelines can keep flowing without
67        // introducing conditional command dispatch in the runtime path.
68        if call.get_parser_info(stack, "noop").is_some() {
69            return Ok(input);
70        }
71
72        // Parser-time metadata tells us exactly which script block was compiled for this call.
73        // We intentionally execute that precompiled block instead of reparsing at runtime.
74        //
75        let block_id_name: String = call.req_parser_info(engine_state, stack, "block_id_name")?;
76        let full_reparse = call.get_parser_info(stack, "full_reparse").is_some();
77
78        // Resolve the script path to an absolute path for consistent `CURRENT_FILE` / `FILE_PWD`
79        // behavior. Device paths on Windows are already absolute-like and must be preserved.
80        let cwd = engine_state.cwd_as_string(Some(stack))?;
81        let pb = std::path::PathBuf::from(block_id_name);
82        let parent = pb.parent().unwrap_or(std::path::Path::new(""));
83        let file_path = if is_windows_device_path(pb.as_path()) {
84            pb.clone()
85        } else {
86            let path = absolute_with(pb.as_path(), cwd)
87                .map_err(|err| IoError::new(err, call.head, pb.clone()))?;
88            match path.try_exists() {
89                Ok(true) => {}
90                Ok(false) => {
91                    return Err(IoError::new(ErrorKind::FileNotFound, call.head, pb.clone()).into());
92                }
93                Err(e) => return Err(IoError::new(e, call.head, pb.clone()).into()),
94            };
95            path
96        };
97
98        let mut full_reparse_engine_state = None;
99        let (block, main_block) = if full_reparse {
100            let (reparsed_engine_state, reparsed_block, reparsed_main_block_id) =
101                parse_run_script_fresh(engine_state, &file_path, call.head)?;
102            let reparsed_main_block =
103                reparsed_main_block_id.map(|id| reparsed_engine_state.get_block(id).clone());
104            full_reparse_engine_state = Some(reparsed_engine_state);
105            (reparsed_block, reparsed_main_block)
106        } else {
107            // - `block_id`: block compiled from the resolved script file
108            let block_id: i64 = call.req_parser_info(engine_state, stack, "block_id")?;
109            let block_id = BlockId::new(block_id as usize);
110            let block = engine_state.get_block(block_id).clone();
111            let main_block = if call.get_parser_info(stack, "main_block_id").is_some() {
112                let main_block_id: i64 =
113                    call.req_parser_info(engine_state, stack, "main_block_id")?;
114                Some(
115                    engine_state
116                        .get_block(BlockId::new(main_block_id as usize))
117                        .clone(),
118                )
119            } else {
120                None
121            };
122            (block, main_block)
123        };
124        let eval_engine_state = full_reparse_engine_state.as_ref().unwrap_or(engine_state);
125
126        // Stash caller values so we can restore them after execution. `run` should expose file
127        // context to the script, but must not leak modified values back to the caller.
128        let old_file_pwd = stack.get_env_var(engine_state, "FILE_PWD").cloned();
129        let old_current_file = stack.get_env_var(engine_state, "CURRENT_FILE").cloned();
130
131        // Mirror `source`-style file context for script execution.
132        stack.add_env_var(
133            "FILE_PWD".to_string(),
134            Value::string(parent.to_string_lossy(), call.head),
135        );
136        stack.add_env_var(
137            "CURRENT_FILE".to_string(),
138            Value::string(file_path.to_string_lossy(), call.head),
139        );
140
141        let eval_block_with_early_return = get_eval_block_with_early_return(eval_engine_state);
142        let return_result = (|| {
143            // If parser metadata includes a `main` entrypoint, invoke that specific declaration.
144            // Otherwise evaluate the full script block as a pipeline transform.
145            if let Some(main_block) = main_block.clone() {
146                let signature = (*main_block.signature).clone();
147                let callee_stack = stack.gather_captures(eval_engine_state, &main_block.captures);
148                let mut call_eval = CallEval::new(
149                    callee_stack,
150                    call.head,
151                    main_block.span.unwrap_or(call.head),
152                    eval_block_with_early_return,
153                );
154
155                // Forward remaining run arguments (`run file.nu ...args`) to `main`.
156                // This helper normalizes long/short flags and supports AST+IR call representations
157                // while delegating actual binding/type validation to CallEval.
158                bind_main_arguments(eval_engine_state, stack, call, &signature, &mut call_eval)?;
159                call_eval.finalize_for_signature(&signature)?;
160
161                // Execute a signature-stripped copy of `main` after manually binding all
162                // arguments so pipeline input remains available as `$in` and is not rebound
163                // to positional parameters by call-time argument machinery.
164                // Pipeline input passes through as `$in`; positional args come only from
165                // explicit `run file.nu ...args` tokens bound above.
166                let mut executable_main_block = (*main_block).clone();
167                *executable_main_block.signature = Signature::new("main");
168
169                call_eval.run_prebound(eval_engine_state, &executable_main_block, input)
170            } else {
171                // No explicit `main`: execute the script block directly in an isolated child stack.
172                // Parent scope values remain readable via stack parenting, but script mutations do
173                // not leak back to the caller.
174                let parent_stack = Arc::new(stack.clone());
175                let mut callee_stack = Stack::with_parent(parent_stack);
176                eval_block_with_early_return(eval_engine_state, &mut callee_stack, &block, input)
177                    .map(|p| p.body)
178            }
179        })();
180
181        // Always restore caller file-context env after script evaluation (success or error).
182        // If values did not exist before `run`, remove them instead of leaving command-introduced
183        // entries behind.
184        if let Some(old_file_pwd) = old_file_pwd {
185            stack.add_env_var("FILE_PWD".to_string(), old_file_pwd);
186        } else {
187            stack.remove_env_var(engine_state, "FILE_PWD");
188        }
189        if let Some(old_current_file) = old_current_file {
190            stack.add_env_var("CURRENT_FILE".to_string(), old_current_file);
191        } else {
192            stack.remove_env_var(engine_state, "CURRENT_FILE");
193        }
194
195        return_result
196    }
197
198    fn examples(&self) -> Vec<Example<'_>> {
199        vec![
200            Example {
201                description: "Run a simple transformation script in a pipeline.",
202                example: r#""hello" | run transform.nu"#,
203                result: None,
204            },
205            Example {
206                description: "Run a script with arguments.",
207                example: r#""test" | run format.nu --prefix ">>>" "#,
208                result: None,
209            },
210            Example {
211                description: "Run a script as part of a larger pipeline.",
212                example: "ls | run process.nu | select name size",
213                result: None,
214            },
215            Example {
216                description: "Always reload and reparse a script before each invocation.",
217                example: "watch . -g *.nu | each -f { run --full-reparse ./test.nu }",
218                result: None,
219            },
220        ]
221    }
222}
223
224/// Reload, reparse, and compile a script file against a cloned engine state.
225///
226/// This is used by `run --full-reparse` to bypass parser-time script caching while keeping
227/// declaration resolution and execution isolated from the caller's engine state.
228///
229/// Parse errors are surfaced at runtime as `ShellError::Generic`, which is an intentional behavior
230/// difference from parse-time `run` compilation.
231fn parse_run_script_fresh(
232    engine_state: &EngineState,
233    file_path: &std::path::Path,
234    call_head: Span,
235) -> Result<(EngineState, Arc<Block>, Option<BlockId>), ShellError> {
236    let display_path = file_path.display().to_string();
237    let contents = match read_run_script_file(file_path, MAX_RUN_SCRIPT_BYTES) {
238        Ok(contents) => contents,
239        Err(ScriptLoadError::TooLarge { size, max_size }) => {
240            return Err(GenericError::new(
241                "Script file is too large to load",
242                format!(
243                    "Refusing to load files larger than {max_size} bytes for `run` (file is {size} bytes): {display_path}"
244                ),
245                call_head,
246            )
247            .into());
248        }
249        Err(ScriptLoadError::NotText) => {
250            return Err(GenericError::new(
251                "Script file does not appear to be text",
252                format!(
253                    "The file does not look like UTF-8 text and cannot be loaded by `run`: {display_path}"
254                ),
255                call_head,
256            )
257            .into());
258        }
259        Err(ScriptLoadError::Unreadable) => {
260            return Err(GenericError::new(
261                "Failed to read script",
262                format!("Could not read script file: {display_path}"),
263                call_head,
264            )
265            .into());
266        }
267    };
268
269    let mut full_reparse_engine_state = engine_state.clone();
270    let mut working_set = StateWorkingSet::new(&full_reparse_engine_state);
271    working_set
272        .files
273        .push(file_path.to_path_buf(), call_head)
274        .map_err(|err| GenericError::new("Failed to parse script", err.to_string(), call_head))?;
275
276    let filename = file_path.to_string_lossy();
277    let script_block = parse(&mut working_set, Some(filename.as_ref()), &contents, false);
278    let script_main_block_id = find_main_block_id_in_script(&working_set, &script_block);
279    working_set.files.pop();
280
281    if let Some(parse_error) = working_set.parse_errors.first() {
282        return Err(GenericError::new(
283            "Failed to parse script",
284            parse_error.to_string(),
285            call_head,
286        )
287        .into());
288    }
289
290    let delta = working_set.render();
291    full_reparse_engine_state.merge_delta(delta)?;
292
293    Ok((
294        full_reparse_engine_state,
295        script_block,
296        script_main_block_id,
297    ))
298}
299
300/// Parse a source token that looks like a long or short named flag.
301///
302/// Returns `(long_name, short_name)` where:
303/// - `--char` becomes `("char", None)`
304/// - `-c` becomes `("c", Some("c"))`
305fn parse_flag_name(token: &str) -> Option<(String, Option<String>)> {
306    if let Some(flag_name) = token.strip_prefix("--")
307        && !flag_name.is_empty()
308    {
309        return Some((flag_name.to_string(), None));
310    }
311
312    let mut chars = token.chars();
313    if chars.next() == Some('-')
314        && let Some(short) = chars.next()
315        && chars.next().is_none()
316        && short.is_ascii_alphabetic()
317    {
318        let short = short.to_string();
319        return Some((short.clone(), Some(short)));
320    }
321
322    None
323}
324
325/// Parse a forwarded argument value into a flag token.
326///
327/// Source text is preferred so quoted literals like `"-c"` stay positional values.
328fn parse_flag_token(engine_state: &EngineState, value: &Value) -> Option<(String, Option<String>)> {
329    let span = value.span();
330    let span_contents = engine_state.get_span_contents(span);
331    if let Ok(token) = std::str::from_utf8(span_contents) {
332        if let Some(flag) = parse_flag_name(token) {
333            return Some(flag);
334        }
335
336        if token.starts_with('"') || token.starts_with('\'') {
337            return None;
338        }
339    }
340
341    match value {
342        Value::String { val, .. } => parse_flag_name(val),
343        _ => None,
344    }
345}
346
347/// Check whether a parsed flag token matches a named parameter from a signature.
348///
349/// Matches on the long name (`--char` → `"char"`) or by comparing the single short character
350/// extracted from a `-c` token against the flag's declared short character.
351fn matches_named_flag(named: &Flag, long: &str, short: Option<&str>) -> bool {
352    if named.long == long {
353        return true;
354    }
355
356    // Only fall back to short-character matching when the token actually was a
357    // short flag (`-c`). A long flag (`--name`) carries `short == None`, and
358    // comparing that directly against `named.short` would treat "no short given"
359    // as equal to "flag declares no short", spuriously matching the first
360    // short-less flag and binding the value to the wrong parameter.
361    match short.and_then(|name| name.chars().next()) {
362        Some(short_char) => named.short == Some(short_char),
363        None => false,
364    }
365}
366
367/// Resolve a parsed flag token to the matching signature flag, if any.
368fn resolve_named_flag<'a>(
369    signature: &'a Signature,
370    long: &str,
371    short: Option<&str>,
372) -> Option<&'a Flag> {
373    signature
374        .named
375        .iter()
376        .find(|named| matches_named_flag(named, long, short))
377}
378
379/// Bind explicit `run file.nu ...args` arguments onto a script `def main` call evaluator.
380fn bind_main_arguments(
381    engine_state: &EngineState,
382    caller_stack: &mut Stack,
383    call: &Call,
384    signature: &Signature,
385    call_eval: &mut CallEval,
386) -> Result<(), ShellError> {
387    let rest_values = collect_explicit_run_arguments(engine_state, caller_stack, call)?;
388
389    let mut index = 0;
390    while index < rest_values.len() {
391        if let Some((long, short)) = parse_flag_token(engine_state, &rest_values[index]) {
392            let matched_flag = resolve_named_flag(signature, &long, short.as_deref());
393            if let Some(flag) = matched_flag {
394                let expects_value = flag.arg.is_some();
395                let value = if expects_value
396                    && index + 1 < rest_values.len()
397                    && parse_flag_token(engine_state, &rest_values[index + 1]).is_none()
398                {
399                    index += 1;
400                    Some(std::borrow::Cow::Owned(rest_values[index].clone()))
401                } else {
402                    None
403                };
404
405                call_eval.add_named(signature, &flag.long, short, value)?;
406            }
407        } else {
408            call_eval.add_positional(
409                signature,
410                std::borrow::Cow::Owned(rest_values[index].clone()),
411            )?;
412        }
413
414        index += 1;
415    }
416
417    Ok(())
418}
419
420/// Collect only the explicit run arguments after the script filename.
421fn collect_explicit_run_arguments(
422    engine_state: &EngineState,
423    stack: &mut Stack,
424    call: &Call,
425) -> Result<Vec<Value>, ShellError> {
426    let eval_expression = get_eval_expression(engine_state);
427    call.rest_iter_flattened(engine_state, stack, eval_expression, 1)
428}