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