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 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                // Prepend elements in command list to the list of arguments except the first
76                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            // Expand tilde and ndots on the name if it's a bare string / glob (#13000)
84            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        // On Windows, the user could have run the cmd.exe built-in commands "assoc"
93        // and "ftype" to create a file association for an arbitrary file extension.
94        // They then could have added that extension to the PATHEXT environment variable.
95        // For example, a nushell script with extension ".nu" can be set up with
96        // "assoc .nu=nuscript" and "ftype nuscript=C:\path\to\nu.exe '%1' %*",
97        // and then by adding ".NU" to PATHEXT. In this case we use the which command,
98        // which will find the executable with or without the extension. If "which"
99        // returns true, that means that we've found the script and we believe the
100        // user wants to use the windows association to run the script. The only
101        // easy way to do this is to run cmd.exe with the script as an argument.
102        // File extensions of .COM, .EXE, .BAT, and .CMD are ignored because Windows
103        // can run those files directly. PS1 files are also ignored and that
104        // extension is handled in a separate block below.
105        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's make sure it's a .ps1 script, but only on Windows
124        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        // Find the absolute path to the executable. On Windows, set the
140        // executable to "cmd.exe" if it's a CMD internal command. If the
141        // command is not found, display a helpful error message.
142        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            // If we're on Windows and we're trying to run a PowerShell script, we'll use
148            // `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
149            // it's automatically installed on all modern windows systems.
150            PathBuf::from("powershell.exe")
151        } else {
152            // Determine the PATH to be used and then use `which` to find it - though this has no
153            // effect if it's an absolute path already
154            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        // Create the command.
167        let mut command = std::process::Command::new(&executable);
168
169        // Configure PWD.
170        command.current_dir(cwd);
171
172        // Configure environment variables.
173        let envs = env_to_strings(engine_state, stack)?;
174        command.env_clear();
175        command.envs(envs);
176
177        // Configure args.
178        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            // The /D flag disables execution of AutoRun commands from registry.
182            // The /C flag followed by a command name instructs CMD to execute
183            // that command and quit.
184            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        // Configure stdout and stderr. If both are set to `OutDest::Pipe`,
201        // we'll set up a pipe that merges two streams into one.
202        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        // Configure stdin. We'll try connecting input to the child process
239        // directly. If that's not possible, we'll set up a pipe and spawn a
240        // thread to copy data into the child process.
241        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 the command we're about to run in case it's useful for debugging purposes.
263        log::trace!("run-external spawning: {command:?}");
264
265        // Spawn the child process. On Unix, also put the child process to
266        // foreground if we're in an interactive session.
267        #[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 we need to copy data into the child process, do it now.
295        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        // Wrap the output into a `PipelineData::ByteStream`.
317        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
366/// Evaluate all arguments, performing expansions when necessary.
367pub 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            // Expand globs passed to run-external
379            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
396/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external`
397/// as well
398fn 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
409/// Performs glob expansion on `arg`. If the expansion found no matches or the pattern
410/// is not a valid glob, then this returns the original string as the expansion result.
411///
412/// Note: This matches the default behavior of Bash, but is known to be
413/// error-prone. We might want to change this behavior in the future.
414fn expand_glob(
415    arg: &str,
416    cwd: &Path,
417    span: Span,
418    signals: Signals,
419) -> Result<Vec<OsString>, ShellError> {
420    // For an argument that isn't a glob, just do the `expand_tilde`
421    // and `expand_ndots` expansion
422    if !nu_glob::is_glob(arg) {
423        let path = expand_ndots_safe(expand_tilde(arg));
424        return Ok(vec![path.into()]);
425    }
426
427    // We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct
428    // dir
429    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        // FIXME: do we want to special-case this further? We might accidentally expand when they don't
444        // intend to
445        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
476/// Write `PipelineData` into `writer`. If `PipelineData` is not binary, it is
477/// first rendered using the `table` command.
478///
479/// Note: Avoid using this function when piping data from an external command to
480/// another external command, because it copies data unnecessarily. Instead,
481/// extract the pipe from the `PipelineData::ByteStream` of the first command
482/// and hand it to the second command directly.
483fn 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        // Turn off color as we pass data through
503        Arc::make_mut(&mut engine_state.config).use_ansi_coloring = UseAnsiColoring::False;
504
505        // Invoke the `table` command.
506        let output =
507            crate::Table.run(&engine_state, &mut stack, &Call::new(Span::unknown()), data)?;
508
509        // Write the output.
510        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
524/// Returns a helpful error message given an invalid command name,
525pub fn command_not_found(
526    name: &str,
527    span: Span,
528    engine_state: &EngineState,
529    stack: &mut Stack,
530    cwd: &AbsolutePath,
531) -> ShellError {
532    // Run the `command_not_found` hook if there is one.
533    if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found {
534        let mut stack = stack.start_collect_value();
535        // Set a special environment variable to avoid infinite loops when the
536        // `command_not_found` hook triggers itself.
537        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        // Remove the special environment variable that we just set.
560        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                // The hook did not return a string, so ignore it.
575            }
576        }
577    }
578
579    // If the name is one of the removed commands, recommend a replacement.
580    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    // The command might be from another module. Try to find it.
589    if let Some(module) = engine_state.which_module_has_decl(name.as_bytes(), &[]) {
590        let module = String::from_utf8_lossy(module);
591        // Is the command already imported?
592        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    // Try to match the name with the search terms of existing commands.
611    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    // Try a fuzzy search on the names of all existing commands.
625    if let Some(cmd) = did_you_mean(signatures.iter().map(|(sig, _)| &sig.name), name) {
626        // The user is invoking an external command with the same name as a
627        // built-in command. Remind them of this.
628        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 we find a file, it's likely that the user forgot to set permissions
643    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    // We found nothing useful. Give up and return a generic error message.
654    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
661/// Searches for the absolute path of an executable by name. `.bat` and `.cmd`
662/// files are recognized as executables on Windows.
663///
664/// This is a wrapper around `which::which_in()` except that, on Windows, it
665/// also searches the current directory before any PATH entries.
666///
667/// Note: the `which.rs` crate always uses PATHEXT from the environment. As
668/// such, changing PATHEXT within Nushell doesn't work without updating the
669/// actual environment of the Nushell process.
670pub 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
676/// Returns true if `name` is a (somewhat useful) CMD internal command. The full
677/// list can be found at <https://ss64.com/nt/syntax-internal.html>
678fn 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
685/// Returns true if a string contains CMD special characters.
686fn 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/// Escape an argument for CMD internal commands. The result can be safely passed to `raw_arg()`.
693#[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        // \r and \n truncate the rest of the arguments and % can expand environment variables
699        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 `arg` is already quoted by double quotes, confirm there's no
708        // embedded double quotes, then leave it as is.
709        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        // If `arg` contains space or special characters, quote the entire argument by double quotes.
724        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        // FIXME?: what if `arg.is_empty()`?
731        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        // 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::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}