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