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#[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 if call.get_parser_info(stack, "noop").is_some() {
69 return Ok(input);
70 }
71
72 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 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 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 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 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 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 bind_main_arguments(eval_engine_state, stack, call, &signature, &mut call_eval)?;
159 call_eval.finalize_for_signature(&signature)?;
160
161 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 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 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
224fn 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
300fn 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
325fn 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
347fn matches_named_flag(named: &Flag, long: &str, short: Option<&str>) -> bool {
352 if named.long == long {
353 return true;
354 }
355
356 match short.and_then(|name| name.chars().next()) {
362 Some(short_char) => named.short == Some(short_char),
363 None => false,
364 }
365}
366
367fn 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
379fn 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
420fn 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}