Skip to main content

nu_command/system/
run_external.rs

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        "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                // Prepend elements in command list to the list of arguments except the first
81                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            // Expand tilde and ndots on the name if it's a bare string / glob (#13000)
89            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        // On Windows, the user could have run the cmd.exe built-in commands "assoc"
98        // and "ftype" to create a file association for an arbitrary file extension.
99        // They then could have added that extension to the PATHEXT environment variable.
100        // For example, a nushell script with extension ".nu" can be set up with
101        // "assoc .nu=nuscript" and "ftype nuscript=C:\path\to\nu.exe '%1' %*",
102        // and then by adding ".NU" to PATHEXT. In this case we use the which command,
103        // which will find the executable with or without the extension. If "which"
104        // returns true, that means that we've found the script and we believe the
105        // user wants to use the windows association to run the script. The only
106        // easy way to do this is to run cmd.exe with the script as an argument.
107        // File extensions of .COM, .EXE, .BAT, and .CMD are ignored because Windows
108        // can run those files directly. PS1 files are also ignored and that
109        // extension is handled in a separate block below.
110        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's make sure it's a .ps1 script, but only on Windows
129        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        // Find the absolute path to the executable. On Windows, set the
145        // executable to "cmd.exe" if it's a CMD internal command. If the
146        // command is not found, display a helpful error message.
147        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            // If we're on Windows and we're trying to run a PowerShell script, we'll use
153            // `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
154            // it's automatically installed on all modern windows systems.
155            PathBuf::from("powershell.exe")
156        } else {
157            // Determine the PATH to be used and then use `which` to find it - though this has no
158            // effect if it's an absolute path already
159            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        // Create the command.
172        let mut command = std::process::Command::new(&executable);
173
174        // Configure PWD.
175        command.current_dir(cwd);
176
177        // Configure environment variables.
178        let envs = env_to_strings(engine_state, stack)?;
179        command.env_clear();
180        command.envs(envs);
181
182        // Configure args.
183        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            // The /D flag disables execution of AutoRun commands from registry.
187            // The /C flag followed by a command name instructs CMD to execute
188            // that command and quit.
189            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        // Configure stdout and stderr. If both are set to `OutDest::Pipe`,
206        // we'll set up a pipe that merges two streams into one.
207        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        // Configure stdin. We'll try connecting input to the child process
244        // directly. If that's not possible, we'll set up a pipe and spawn a
245        // thread to copy data into the child process.
246        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                // MCP servers run non-interactively - use null stdin to prevent commands
259                // from hanging when they prompt for passwords or other input.
260                // In the future, this may become a more general option (e.g., no_stdin)
261                // but needs more testing first. See:
262                // https://github.com/nushell/nushell/pull/17161#discussion_r2761243143
263                if engine_state.is_mcp {
264                    command.stdin(Stdio::null());
265                } else {
266                    command.stdin(Stdio::inherit());
267                }
268                None
269            }
270            value => {
271                command.stdin(Stdio::piped());
272                Some(value)
273            }
274        };
275
276        // Log the command we're about to run in case it's useful for debugging purposes.
277        log::trace!("run-external spawning: {command:?}");
278
279        // Spawn the child process. On Unix, also put the child process to
280        // foreground if we're in an interactive session.
281        #[cfg(windows)]
282        let child = ForegroundChild::spawn(command);
283        #[cfg(unix)]
284        let child = ForegroundChild::spawn(
285            command,
286            engine_state.is_interactive,
287            engine_state.is_background_job(),
288            &engine_state.pipeline_externals_state,
289        );
290
291        let mut child = child.map_err(|err| {
292            let context = format!("Could not spawn foreground child: {err}");
293            IoError::new_internal(err, context)
294        })?;
295
296        if let Some(thread_job) = engine_state.current_thread_job()
297            && !thread_job.try_add_pid(child.pid())
298        {
299            kill_by_pid(child.pid().into()).map_err(|err| {
300                ShellError::Io(IoError::new_internal(
301                    err,
302                    "Could not spawn external stdin worker",
303                ))
304            })?;
305        }
306
307        // If we need to copy data into the child process, do it now.
308        if let Some(data) = data_to_copy_into_stdin {
309            let stdin = child.as_mut().stdin.take().expect("stdin is piped");
310            let engine_state = engine_state.clone();
311            let stack = stack.clone();
312            thread::Builder::new()
313                .name("external stdin worker".into())
314                .spawn(move || {
315                    let _ = write_pipeline_data(engine_state, stack, data, stdin);
316                })
317                .map_err(|err| {
318                    IoError::new_with_additional_context(
319                        err,
320                        call.head,
321                        None,
322                        "Could not spawn external stdin worker",
323                    )
324                })?;
325        }
326
327        let child_pid = child.pid();
328
329        // Wrap the output into a `PipelineData::byte_stream`.
330        let child = ChildProcess::new(
331            child,
332            merged_stream,
333            matches!(stderr, OutDest::Pipe),
334            call.head,
335            Some(PostWaitCallback::for_job_control(
336                engine_state,
337                Some(child_pid),
338                executable
339                    .as_path()
340                    .file_name()
341                    .and_then(|it| it.to_str())
342                    .map(|it| it.to_string()),
343            )),
344        )?;
345
346        Ok(PipelineData::byte_stream(
347            ByteStream::child(child, call.head),
348            None,
349        ))
350    }
351
352    fn examples(&self) -> Vec<Example<'_>> {
353        vec![
354            Example {
355                description: "Run an external command",
356                example: r#"run-external "echo" "-n" "hello""#,
357                result: None,
358            },
359            Example {
360                description: "Redirect stdout from an external command into the pipeline",
361                example: r#"run-external "echo" "-n" "hello" | split chars"#,
362                result: None,
363            },
364            Example {
365                description: "Redirect stderr from an external command into the pipeline",
366                example: r#"run-external "nu" "-c" "print -e hello" e>| split chars"#,
367                result: None,
368            },
369        ]
370    }
371}
372
373/// Evaluate all arguments, performing expansions when necessary.
374pub fn eval_external_arguments(
375    engine_state: &EngineState,
376    stack: &mut Stack,
377    call_args: Vec<Value>,
378) -> Result<Vec<Spanned<OsString>>, ShellError> {
379    let cwd = engine_state.cwd(Some(stack))?;
380    let mut args: Vec<Spanned<OsString>> = Vec::with_capacity(call_args.len());
381
382    for arg in call_args {
383        let span = arg.span();
384        match arg {
385            // Expand globs passed to run-external
386            Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
387                expand_glob(
388                    &val,
389                    cwd.as_std_path(),
390                    span,
391                    engine_state.signals().clone(),
392                )?
393                .into_iter()
394                .map(|s| s.into_spanned(span)),
395            ),
396            other => args
397                .push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)),
398        }
399    }
400    Ok(args)
401}
402
403/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external`
404/// as well
405fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result<String, ShellError> {
406    match val {
407        Value::List { .. } => Err(ShellError::CannotPassListToExternal {
408            arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(),
409            span: val.span(),
410        }),
411        Value::Glob { val, .. } => Ok(val),
412        _ => val.coerce_into_string(),
413    }
414}
415
416/// Performs glob expansion on `arg`. If the expansion found no matches or the pattern
417/// is not a valid glob, then this returns the original string as the expansion result.
418///
419/// Note: This matches the default behavior of Bash, but is known to be
420/// error-prone. We might want to change this behavior in the future.
421fn expand_glob(
422    arg: &str,
423    cwd: &Path,
424    span: Span,
425    signals: Signals,
426) -> Result<Vec<OsString>, ShellError> {
427    // For an argument that isn't a glob, just do the `expand_tilde`
428    // and `expand_ndots` expansion
429    if !nu_glob::is_glob(arg) {
430        let path = expand_ndots_safe(expand_tilde(arg));
431        return Ok(vec![path.into()]);
432    }
433
434    // We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct
435    // dir
436    let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
437    if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None, signals.clone()) {
438        let mut result: Vec<OsString> = vec![];
439
440        for m in matches {
441            signals.check(&span)?;
442            if let Ok(arg) = m {
443                let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd);
444                result.push(arg.into());
445            } else {
446                result.push(arg.into());
447            }
448        }
449
450        // FIXME: do we want to special-case this further? We might accidentally expand when they don't
451        // intend to
452        if result.is_empty() {
453            result.push(arg.into());
454        }
455
456        Ok(result)
457    } else {
458        Ok(vec![arg.into()])
459    }
460}
461
462fn resolve_globbed_path_to_cwd_relative(
463    path: PathBuf,
464    prefix: Option<&PathBuf>,
465    cwd: &Path,
466) -> PathBuf {
467    if let Some(prefix) = prefix {
468        if let Ok(remainder) = path.strip_prefix(prefix) {
469            let new_prefix = if let Some(pfx) = diff_paths(prefix, cwd) {
470                pfx
471            } else {
472                prefix.to_path_buf()
473            };
474            new_prefix.join(remainder)
475        } else {
476            path
477        }
478    } else {
479        path
480    }
481}
482
483/// Write `PipelineData` into `writer`. If `PipelineData` is not binary, it is
484/// first rendered using the `table` command.
485///
486/// Note: Avoid using this function when piping data from an external command to
487/// another external command, because it copies data unnecessarily. Instead,
488/// extract the pipe from the `PipelineData::byte_stream` of the first command
489/// and hand it to the second command directly.
490fn write_pipeline_data(
491    mut engine_state: EngineState,
492    mut stack: Stack,
493    data: PipelineData,
494    mut writer: impl Write,
495) -> Result<(), ShellError> {
496    if let PipelineData::ByteStream(stream, ..) = data {
497        stream.write_to(writer)?;
498    } else if let PipelineData::Value(Value::Binary { val, .. }, ..) = data {
499        writer
500            .write_all(&val)
501            .map_err(|err| IoError::new_internal(err, "Could not write pipeline data"))?;
502    } else {
503        stack.start_collect_value();
504
505        // Turn off color as we pass data through
506        Arc::make_mut(&mut engine_state.config).use_ansi_coloring = UseAnsiColoring::False;
507
508        // Invoke the `table` command.
509        let output =
510            crate::Table.run(&engine_state, &mut stack, &Call::new(Span::unknown()), data)?;
511
512        // Write the output.
513        for value in output {
514            let bytes = value.coerce_into_binary()?;
515            writer
516                .write_all(&bytes)
517                .map_err(|err| IoError::new_internal(err, "Could not write pipeline data"))?;
518        }
519    }
520    Ok(())
521}
522
523/// Returns a helpful error message given an invalid command name,
524pub fn command_not_found(
525    name: &str,
526    span: Span,
527    engine_state: &EngineState,
528    stack: &mut Stack,
529    cwd: &AbsolutePath,
530) -> ShellError {
531    // Run the `command_not_found` hook if there is one.
532    if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found {
533        let mut stack = stack.start_collect_value();
534        // Set a special environment variable to avoid infinite loops when the
535        // `command_not_found` hook triggers itself.
536        let canary = "ENTERED_COMMAND_NOT_FOUND";
537        if stack.has_env_var(engine_state, canary) {
538            return ShellError::ExternalCommand {
539                label: format!(
540                    "Command {name} not found while running the `command_not_found` hook"
541                ),
542                help: "Make sure the `command_not_found` hook itself does not use unknown commands"
543                    .into(),
544                span,
545            };
546        }
547        stack.add_env_var(canary.into(), Value::bool(true, Span::unknown()));
548
549        let output = eval_hook(
550            &mut engine_state.clone(),
551            &mut stack,
552            None,
553            vec![("cmd_name".into(), Value::string(name, span))],
554            hook,
555            "command_not_found",
556        );
557
558        // Remove the special environment variable that we just set.
559        stack.remove_env_var(engine_state, canary);
560
561        match output {
562            Ok(PipelineData::Value(Value::String { val, .. }, ..)) => {
563                return ShellError::ExternalCommand {
564                    label: format!("Command `{name}` not found"),
565                    help: val,
566                    span,
567                };
568            }
569            Err(err) => {
570                return err;
571            }
572            _ => {
573                // The hook did not return a string, so ignore it.
574            }
575        }
576    }
577
578    // If the name is one of the removed commands, recommend a replacement.
579    if let Some(replacement) = crate::removed_commands().get(&name.to_lowercase()) {
580        return ShellError::RemovedCommand {
581            removed: name.to_lowercase(),
582            replacement: replacement.clone(),
583            span,
584        };
585    }
586
587    // The command might be from another module. Try to find it.
588    if let Some(module) = engine_state.which_module_has_decl(name.as_bytes(), &[]) {
589        let module = String::from_utf8_lossy(module);
590        // Is the command already imported?
591        let full_name = format!("{module} {name}");
592        if engine_state.find_decl(full_name.as_bytes(), &[]).is_some() {
593            return ShellError::ExternalCommand {
594                label: format!("Command `{name}` not found"),
595                help: format!("Did you mean `{full_name}`?"),
596                span,
597            };
598        } else {
599            return ShellError::ExternalCommand {
600                label: format!("Command `{name}` not found"),
601                help: format!(
602                    "A command with that name exists in module `{module}`. Try importing it with `use`"
603                ),
604                span,
605            };
606        }
607    }
608
609    // Try to match the name with the search terms of existing commands.
610    let signatures = engine_state.get_signatures_and_declids(false);
611    if let Some((sig, _)) = signatures.iter().find(|(sig, _)| {
612        sig.search_terms
613            .iter()
614            .any(|term| term.to_folded_case() == name.to_folded_case())
615    }) {
616        return ShellError::ExternalCommand {
617            label: format!("Command `{name}` not found"),
618            help: format!("Did you mean `{}`?", sig.name),
619            span,
620        };
621    }
622
623    // Try a fuzzy search on the names of all existing commands.
624    if let Some(cmd) = did_you_mean(signatures.iter().map(|(sig, _)| &sig.name), name) {
625        // The user is invoking an external command with the same name as a
626        // built-in command. Remind them of this.
627        if cmd == name {
628            return ShellError::ExternalCommand {
629                label: format!("Command `{name}` not found"),
630                help: "There is a built-in command with the same name".into(),
631                span,
632            };
633        }
634        return ShellError::ExternalCommand {
635            label: format!("Command `{name}` not found"),
636            help: format!("Did you mean `{cmd}`?"),
637            span,
638        };
639    }
640
641    // If we find a file, it's likely that the user forgot to set permissions
642    if cwd.join(name).is_file() {
643        return ShellError::ExternalCommand {
644            label: format!("Command `{name}` not found"),
645            help: format!(
646                "`{name}` refers to a file that is not executable. Did you forget to set execute permissions?"
647            ),
648            span,
649        };
650    }
651
652    // We found nothing useful. Give up and return a generic error message.
653    ShellError::ExternalCommand {
654        label: format!("Command `{name}` not found"),
655        help: format!("`{name}` is neither a Nushell built-in or a known external command"),
656        span,
657    }
658}
659
660/// Searches for the absolute path of an executable by name. `.bat` and `.cmd`
661/// files are recognized as executables on Windows.
662///
663/// This is a wrapper around `which::which_in()` except that, on Windows, it
664/// also searches the current directory before any PATH entries.
665///
666/// Note: the `which.rs` crate always uses PATHEXT from the environment. As
667/// such, changing PATHEXT within Nushell doesn't work without updating the
668/// actual environment of the Nushell process.
669pub fn which(name: impl AsRef<OsStr>, paths: &str, cwd: &Path) -> Option<PathBuf> {
670    #[cfg(windows)]
671    let paths = format!("{};{}", cwd.display(), paths);
672    which::which_in(name, Some(paths), cwd).ok()
673}
674
675/// Returns true if `name` is a (somewhat useful) CMD internal command. The full
676/// list can be found at <https://ss64.com/nt/syntax-internal.html>
677fn is_cmd_internal_command(name: &str) -> bool {
678    const COMMANDS: &[&str] = &[
679        "ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
680    ];
681    COMMANDS.iter().any(|cmd| cmd.eq_ignore_ascii_case(name))
682}
683
684/// Returns true if a string contains CMD special characters.
685fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool {
686    s.as_ref()
687        .iter()
688        .any(|b| matches!(b, b'<' | b'>' | b'&' | b'|' | b'^'))
689}
690
691/// Escape an argument for CMD internal commands. The result can be safely passed to `raw_arg()`.
692#[cfg_attr(not(windows), allow(dead_code))]
693fn escape_cmd_argument(arg: &Spanned<OsString>) -> Result<Cow<'_, OsStr>, ShellError> {
694    let Spanned { item: arg, span } = arg;
695    let bytes = arg.as_encoded_bytes();
696    if bytes.iter().any(|b| matches!(b, b'\r' | b'\n' | b'%')) {
697        // \r and \n truncate the rest of the arguments and % can expand environment variables
698        Err(ShellError::ExternalCommand {
699            label:
700                "Arguments to CMD internal commands cannot contain new lines or percent signs '%'"
701                    .into(),
702            help: "some characters currently cannot be securely escaped".into(),
703            span: *span,
704        })
705    } else if bytes.contains(&b'"') {
706        // If `arg` is already quoted by double quotes, confirm there's no
707        // embedded double quotes, then leave it as is.
708        if bytes.iter().filter(|b| **b == b'"').count() == 2
709            && bytes.starts_with(b"\"")
710            && bytes.ends_with(b"\"")
711        {
712            Ok(Cow::Borrowed(arg))
713        } else {
714            Err(ShellError::ExternalCommand {
715                label: "Arguments to CMD internal commands cannot contain embedded double quotes"
716                    .into(),
717                help: "this case currently cannot be securely handled".into(),
718                span: *span,
719            })
720        }
721    } else if bytes.contains(&b' ') || has_cmd_special_character(bytes) {
722        // If `arg` contains space or special characters, quote the entire argument by double quotes.
723        let mut new_str = OsString::new();
724        new_str.push("\"");
725        new_str.push(arg);
726        new_str.push("\"");
727        Ok(Cow::Owned(new_str))
728    } else {
729        // FIXME?: what if `arg.is_empty()`?
730        Ok(Cow::Borrowed(arg))
731    }
732}
733
734#[cfg(test)]
735mod test {
736    use super::*;
737    use nu_test_support::{fs::Stub, playground::Playground};
738
739    #[test]
740    fn test_expand_glob() {
741        Playground::setup("test_expand_glob", |dirs, play| {
742            play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]);
743
744            let cwd = dirs.test().as_std_path();
745
746            let actual = expand_glob("*.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
747            let expected = &["a.txt", "b.txt"];
748            assert_eq!(actual, expected);
749
750            let actual = expand_glob("./*.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
751            assert_eq!(actual, expected);
752
753            let actual = expand_glob("'*.txt'", cwd, Span::test_data(), Signals::empty()).unwrap();
754            let expected = &["'*.txt'"];
755            assert_eq!(actual, expected);
756
757            let actual = expand_glob(".", cwd, Span::test_data(), Signals::empty()).unwrap();
758            let expected = &["."];
759            assert_eq!(actual, expected);
760
761            let actual = expand_glob("./a.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
762            let expected = &["./a.txt"];
763            assert_eq!(actual, expected);
764
765            let actual = expand_glob("[*.txt", cwd, Span::test_data(), Signals::empty()).unwrap();
766            let expected = &["[*.txt"];
767            assert_eq!(actual, expected);
768
769            let actual =
770                expand_glob("~/foo.txt", cwd, Span::test_data(), 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        // set the PWD environment variable as it's required now
788        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::test_data()), 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::test_data()), 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::byte_stream(
807            ByteStream::read(
808                b"foo".as_slice(),
809                Span::test_data(),
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}