1use nu_cmd_base::hook::eval_hook;
2use nu_engine::{command_prelude::*, env_to_strings};
3use nu_path::{AbsolutePath, dots::expand_ndots_safe, expand_tilde};
4use nu_protocol::{
5 ByteStream, NuGlob, OutDest, Signals, UseAnsiColoring, did_you_mean,
6 process::{ChildProcess, PostWaitCallback},
7 shell_error::io::IoError,
8};
9use nu_system::{ForegroundChild, kill_by_pid};
10use nu_utils::IgnoreCaseExt;
11use pathdiff::diff_paths;
12#[cfg(windows)]
13use std::os::windows::process::CommandExt;
14use std::{
15 borrow::Cow,
16 ffi::{OsStr, OsString},
17 io::Write,
18 path::{Path, PathBuf},
19 process::Stdio,
20 sync::Arc,
21 thread,
22};
23
24#[derive(Clone)]
25pub struct External;
26
27impl Command for External {
28 fn name(&self) -> &str {
29 "run-external"
30 }
31
32 fn description(&self) -> &str {
33 "Runs external command."
34 }
35
36 fn signature(&self) -> nu_protocol::Signature {
37 Signature::build(self.name())
38 .input_output_types(vec![(Type::Any, Type::Any)])
39 .rest(
40 "command",
41 SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Any]),
42 "External command to run, with arguments.",
43 )
44 .category(Category::System)
45 }
46
47 fn run(
48 &self,
49 engine_state: &EngineState,
50 stack: &mut Stack,
51 call: &Call,
52 input: PipelineData,
53 ) -> Result<PipelineData, ShellError> {
54 let cwd = engine_state.cwd(Some(stack))?;
55 let rest = call.rest::<Value>(engine_state, stack, 0)?;
56 let name_args = rest.split_first().map(|(x, y)| (x, y.to_vec()));
57
58 let Some((name, mut call_args)) = name_args else {
59 return Err(ShellError::MissingParameter {
60 param_name: "no command given".into(),
61 span: call.head,
62 });
63 };
64
65 let name_str: Cow<str> = match &name {
66 Value::Glob { val, .. } => Cow::Borrowed(val),
67 Value::String { val, .. } => Cow::Borrowed(val),
68 Value::List { vals, .. } => {
69 let Some((first, args)) = vals.split_first() else {
70 return Err(ShellError::MissingParameter {
71 param_name: "external command given as list empty".into(),
72 span: call.head,
73 });
74 };
75 call_args.splice(0..0, args.to_vec());
77 first.coerce_str()?
78 }
79 _ => Cow::Owned(name.clone().coerce_into_string()?),
80 };
81
82 let expanded_name = match &name {
83 Value::Glob { no_expand, .. } if !*no_expand => {
85 expand_ndots_safe(expand_tilde(&*name_str))
86 }
87 _ => Path::new(&*name_str).to_owned(),
88 };
89
90 let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
91
92 let pathext_script_in_windows = if cfg!(windows) {
106 if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) {
107 let ext = executable
108 .extension()
109 .unwrap_or_default()
110 .to_string_lossy()
111 .to_uppercase();
112
113 !["COM", "EXE", "BAT", "CMD", "PS1"]
114 .iter()
115 .any(|c| *c == ext)
116 } else {
117 false
118 }
119 } else {
120 false
121 };
122
123 let (potential_powershell_script, path_to_ps1_executable) = if cfg!(windows) {
125 if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) {
126 let ext = executable
127 .extension()
128 .unwrap_or_default()
129 .to_string_lossy()
130 .to_uppercase();
131 (ext == "PS1", Some(executable))
132 } else {
133 (false, None)
134 }
135 } else {
136 (false, None)
137 };
138
139 let executable = if cfg!(windows)
143 && (is_cmd_internal_command(&name_str) || pathext_script_in_windows)
144 {
145 PathBuf::from("cmd.exe")
146 } else if cfg!(windows) && potential_powershell_script && path_to_ps1_executable.is_some() {
147 PathBuf::from("powershell.exe")
151 } else {
152 let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
155 return Err(command_not_found(
156 &name_str,
157 call.head,
158 engine_state,
159 stack,
160 &cwd,
161 ));
162 };
163 executable
164 };
165
166 let mut command = std::process::Command::new(&executable);
168
169 command.current_dir(cwd);
171
172 let envs = env_to_strings(engine_state, stack)?;
174 command.env_clear();
175 command.envs(envs);
176
177 let args = eval_external_arguments(engine_state, stack, call_args)?;
179 #[cfg(windows)]
180 if is_cmd_internal_command(&name_str) || pathext_script_in_windows {
181 command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
185 for arg in &args {
186 command.raw_arg(escape_cmd_argument(arg)?);
187 }
188 } else if potential_powershell_script {
189 command.args([
190 "-File",
191 &path_to_ps1_executable.unwrap_or_default().to_string_lossy(),
192 ]);
193 command.args(args.into_iter().map(|s| s.item));
194 } else {
195 command.args(args.into_iter().map(|s| s.item));
196 }
197 #[cfg(not(windows))]
198 command.args(args.into_iter().map(|s| s.item));
199
200 let stdout = stack.stdout();
203 let stderr = stack.stderr();
204 let merged_stream = if matches!(stdout, OutDest::Pipe) && matches!(stderr, OutDest::Pipe) {
205 let (reader, writer) =
206 os_pipe::pipe().map_err(|err| IoError::new(err, call.head, None))?;
207 command.stdout(
208 writer
209 .try_clone()
210 .map_err(|err| IoError::new(err, call.head, None))?,
211 );
212 command.stderr(writer);
213 Some(reader)
214 } else {
215 if engine_state.is_background_job()
216 && matches!(stdout, OutDest::Inherit | OutDest::Print)
217 {
218 command.stdout(Stdio::null());
219 } else {
220 command.stdout(
221 Stdio::try_from(stdout).map_err(|err| IoError::new(err, call.head, None))?,
222 );
223 }
224
225 if engine_state.is_background_job()
226 && matches!(stderr, OutDest::Inherit | OutDest::Print)
227 {
228 command.stderr(Stdio::null());
229 } else {
230 command.stderr(
231 Stdio::try_from(stderr).map_err(|err| IoError::new(err, call.head, None))?,
232 );
233 }
234
235 None
236 };
237
238 let data_to_copy_into_stdin = match input {
242 PipelineData::ByteStream(stream, metadata) => match stream.into_stdio() {
243 Ok(stdin) => {
244 command.stdin(stdin);
245 None
246 }
247 Err(stream) => {
248 command.stdin(Stdio::piped());
249 Some(PipelineData::ByteStream(stream, metadata))
250 }
251 },
252 PipelineData::Empty => {
253 command.stdin(Stdio::inherit());
254 None
255 }
256 value => {
257 command.stdin(Stdio::piped());
258 Some(value)
259 }
260 };
261
262 log::trace!("run-external spawning: {command:?}");
264
265 #[cfg(windows)]
268 let child = ForegroundChild::spawn(command);
269 #[cfg(unix)]
270 let child = ForegroundChild::spawn(
271 command,
272 engine_state.is_interactive,
273 engine_state.is_background_job(),
274 &engine_state.pipeline_externals_state,
275 );
276
277 let mut child = child.map_err(|err| {
278 let context = format!("Could not spawn foreground child: {err}");
279 IoError::new_internal(err, context, nu_protocol::location!())
280 })?;
281
282 if let Some(thread_job) = engine_state.current_thread_job() {
283 if !thread_job.try_add_pid(child.pid()) {
284 kill_by_pid(child.pid().into()).map_err(|err| {
285 ShellError::Io(IoError::new_internal(
286 err,
287 "Could not spawn external stdin worker",
288 nu_protocol::location!(),
289 ))
290 })?;
291 }
292 }
293
294 if let Some(data) = data_to_copy_into_stdin {
296 let stdin = child.as_mut().stdin.take().expect("stdin is piped");
297 let engine_state = engine_state.clone();
298 let stack = stack.clone();
299 thread::Builder::new()
300 .name("external stdin worker".into())
301 .spawn(move || {
302 let _ = write_pipeline_data(engine_state, stack, data, stdin);
303 })
304 .map_err(|err| {
305 IoError::new_with_additional_context(
306 err,
307 call.head,
308 None,
309 "Could not spawn external stdin worker",
310 )
311 })?;
312 }
313
314 let child_pid = child.pid();
315
316 let mut child = ChildProcess::new(
318 child,
319 merged_stream,
320 matches!(stderr, OutDest::Pipe),
321 call.head,
322 Some(PostWaitCallback::for_job_control(
323 engine_state,
324 Some(child_pid),
325 executable
326 .as_path()
327 .file_name()
328 .and_then(|it| it.to_str())
329 .map(|it| it.to_string()),
330 )),
331 )?;
332
333 if matches!(stdout, OutDest::Pipe | OutDest::PipeSeparate)
334 || matches!(stderr, OutDest::Pipe | OutDest::PipeSeparate)
335 {
336 child.ignore_error(true);
337 }
338
339 Ok(PipelineData::ByteStream(
340 ByteStream::child(child, call.head),
341 None,
342 ))
343 }
344
345 fn examples(&self) -> Vec<Example> {
346 vec![
347 Example {
348 description: "Run an external command",
349 example: r#"run-external "echo" "-n" "hello""#,
350 result: None,
351 },
352 Example {
353 description: "Redirect stdout from an external command into the pipeline",
354 example: r#"run-external "echo" "-n" "hello" | split chars"#,
355 result: None,
356 },
357 Example {
358 description: "Redirect stderr from an external command into the pipeline",
359 example: r#"run-external "nu" "-c" "print -e hello" e>| split chars"#,
360 result: None,
361 },
362 ]
363 }
364}
365
366pub fn eval_external_arguments(
368 engine_state: &EngineState,
369 stack: &mut Stack,
370 call_args: Vec<Value>,
371) -> Result<Vec<Spanned<OsString>>, ShellError> {
372 let cwd = engine_state.cwd(Some(stack))?;
373 let mut args: Vec<Spanned<OsString>> = Vec::with_capacity(call_args.len());
374
375 for arg in call_args {
376 let span = arg.span();
377 match arg {
378 Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
380 expand_glob(
381 &val,
382 cwd.as_std_path(),
383 span,
384 engine_state.signals().clone(),
385 )?
386 .into_iter()
387 .map(|s| s.into_spanned(span)),
388 ),
389 other => args
390 .push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)),
391 }
392 }
393 Ok(args)
394}
395
396fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result<String, ShellError> {
399 match val {
400 Value::List { .. } => Err(ShellError::CannotPassListToExternal {
401 arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(),
402 span: val.span(),
403 }),
404 Value::Glob { val, .. } => Ok(val),
405 _ => val.coerce_into_string(),
406 }
407}
408
409fn expand_glob(
415 arg: &str,
416 cwd: &Path,
417 span: Span,
418 signals: Signals,
419) -> Result<Vec<OsString>, ShellError> {
420 if !nu_glob::is_glob(arg) {
423 let path = expand_ndots_safe(expand_tilde(arg));
424 return Ok(vec![path.into()]);
425 }
426
427 let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
430 if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None, signals.clone()) {
431 let mut result: Vec<OsString> = vec![];
432
433 for m in matches {
434 signals.check(&span)?;
435 if let Ok(arg) = m {
436 let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd);
437 result.push(arg.into());
438 } else {
439 result.push(arg.into());
440 }
441 }
442
443 if result.is_empty() {
446 result.push(arg.into());
447 }
448
449 Ok(result)
450 } else {
451 Ok(vec![arg.into()])
452 }
453}
454
455fn resolve_globbed_path_to_cwd_relative(
456 path: PathBuf,
457 prefix: Option<&PathBuf>,
458 cwd: &Path,
459) -> PathBuf {
460 if let Some(prefix) = prefix {
461 if let Ok(remainder) = path.strip_prefix(prefix) {
462 let new_prefix = if let Some(pfx) = diff_paths(prefix, cwd) {
463 pfx
464 } else {
465 prefix.to_path_buf()
466 };
467 new_prefix.join(remainder)
468 } else {
469 path
470 }
471 } else {
472 path
473 }
474}
475
476fn write_pipeline_data(
484 mut engine_state: EngineState,
485 mut stack: Stack,
486 data: PipelineData,
487 mut writer: impl Write,
488) -> Result<(), ShellError> {
489 if let PipelineData::ByteStream(stream, ..) = data {
490 stream.write_to(writer)?;
491 } else if let PipelineData::Value(Value::Binary { val, .. }, ..) = data {
492 writer.write_all(&val).map_err(|err| {
493 IoError::new_internal(
494 err,
495 "Could not write pipeline data",
496 nu_protocol::location!(),
497 )
498 })?;
499 } else {
500 stack.start_collect_value();
501
502 Arc::make_mut(&mut engine_state.config).use_ansi_coloring = UseAnsiColoring::False;
504
505 let output =
507 crate::Table.run(&engine_state, &mut stack, &Call::new(Span::unknown()), data)?;
508
509 for value in output {
511 let bytes = value.coerce_into_binary()?;
512 writer.write_all(&bytes).map_err(|err| {
513 IoError::new_internal(
514 err,
515 "Could not write pipeline data",
516 nu_protocol::location!(),
517 )
518 })?;
519 }
520 }
521 Ok(())
522}
523
524pub fn command_not_found(
526 name: &str,
527 span: Span,
528 engine_state: &EngineState,
529 stack: &mut Stack,
530 cwd: &AbsolutePath,
531) -> ShellError {
532 if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found {
534 let mut stack = stack.start_collect_value();
535 let canary = "ENTERED_COMMAND_NOT_FOUND";
538 if stack.has_env_var(engine_state, canary) {
539 return ShellError::ExternalCommand {
540 label: format!(
541 "Command {name} not found while running the `command_not_found` hook"
542 ),
543 help: "Make sure the `command_not_found` hook itself does not use unknown commands"
544 .into(),
545 span,
546 };
547 }
548 stack.add_env_var(canary.into(), Value::bool(true, Span::unknown()));
549
550 let output = eval_hook(
551 &mut engine_state.clone(),
552 &mut stack,
553 None,
554 vec![("cmd_name".into(), Value::string(name, span))],
555 hook,
556 "command_not_found",
557 );
558
559 stack.remove_env_var(engine_state, canary);
561
562 match output {
563 Ok(PipelineData::Value(Value::String { val, .. }, ..)) => {
564 return ShellError::ExternalCommand {
565 label: format!("Command `{name}` not found"),
566 help: val,
567 span,
568 };
569 }
570 Err(err) => {
571 return err;
572 }
573 _ => {
574 }
576 }
577 }
578
579 if let Some(replacement) = crate::removed_commands().get(&name.to_lowercase()) {
581 return ShellError::RemovedCommand {
582 removed: name.to_lowercase(),
583 replacement: replacement.clone(),
584 span,
585 };
586 }
587
588 if let Some(module) = engine_state.which_module_has_decl(name.as_bytes(), &[]) {
590 let module = String::from_utf8_lossy(module);
591 let full_name = format!("{module} {name}");
593 if engine_state.find_decl(full_name.as_bytes(), &[]).is_some() {
594 return ShellError::ExternalCommand {
595 label: format!("Command `{name}` not found"),
596 help: format!("Did you mean `{full_name}`?"),
597 span,
598 };
599 } else {
600 return ShellError::ExternalCommand {
601 label: format!("Command `{name}` not found"),
602 help: format!(
603 "A command with that name exists in module `{module}`. Try importing it with `use`"
604 ),
605 span,
606 };
607 }
608 }
609
610 let signatures = engine_state.get_signatures_and_declids(false);
612 if let Some((sig, _)) = signatures.iter().find(|(sig, _)| {
613 sig.search_terms
614 .iter()
615 .any(|term| term.to_folded_case() == name.to_folded_case())
616 }) {
617 return ShellError::ExternalCommand {
618 label: format!("Command `{name}` not found"),
619 help: format!("Did you mean `{}`?", sig.name),
620 span,
621 };
622 }
623
624 if let Some(cmd) = did_you_mean(signatures.iter().map(|(sig, _)| &sig.name), name) {
626 if cmd == name {
629 return ShellError::ExternalCommand {
630 label: format!("Command `{name}` not found"),
631 help: "There is a built-in command with the same name".into(),
632 span,
633 };
634 }
635 return ShellError::ExternalCommand {
636 label: format!("Command `{name}` not found"),
637 help: format!("Did you mean `{cmd}`?"),
638 span,
639 };
640 }
641
642 if cwd.join(name).is_file() {
644 return ShellError::ExternalCommand {
645 label: format!("Command `{name}` not found"),
646 help: format!(
647 "`{name}` refers to a file that is not executable. Did you forget to set execute permissions?"
648 ),
649 span,
650 };
651 }
652
653 ShellError::ExternalCommand {
655 label: format!("Command `{name}` not found"),
656 help: format!("`{name}` is neither a Nushell built-in or a known external command"),
657 span,
658 }
659}
660
661pub fn which(name: impl AsRef<OsStr>, paths: &str, cwd: &Path) -> Option<PathBuf> {
671 #[cfg(windows)]
672 let paths = format!("{};{}", cwd.display(), paths);
673 which::which_in(name, Some(paths), cwd).ok()
674}
675
676fn is_cmd_internal_command(name: &str) -> bool {
679 const COMMANDS: &[&str] = &[
680 "ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
681 ];
682 COMMANDS.iter().any(|cmd| cmd.eq_ignore_ascii_case(name))
683}
684
685fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool {
687 s.as_ref()
688 .iter()
689 .any(|b| matches!(b, b'<' | b'>' | b'&' | b'|' | b'^'))
690}
691
692#[cfg_attr(not(windows), allow(dead_code))]
694fn escape_cmd_argument(arg: &Spanned<OsString>) -> Result<Cow<'_, OsStr>, ShellError> {
695 let Spanned { item: arg, span } = arg;
696 let bytes = arg.as_encoded_bytes();
697 if bytes.iter().any(|b| matches!(b, b'\r' | b'\n' | b'%')) {
698 Err(ShellError::ExternalCommand {
700 label:
701 "Arguments to CMD internal commands cannot contain new lines or percent signs '%'"
702 .into(),
703 help: "some characters currently cannot be securely escaped".into(),
704 span: *span,
705 })
706 } else if bytes.contains(&b'"') {
707 if bytes.iter().filter(|b| **b == b'"').count() == 2
710 && bytes.starts_with(b"\"")
711 && bytes.ends_with(b"\"")
712 {
713 Ok(Cow::Borrowed(arg))
714 } else {
715 Err(ShellError::ExternalCommand {
716 label: "Arguments to CMD internal commands cannot contain embedded double quotes"
717 .into(),
718 help: "this case currently cannot be securely handled".into(),
719 span: *span,
720 })
721 }
722 } else if bytes.contains(&b' ') || has_cmd_special_character(bytes) {
723 let mut new_str = OsString::new();
725 new_str.push("\"");
726 new_str.push(arg);
727 new_str.push("\"");
728 Ok(Cow::Owned(new_str))
729 } else {
730 Ok(Cow::Borrowed(arg))
732 }
733}
734
735#[cfg(test)]
736mod test {
737 use super::*;
738 use nu_test_support::{fs::Stub, playground::Playground};
739
740 #[test]
741 fn test_expand_glob() {
742 Playground::setup("test_expand_glob", |dirs, play| {
743 play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]);
744
745 let cwd = dirs.test().as_std_path();
746
747 let actual = expand_glob("*.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
748 let expected = &["a.txt", "b.txt"];
749 assert_eq!(actual, expected);
750
751 let actual = expand_glob("./*.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
752 assert_eq!(actual, expected);
753
754 let actual = expand_glob("'*.txt'", cwd, Span::unknown(), Signals::empty()).unwrap();
755 let expected = &["'*.txt'"];
756 assert_eq!(actual, expected);
757
758 let actual = expand_glob(".", cwd, Span::unknown(), Signals::empty()).unwrap();
759 let expected = &["."];
760 assert_eq!(actual, expected);
761
762 let actual = expand_glob("./a.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
763 let expected = &["./a.txt"];
764 assert_eq!(actual, expected);
765
766 let actual = expand_glob("[*.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
767 let expected = &["[*.txt"];
768 assert_eq!(actual, expected);
769
770 let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
771 let home = dirs::home_dir().expect("failed to get home dir");
772 let expected: Vec<OsString> = vec![home.join("foo.txt").into()];
773 assert_eq!(actual, expected);
774 })
775 }
776
777 #[test]
778 fn test_write_pipeline_data() {
779 let mut engine_state = EngineState::new();
780 let stack = Stack::new();
781 let cwd = std::env::current_dir()
782 .unwrap()
783 .into_os_string()
784 .into_string()
785 .unwrap();
786
787 engine_state.add_env_var("PWD".into(), Value::string(cwd, Span::test_data()));
789
790 let mut buf = vec![];
791 let input = PipelineData::Empty;
792 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
793 assert_eq!(buf, b"");
794
795 let mut buf = vec![];
796 let input = PipelineData::Value(Value::string("foo", Span::unknown()), None);
797 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
798 assert_eq!(buf, b"foo");
799
800 let mut buf = vec![];
801 let input = PipelineData::Value(Value::binary(b"foo", Span::unknown()), None);
802 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
803 assert_eq!(buf, b"foo");
804
805 let mut buf = vec![];
806 let input = PipelineData::ByteStream(
807 ByteStream::read(
808 b"foo".as_slice(),
809 Span::unknown(),
810 Signals::empty(),
811 ByteStreamType::Unknown,
812 ),
813 None,
814 );
815 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
816 assert_eq!(buf, b"foo");
817 }
818}