1use itertools::Itertools;
2use nu_cmd_base::hook::eval_hook;
3use nu_engine::{command_prelude::*, env_to_strings};
4use nu_path::{AbsolutePath, dots::expand_ndots_safe, expand_tilde};
5use nu_protocol::{
6 ByteStream, NuGlob, OutDest, Signals, UseAnsiColoring, did_you_mean,
7 process::{ChildProcess, PostWaitCallback},
8 shell_error::io::IoError,
9};
10use nu_system::{ForegroundChild, kill_by_pid, prepare_background_command};
11use nu_utils::IgnoreCaseExt;
12use pathdiff::diff_paths;
13#[cfg(windows)]
14use std::os::windows::process::CommandExt;
15use std::{
16 borrow::Cow,
17 ffi::{OsStr, OsString},
18 io::Write,
19 path::{Path, PathBuf},
20 process::Stdio,
21 sync::Arc,
22 thread,
23};
24
25#[derive(Clone)]
26pub struct External;
27
28impl Command for External {
29 fn name(&self) -> &str {
30 "run-external"
31 }
32
33 fn description(&self) -> &str {
34 "Runs external command."
35 }
36
37 fn extra_description(&self) -> &str {
38 "All externals are run with this command, whether you call it directly with `run-external external` or use `external` or `^external`.
39If you create a custom command with this name, that will be used instead."
40 }
41
42 fn signature(&self) -> nu_protocol::Signature {
43 Signature::build(self.name())
44 .input_output_types(vec![(Type::Any, Type::Any)])
45 .rest(
46 "command",
47 SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Any]),
48 "External command to run, with arguments.",
49 )
50 .category(Category::System)
51 }
52
53 fn run(
54 &self,
55 engine_state: &EngineState,
56 stack: &mut Stack,
57 call: &Call,
58 input: PipelineData,
59 ) -> Result<PipelineData, ShellError> {
60 let cwd = engine_state.cwd(Some(stack))?;
61 let rest = call.rest::<Value>(engine_state, stack, 0)?;
62 let name_args = rest.split_first().map(|(x, y)| (x, y.to_vec()));
63
64 let Some((name, mut call_args)) = name_args else {
65 return Err(ShellError::MissingParameter {
66 param_name: "no command given".into(),
67 span: call.head,
68 });
69 };
70
71 let name_str: Cow<str> = match &name {
72 Value::Glob { val, .. } => Cow::Borrowed(val),
73 Value::String { val, .. } => Cow::Borrowed(val),
74 Value::List { vals, .. } => {
75 let Some((first, args)) = vals.split_first() else {
76 return Err(ShellError::MissingParameter {
77 param_name: "external command given as list empty".into(),
78 span: call.head,
79 });
80 };
81 call_args.splice(..0, args.to_vec());
83 first.coerce_str()?
84 }
85 _ => Cow::Owned(name.clone().coerce_into_string()?),
86 };
87
88 let expanded_name = match &name {
89 Value::Glob { no_expand, .. } if !*no_expand => {
91 expand_ndots_safe(expand_tilde(&*name_str))
92 }
93 _ => Path::new(&*name_str).to_owned(),
94 };
95
96 let paths = nu_engine::env::path_str(engine_state, stack, call.head).unwrap_or_default();
97
98 let pathext_script_in_windows = if cfg!(windows) {
112 if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) {
113 let ext = executable
114 .extension()
115 .unwrap_or_default()
116 .to_string_lossy()
117 .to_uppercase();
118
119 !["COM", "EXE", "BAT", "CMD", "PS1"]
120 .iter()
121 .any(|c| *c == ext)
122 } else {
123 false
124 }
125 } else {
126 false
127 };
128
129 let (potential_powershell_script, path_to_ps1_executable) = if cfg!(windows) {
131 if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) {
132 let ext = executable
133 .extension()
134 .unwrap_or_default()
135 .to_string_lossy()
136 .to_uppercase();
137 (ext == "PS1", Some(executable))
138 } else {
139 (false, None)
140 }
141 } else {
142 (false, None)
143 };
144
145 let executable = if cfg!(windows)
149 && (is_cmd_internal_command(&name_str) || pathext_script_in_windows)
150 {
151 PathBuf::from("cmd.exe")
152 } else if cfg!(windows) && potential_powershell_script && path_to_ps1_executable.is_some() {
153 PathBuf::from("powershell.exe")
157 } else {
158 let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
161 return Err(command_not_found(
162 &name_str,
163 call.head,
164 engine_state,
165 stack,
166 &cwd,
167 ));
168 };
169 executable
170 };
171
172 let mut command = std::process::Command::new(&executable);
174
175 command.current_dir(cwd);
177
178 let envs = env_to_strings(engine_state, stack)?;
180 command.env_clear();
181 command.envs(envs);
182
183 let args = eval_external_arguments(engine_state, stack, call_args)?;
185 #[cfg(windows)]
186 if is_cmd_internal_command(&name_str) || pathext_script_in_windows {
187 command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
191 for arg in &args {
192 command.raw_arg(escape_cmd_argument(arg)?);
193 }
194 } else if potential_powershell_script {
195 command.args([
196 "-File",
197 &path_to_ps1_executable.unwrap_or_default().to_string_lossy(),
198 ]);
199 command.args(args.into_iter().map(|s| s.item));
200 } else {
201 command.args(args.into_iter().map(|s| s.item));
202 }
203 #[cfg(not(windows))]
204 command.args(args.into_iter().map(|s| s.item));
205
206 let stdout = stack.stdout();
216 let stderr = stack.stderr();
217 let merged_stream = if matches!(stdout, OutDest::Pipe) && matches!(stderr, OutDest::Pipe) {
218 let (reader, writer) =
219 os_pipe::pipe().map_err(|err| IoError::new(err, call.head, None))?;
220 command.stdout(
221 writer
222 .try_clone()
223 .map_err(|err| IoError::new(err, call.head, None))?,
224 );
225 command.stderr(writer);
226 Some(reader)
227 } else {
228 if engine_state.is_background_job()
229 && matches!(stdout, OutDest::Inherit | OutDest::Print)
230 {
231 command.stdout(Stdio::null());
232 } else {
233 command.stdout(
234 Stdio::try_from(stdout).map_err(|err| IoError::new(err, call.head, None))?,
235 );
236 }
237
238 if engine_state.is_background_job()
239 && matches!(stderr, OutDest::Inherit | OutDest::Print)
240 {
241 command.stderr(Stdio::null());
242 } else {
243 command.stderr(
244 Stdio::try_from(stderr).map_err(|err| IoError::new(err, call.head, None))?,
245 );
246 }
247
248 None
249 };
250
251 let data_to_copy_into_stdin = match input {
255 PipelineData::ByteStream(stream, metadata) => match stream.into_stdio() {
256 Ok(stdin) => {
257 command.stdin(stdin);
258 None
259 }
260 Err(stream) => {
261 command.stdin(Stdio::piped());
262 Some(PipelineData::byte_stream(stream, metadata))
263 }
264 },
265 PipelineData::Empty => {
266 if engine_state.is_mcp || stack.suppress_stdin {
268 command.stdin(Stdio::null());
269 } else {
270 command.stdin(Stdio::inherit());
271 }
272 None
273 }
274 value => {
275 command.stdin(Stdio::piped());
276 Some(value)
277 }
278 };
279
280 if engine_state.is_mcp || stack.suppress_stdin {
283 prepare_background_command(&mut command);
284 }
285
286 log::trace!("run-external spawning: {command:?}");
288
289 #[cfg(windows)]
292 let child = ForegroundChild::spawn(command);
293 #[cfg(unix)]
294 let child = ForegroundChild::spawn(
295 command,
296 engine_state.is_interactive && !stack.suppress_stdin,
299 engine_state.is_background_job(),
300 &engine_state.pipeline_externals_state,
301 );
302
303 let mut child = child.map_err(|err| {
304 let context = format!("Could not spawn foreground child: {err}");
305 IoError::new_internal(err, context)
306 })?;
307
308 if let Some(thread_job) = engine_state.current_thread_job()
309 && !thread_job.try_add_pid(child.pid())
310 {
311 kill_by_pid(child.pid().into()).map_err(|err| {
312 ShellError::Io(IoError::new_internal(
313 err,
314 "Could not spawn external stdin worker",
315 ))
316 })?;
317 }
318
319 if let Some(data) = data_to_copy_into_stdin {
321 let stdin = child.as_mut().stdin.take().expect("stdin is piped");
322 let engine_state = engine_state.clone();
323 let stack = stack.clone();
324 thread::Builder::new()
325 .name("external stdin worker".into())
326 .spawn(move || {
327 let _ = write_pipeline_data(engine_state, stack, data, stdin);
328 })
329 .map_err(|err| {
330 IoError::new_with_additional_context(
331 err,
332 call.head,
333 None,
334 "Could not spawn external stdin worker",
335 )
336 })?;
337 }
338
339 let child_pid = child.pid();
340
341 let child = ChildProcess::new(
343 child,
344 merged_stream,
345 matches!(stderr, OutDest::Pipe),
346 call.head,
347 Some(PostWaitCallback::for_job_control(
348 engine_state,
349 Some(child_pid),
350 executable
351 .as_path()
352 .file_name()
353 .and_then(|it| it.to_str())
354 .map(|it| it.to_string()),
355 )),
356 )?;
357
358 Ok(PipelineData::byte_stream(
359 ByteStream::child(child, call.head),
360 None,
361 ))
362 }
363
364 fn examples(&self) -> Vec<Example<'_>> {
365 vec![
366 Example {
367 description: "Run an external command",
368 example: r#"run-external "echo" "-n" "hello""#,
369 result: None,
370 },
371 Example {
372 description: "Redirect stdout from an external command into the pipeline",
373 example: r#"run-external "echo" "-n" "hello" | split chars"#,
374 result: None,
375 },
376 Example {
377 description: "Redirect stderr from an external command into the pipeline",
378 example: r#"run-external "nu" "-c" "print -e hello" e>| split chars"#,
379 result: None,
380 },
381 ]
382 }
383}
384
385pub fn eval_external_arguments(
387 engine_state: &EngineState,
388 stack: &mut Stack,
389 call_args: Vec<Value>,
390) -> Result<Vec<Spanned<OsString>>, ShellError> {
391 let cwd = engine_state.cwd(Some(stack))?;
392 let mut args: Vec<Spanned<OsString>> = Vec::with_capacity(call_args.len());
393
394 for arg in call_args {
395 let span = arg.span();
396 match arg {
397 Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
399 expand_glob(
400 &val,
401 cwd.as_std_path(),
402 span,
403 engine_state.signals().clone(),
404 )?
405 .into_iter()
406 .map(|s| s.into_spanned(span)),
407 ),
408 other => args
409 .push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)),
410 }
411 }
412 Ok(args)
413}
414
415fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result<String, ShellError> {
418 match val {
419 Value::List { .. } => Err(ShellError::CannotPassListToExternal {
420 arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(),
421 span: val.span(),
422 }),
423 Value::Glob { val, .. } => Ok(val),
424 _ => val.coerce_into_string(),
425 }
426}
427
428fn expand_glob(
434 arg: &str,
435 cwd: &Path,
436 span: Span,
437 signals: Signals,
438) -> Result<Vec<OsString>, ShellError> {
439 if !nu_glob::is_glob_with_backend(arg) {
442 let path = expand_ndots_safe(expand_tilde(arg));
443 return Ok(vec![path.into()]);
444 }
445
446 let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
449 if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None, signals.clone()) {
450 let mut result: Vec<OsString> = vec![];
451
452 for m in matches {
453 signals.check(&span)?;
454 if let Ok(arg) = m {
455 let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd);
456 result.push(arg.into());
457 } else {
458 result.push(arg.into());
459 }
460 }
461
462 if result.is_empty() {
465 result.push(arg.into());
466 }
467
468 Ok(result)
469 } else {
470 Ok(vec![arg.into()])
471 }
472}
473
474fn resolve_globbed_path_to_cwd_relative(
475 path: PathBuf,
476 prefix: Option<&PathBuf>,
477 cwd: &Path,
478) -> PathBuf {
479 if let Some(prefix) = prefix {
480 if let Ok(remainder) = path.strip_prefix(prefix) {
481 let new_prefix = if let Some(pfx) = diff_paths(prefix, cwd) {
482 pfx
483 } else {
484 prefix.to_path_buf()
485 };
486 new_prefix.join(remainder)
487 } else {
488 path
489 }
490 } else {
491 path
492 }
493}
494
495fn write_pipeline_data(
503 mut engine_state: EngineState,
504 mut stack: Stack,
505 data: PipelineData,
506 mut writer: impl Write,
507) -> Result<(), ShellError> {
508 if let PipelineData::ByteStream(stream, ..) = data {
509 stream.write_to(writer)?;
510 } else if let PipelineData::Value(Value::Binary { val, .. }, ..) = data {
511 writer
512 .write_all(&val)
513 .map_err(|err| IoError::new_internal(err, "Could not write pipeline data"))?;
514 } else {
515 stack.start_collect_value();
516
517 Arc::make_mut(&mut engine_state.config).use_ansi_coloring = UseAnsiColoring::False;
519
520 let output =
522 crate::Table.run(&engine_state, &mut stack, &Call::new(Span::unknown()), data)?;
523
524 for value in output {
526 let bytes = value.coerce_into_binary()?;
527 writer
528 .write_all(&bytes)
529 .map_err(|err| IoError::new_internal(err, "Could not write pipeline data"))?;
530 }
531 }
532 Ok(())
533}
534
535pub fn command_not_found(
537 name: &str,
538 span: Span,
539 engine_state: &EngineState,
540 stack: &mut Stack,
541 cwd: &AbsolutePath,
542) -> ShellError {
543 if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found {
545 let mut stack = stack.start_collect_value();
546 let canary = "ENTERED_COMMAND_NOT_FOUND";
549 if stack.has_env_var(engine_state, canary) {
550 return ShellError::ExternalCommand {
551 label: format!(
552 "Command {name} not found while running the `command_not_found` hook"
553 ),
554 help: "Make sure the `command_not_found` hook itself does not use unknown commands"
555 .into(),
556 span,
557 };
558 }
559 stack.add_env_var(canary.into(), Value::bool(true, Span::unknown()));
560
561 let output = eval_hook(
562 &mut engine_state.clone(),
563 &mut stack,
564 None,
565 vec![("cmd_name".into(), Value::string(name, span))],
566 hook,
567 "command_not_found",
568 );
569
570 stack.remove_env_var(engine_state, canary);
572
573 match output {
574 Ok(PipelineData::Value(Value::String { val, .. }, ..)) => {
575 return ShellError::ExternalCommand {
576 label: format!("Command `{name}` not found"),
577 help: val,
578 span,
579 };
580 }
581 Err(err) => {
582 return err;
583 }
584 _ => {
585 }
587 }
588 }
589
590 if let Some(replacement) = crate::removed_commands().get(&name.to_lowercase()) {
592 return ShellError::RemovedCommand {
593 removed: name.to_lowercase(),
594 replacement: replacement.clone(),
595 span,
596 };
597 }
598
599 let help = (|| {
601 if let Some(module) = engine_state.which_module_has_decl(name.as_bytes(), &[]) {
605 let module = String::from_utf8_lossy(module);
606
607 let full_name = format!("{module} {name}");
609 if engine_state.find_decl(full_name.as_bytes(), &[]).is_some() {
610 return format!("Did you mean `{full_name}`?");
611 }
612
613 return format!(
614 "A command with that name exists in module `{module}`. Try importing it with `use`"
615 );
616 }
617
618 let signatures = engine_state.get_signatures_and_declids(false);
620 if let Some((last, others)) = signatures
621 .iter()
622 .map(|(sig, _)| sig)
623 .filter(|sig| {
624 let name = name.to_folded_case(); sig.name
626 .to_folded_case()
627 .split_ascii_whitespace() .contains(name.as_str()) || sig
630 .search_terms
631 .iter()
632 .any(|term| term.to_folded_case() == name)
633 })
634 .map(|sig| format!("`{}`", sig.name))
635 .collect::<Vec<_>>()
636 .split_last()
637 {
638 let commands = if others.is_empty() {
639 last
640 } else {
641 &format!("{} or {last}", others.join(", "))
644 };
645
646 return format!("Did you mean {commands}?");
647 }
648
649 if let Some(cmd) = did_you_mean(signatures.iter().map(|(sig, _)| &sig.name), name) {
651 if cmd == name {
654 return "There is a built-in command with the same name".to_string();
655 }
656
657 return format!("Did you mean `{cmd}`?");
658 }
659
660 if cwd.join(name).is_file() {
662 return format!(
663 "`{name}` refers to a file that is not executable. Did you forget to set execute permissions?"
664 );
665 }
666
667 format!("`{name}` is neither a Nushell built-in or a known external command")
669 })();
670
671 ShellError::ExternalCommand {
672 label: format!("Command `{name}` not found"),
673 help,
674 span,
675 }
676}
677
678pub fn which(name: impl AsRef<OsStr>, paths: &str, cwd: &Path) -> Option<PathBuf> {
688 #[cfg(windows)]
689 let paths = format!("{};{}", cwd.display(), paths);
690 which::which_in(name, Some(paths), cwd).ok()
691}
692
693fn is_cmd_internal_command(name: &str) -> bool {
696 const COMMANDS: &[&str] = &[
697 "ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
698 ];
699 COMMANDS.iter().any(|cmd| cmd.eq_ignore_ascii_case(name))
700}
701
702fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool {
704 s.as_ref()
705 .iter()
706 .any(|b| matches!(b, b'<' | b'>' | b'&' | b'|' | b'^'))
707}
708
709#[cfg_attr(not(windows), allow(dead_code))]
711fn escape_cmd_argument(arg: &Spanned<OsString>) -> Result<Cow<'_, OsStr>, ShellError> {
712 let Spanned { item: arg, span } = arg;
713 let bytes = arg.as_encoded_bytes();
714 if bytes.iter().any(|b| matches!(b, b'\r' | b'\n' | b'%')) {
715 Err(ShellError::ExternalCommand {
717 label:
718 "Arguments to CMD internal commands cannot contain new lines or percent signs '%'"
719 .into(),
720 help: "some characters currently cannot be securely escaped".into(),
721 span: *span,
722 })
723 } else if bytes.contains(&b'"') {
724 if bytes.iter().filter(|b| **b == b'"').count() == 2
727 && bytes.starts_with(b"\"")
728 && bytes.ends_with(b"\"")
729 {
730 Ok(Cow::Borrowed(arg))
731 } else {
732 Err(ShellError::ExternalCommand {
733 label: "Arguments to CMD internal commands cannot contain embedded double quotes"
734 .into(),
735 help: "this case currently cannot be securely handled".into(),
736 span: *span,
737 })
738 }
739 } else if bytes.contains(&b' ') || has_cmd_special_character(bytes) {
740 let mut new_str = OsString::new();
742 new_str.push("\"");
743 new_str.push(arg);
744 new_str.push("\"");
745 Ok(Cow::Owned(new_str))
746 } else {
747 Ok(Cow::Borrowed(arg))
749 }
750}
751
752#[cfg(test)]
753mod test {
754 use super::*;
755 use nu_test_support::{fs::Stub, playground::Playground};
756
757 #[test]
758 fn test_expand_glob() {
759 Playground::setup("test_expand_glob", |dirs, play| {
760 play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]);
761
762 let cwd = dirs.test().as_std_path();
763
764 let actual = expand_glob("*.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
765 let expected = &["a.txt", "b.txt"];
766 assert_eq!(actual, expected);
767
768 let actual = expand_glob("./*.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
769 assert_eq!(actual, expected);
770
771 let actual = expand_glob("'*.txt'", cwd, Span::test_data(), Signals::empty()).unwrap();
772 let expected = &["'*.txt'"];
773 assert_eq!(actual, expected);
774
775 let actual = expand_glob(".", cwd, Span::test_data(), Signals::empty()).unwrap();
776 let expected = &["."];
777 assert_eq!(actual, expected);
778
779 let actual = expand_glob("./a.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
780 let expected = &["./a.txt"];
781 assert_eq!(actual, expected);
782
783 let actual = expand_glob("[*.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
784 let expected = &["[*.txt"];
785 assert_eq!(actual, expected);
786
787 let actual =
788 expand_glob("~/foo.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
789 let home = dirs::home_dir().expect("failed to get home dir");
790 let expected: Vec<OsString> = vec![home.join("foo.txt").into()];
791 assert_eq!(actual, expected);
792 })
793 }
794
795 #[test]
796 fn test_write_pipeline_data() {
797 let mut engine_state = EngineState::new();
798 let stack = Stack::new();
799 let cwd = std::env::current_dir()
800 .unwrap()
801 .into_os_string()
802 .into_string()
803 .unwrap();
804
805 engine_state.add_env_var("PWD".into(), Value::string(cwd, Span::test_data()));
807
808 let mut buf = vec![];
809 let input = PipelineData::empty();
810 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
811 assert_eq!(buf, b"");
812
813 let mut buf = vec![];
814 let input = PipelineData::value(Value::string("foo", Span::test_data()), None);
815 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
816 assert_eq!(buf, b"foo");
817
818 let mut buf = vec![];
819 let input = PipelineData::value(Value::binary(b"foo", Span::test_data()), None);
820 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
821 assert_eq!(buf, b"foo");
822
823 let mut buf = vec![];
824 let input = PipelineData::byte_stream(
825 ByteStream::read(
826 b"foo".as_slice(),
827 Span::test_data(),
828 Signals::empty(),
829 ByteStreamType::Unknown,
830 ),
831 None,
832 );
833 write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
834 assert_eq!(buf, b"foo");
835 }
836}
837
838#[cfg(test)]
841mod background_isolation_tests {
842 use nu_system::prepare_background_command;
843 use std::process::{Command, Stdio};
844
845 #[cfg(unix)]
846 fn assert_child_has_no_tty(stdin: Stdio) {
847 let mut cmd = Command::new("sh");
848 cmd.args([
850 "-c",
851 "(exec 3>/dev/tty) 2>/dev/null && echo has_tty || echo no_tty",
852 ])
853 .stdin(stdin)
854 .stdout(Stdio::piped())
855 .stderr(Stdio::null());
856 prepare_background_command(&mut cmd);
857
858 let output = cmd.output().expect("sh should run");
859 assert_eq!(
860 String::from_utf8_lossy(&output.stdout).trim(),
861 "no_tty",
862 "child must not retain a controlling terminal after setsid"
863 );
864 }
865
866 #[cfg(unix)]
867 #[test]
868 fn setsid_removes_controlling_terminal() {
869 assert_child_has_no_tty(Stdio::null());
870 }
871
872 #[cfg(unix)]
873 #[test]
874 fn setsid_removes_controlling_terminal_with_piped_stdin() {
875 assert_child_has_no_tty(Stdio::piped());
876 }
877
878 #[cfg(windows)]
879 #[test]
880 fn create_no_window_has_no_console_window() {
881 let mut cmd = Command::new("powershell.exe");
889 cmd.args([
890 "-NoProfile",
891 "-NonInteractive",
892 "-Command",
893 concat!(
894 "Add-Type -Namespace NuBg -Name Native -MemberDefinition '",
895 "[DllImport(\"kernel32.dll\")] public static extern System.IntPtr GetConsoleWindow();",
896 "'; ",
897 "if ([NuBg.Native]::GetConsoleWindow() -eq [System.IntPtr]::Zero) { ",
898 "[Console]::Out.Write('no_console') ",
899 "} else { ",
900 "[Console]::Out.Write('has_console') ",
901 "}",
902 ),
903 ])
904 .stdin(Stdio::null())
905 .stdout(Stdio::piped())
906 .stderr(Stdio::piped());
907 prepare_background_command(&mut cmd);
908
909 let output = cmd.output().expect("powershell should run");
910 let stdout = String::from_utf8_lossy(&output.stdout);
911 let stderr = String::from_utf8_lossy(&output.stderr);
912 let token = stdout
913 .split_whitespace()
914 .find(|t| *t == "no_console" || *t == "has_console")
915 .unwrap_or("");
916 assert_eq!(
917 token, "no_console",
918 "child must have no console window under CREATE_NO_WINDOW; stdout={stdout:?} stderr={stderr}"
919 );
920 }
921}