nushell/crates/nu-command/src/system/run_external.rs
Nils Feierabend 237a685605
Consider PATH when running command is nuscript in windows (#15486)
<!--
if this PR closes one or more issues, you can automatically link the PR
with
them by using one of the [*linking
keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword),
e.g.
- this PR should close #xxxx
- fixes #xxxx

you can also mention related issues, PRs or discussions!
-->

Fixes #15476

# Description
<!--
Thank you for improving Nushell. Please, check our [contributing
guide](../CONTRIBUTING.md) and talk to the core team before making major
changes.

Description of your pull request goes here. **Provide examples and/or
screenshots** if your changes affect the user experience.
-->

Consider PATH when checking for potential_nuscript_in_windows to allow
executing scripts which are in PATH without having to full path address
them. It previously only checked the current working directory so only
relative paths to cwd and full path worked.

The current implementation runs this then through cmd.exe /D /C which
can run it with assoc and ftype set for nushell scripts.
We could instead run it through nu as `std::env::current_exe()` avoiding
the cmd call and the need for assoc and ftype (see:
8b25173f02).
But ive left the current implementation for this intact to not change
implementation details, avoid a bigger change and leave this open for
discussion here since im not sure if this has any major implications.

# User-Facing Changes
<!-- List of all changes that impact the user experience here. This
helps us keep track of breaking changes. -->
This would now run every external command through PATH an additional
time on windows, so potentially twice. I dont think this has any bigger
effect.

# Tests + Formatting
<!--
Don't forget to add tests that cover your changes.

Make sure you've run and fixed any issues with these commands:

- `cargo fmt --all -- --check` to check standard code formatting (`cargo
fmt --all` applies these changes)
- `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to
check that you're using the standard code style
- `cargo test --workspace` to check that all tests pass (on Windows make
sure to [enable developer
mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging))
- `cargo run -- -c "use toolkit.nu; toolkit test stdlib"` to run the
tests for the standard library

> **Note**
> from `nushell` you can also use the `toolkit` as follows
> ```bash
> use toolkit.nu # or use an `env_change` hook to activate it
automatically
> toolkit check pr
> ```
-->

# After Submitting
<!-- If your PR had any user-facing changes, update [the
documentation](https://github.com/nushell/nushell.github.io) after the
PR is merged, if necessary. This will help us keep the docs up to date.
-->
2025-04-04 06:35:36 -05:00

825 lines
30 KiB
Rust

use nu_cmd_base::hook::eval_hook;
use nu_engine::{command_prelude::*, env_to_strings};
use nu_path::{dots::expand_ndots_safe, expand_tilde, AbsolutePath};
use nu_protocol::{
did_you_mean,
engine::{FrozenJob, Job},
process::{ChildProcess, PostWaitCallback},
shell_error::io::IoError,
ByteStream, NuGlob, OutDest, Signals, UseAnsiColoring,
};
use nu_system::{kill_by_pid, ForegroundChild, ForegroundWaitStatus};
use nu_utils::IgnoreCaseExt;
use pathdiff::diff_paths;
#[cfg(windows)]
use std::os::windows::process::CommandExt;
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
io::Write,
path::{Path, PathBuf},
process::Stdio,
sync::Arc,
thread,
};
#[derive(Clone)]
pub struct External;
impl Command for External {
fn name(&self) -> &str {
"run-external"
}
fn description(&self) -> &str {
"Runs external command."
}
fn signature(&self) -> nu_protocol::Signature {
Signature::build(self.name())
.input_output_types(vec![(Type::Any, Type::Any)])
.rest(
"command",
SyntaxShape::OneOf(vec![SyntaxShape::GlobPattern, SyntaxShape::Any]),
"External command to run, with arguments.",
)
.category(Category::System)
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
input: PipelineData,
) -> Result<PipelineData, ShellError> {
let cwd = engine_state.cwd(Some(stack))?;
let rest = call.rest::<Value>(engine_state, stack, 0)?;
let name_args = rest.split_first();
let Some((name, call_args)) = name_args else {
return Err(ShellError::MissingParameter {
param_name: "no command given".into(),
span: call.head,
});
};
let name_str: Cow<str> = match &name {
Value::Glob { val, .. } => Cow::Borrowed(val),
Value::String { val, .. } => Cow::Borrowed(val),
_ => Cow::Owned(name.clone().coerce_into_string()?),
};
let expanded_name = match &name {
// Expand tilde and ndots on the name if it's a bare string / glob (#13000)
Value::Glob { no_expand, .. } if !*no_expand => {
expand_ndots_safe(expand_tilde(&*name_str))
}
_ => Path::new(&*name_str).to_owned(),
};
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
// On Windows, the user could have run the cmd.exe built-in "assoc" command
// Example: "assoc .nu=nuscript" and then run the cmd.exe built-in "ftype" command
// Example: "ftype nuscript=C:\path\to\nu.exe '%1' %*" and then added the nushell
// script extension ".NU" to the PATHEXT environment variable. In this case, we use
// the which command, which will find the executable with or without the extension.
// If it "which" returns true, that means that we've found the nushell script and we
// believe the user wants to use the windows association to run the script. The only
// easy way to do this is to run cmd.exe with the script as an argument.
let potential_nuscript_in_windows = if cfg!(windows) {
// let's make sure it's a .nu script
if let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) {
let ext = executable
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
ext == "NU"
} else {
false
}
} else {
false
};
// let's make sure it's a .ps1 script, but only on Windows
let potential_powershell_script = if cfg!(windows) {
if let Some(executable) = which(&expanded_name, "", cwd.as_ref()) {
let ext = executable
.extension()
.unwrap_or_default()
.to_string_lossy()
.to_uppercase();
ext == "PS1"
} else {
false
}
} else {
false
};
// Find the absolute path to the executable. On Windows, set the
// executable to "cmd.exe" if it's a CMD internal command. If the
// command is not found, display a helpful error message.
let executable = if cfg!(windows)
&& (is_cmd_internal_command(&name_str) || potential_nuscript_in_windows)
{
PathBuf::from("cmd.exe")
} else if cfg!(windows) && potential_powershell_script {
// If we're on Windows and we're trying to run a PowerShell script, we'll use
// `powershell.exe` to run it. We shouldn't have to check for powershell.exe because
// it's automatically installed on all modern windows systems.
PathBuf::from("powershell.exe")
} else {
// Determine the PATH to be used and then use `which` to find it - though this has no
// effect if it's an absolute path already
let Some(executable) = which(&expanded_name, &paths, cwd.as_ref()) else {
return Err(command_not_found(
&name_str,
call.head,
engine_state,
stack,
&cwd,
));
};
executable
};
// Create the command.
let mut command = std::process::Command::new(executable);
// Configure PWD.
command.current_dir(cwd);
// Configure environment variables.
let envs = env_to_strings(engine_state, stack)?;
command.env_clear();
command.envs(envs);
// Configure args.
let args = eval_external_arguments(engine_state, stack, call_args.to_vec())?;
#[cfg(windows)]
if is_cmd_internal_command(&name_str) || potential_nuscript_in_windows {
// The /D flag disables execution of AutoRun commands from registry.
// The /C flag followed by a command name instructs CMD to execute
// that command and quit.
command.args(["/D", "/C", &expanded_name.to_string_lossy()]);
for arg in &args {
command.raw_arg(escape_cmd_argument(arg)?);
}
} else if potential_powershell_script {
use nu_path::canonicalize_with;
// canonicalize the path to the script so that tests pass
let canon_path = if let Ok(cwd) = engine_state.cwd_as_string(None) {
canonicalize_with(&expanded_name, cwd).map_err(|err| {
IoError::new(err.kind(), call.head, PathBuf::from(&expanded_name))
})?
} else {
// If we can't get the current working directory, just provide the expanded name
expanded_name
};
// The -Command flag followed by a script name instructs PowerShell to
// execute that script and quit.
command.args(["-Command", &canon_path.to_string_lossy()]);
for arg in &args {
command.raw_arg(arg.item.clone());
}
} else {
command.args(args.into_iter().map(|s| s.item));
}
#[cfg(not(windows))]
command.args(args.into_iter().map(|s| s.item));
// Configure stdout and stderr. If both are set to `OutDest::Pipe`,
// we'll set up a pipe that merges two streams into one.
let stdout = stack.stdout();
let stderr = stack.stderr();
let merged_stream = if matches!(stdout, OutDest::Pipe) && matches!(stderr, OutDest::Pipe) {
let (reader, writer) =
os_pipe::pipe().map_err(|err| IoError::new(err.kind(), call.head, None))?;
command.stdout(
writer
.try_clone()
.map_err(|err| IoError::new(err.kind(), call.head, None))?,
);
command.stderr(writer);
Some(reader)
} else {
if engine_state.is_background_job()
&& matches!(stdout, OutDest::Inherit | OutDest::Print)
{
command.stdout(Stdio::null());
} else {
command.stdout(
Stdio::try_from(stdout)
.map_err(|err| IoError::new(err.kind(), call.head, None))?,
);
}
if engine_state.is_background_job()
&& matches!(stderr, OutDest::Inherit | OutDest::Print)
{
command.stderr(Stdio::null());
} else {
command.stderr(
Stdio::try_from(stderr)
.map_err(|err| IoError::new(err.kind(), call.head, None))?,
);
}
None
};
// Configure stdin. We'll try connecting input to the child process
// directly. If that's not possible, we'll set up a pipe and spawn a
// thread to copy data into the child process.
let data_to_copy_into_stdin = match input {
PipelineData::ByteStream(stream, metadata) => match stream.into_stdio() {
Ok(stdin) => {
command.stdin(stdin);
None
}
Err(stream) => {
command.stdin(Stdio::piped());
Some(PipelineData::ByteStream(stream, metadata))
}
},
PipelineData::Empty => {
command.stdin(Stdio::inherit());
None
}
value => {
command.stdin(Stdio::piped());
Some(value)
}
};
// Log the command we're about to run in case it's useful for debugging purposes.
log::trace!("run-external spawning: {command:?}");
// Spawn the child process. On Unix, also put the child process to
// foreground if we're in an interactive session.
#[cfg(windows)]
let child = ForegroundChild::spawn(command);
#[cfg(unix)]
let child = ForegroundChild::spawn(
command,
engine_state.is_interactive,
engine_state.is_background_job(),
&engine_state.pipeline_externals_state,
);
let mut child = child.map_err(|err| {
IoError::new_internal(
err.kind(),
"Could not spawn foreground child",
nu_protocol::location!(),
)
})?;
if let Some(thread_job) = &engine_state.current_thread_job {
if !thread_job.try_add_pid(child.pid()) {
kill_by_pid(child.pid().into()).map_err(|err| {
ShellError::Io(IoError::new_internal(
err.kind(),
"Could not spawn external stdin worker",
nu_protocol::location!(),
))
})?;
}
}
// If we need to copy data into the child process, do it now.
if let Some(data) = data_to_copy_into_stdin {
let stdin = child.as_mut().stdin.take().expect("stdin is piped");
let engine_state = engine_state.clone();
let stack = stack.clone();
thread::Builder::new()
.name("external stdin worker".into())
.spawn(move || {
let _ = write_pipeline_data(engine_state, stack, data, stdin);
})
.map_err(|err| {
IoError::new_with_additional_context(
err.kind(),
call.head,
None,
"Could not spawn external stdin worker",
)
})?;
}
let jobs = engine_state.jobs.clone();
let this_job = engine_state.current_thread_job.clone();
let is_interactive = engine_state.is_interactive;
let child_pid = child.pid();
// Wrap the output into a `PipelineData::ByteStream`.
let mut child = ChildProcess::new(
child,
merged_stream,
matches!(stderr, OutDest::Pipe),
call.head,
// handle wait statuses for job control
Some(PostWaitCallback(Box::new(move |status| {
if let Some(this_job) = this_job {
this_job.remove_pid(child_pid);
}
if let ForegroundWaitStatus::Frozen(unfreeze) = status {
let mut jobs = jobs.lock().expect("jobs lock is poisoned!");
let job_id = jobs.add_job(Job::Frozen(FrozenJob { unfreeze }));
if is_interactive {
println!("\nJob {} is frozen", job_id.get());
}
}
}))),
)?;
if matches!(stdout, OutDest::Pipe | OutDest::PipeSeparate)
|| matches!(stderr, OutDest::Pipe | OutDest::PipeSeparate)
{
child.ignore_error(true);
}
Ok(PipelineData::ByteStream(
ByteStream::child(child, call.head),
None,
))
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
description: "Run an external command",
example: r#"run-external "echo" "-n" "hello""#,
result: None,
},
Example {
description: "Redirect stdout from an external command into the pipeline",
example: r#"run-external "echo" "-n" "hello" | split chars"#,
result: None,
},
Example {
description: "Redirect stderr from an external command into the pipeline",
example: r#"run-external "nu" "-c" "print -e hello" e>| split chars"#,
result: None,
},
]
}
}
/// Evaluate all arguments, performing expansions when necessary.
pub fn eval_external_arguments(
engine_state: &EngineState,
stack: &mut Stack,
call_args: Vec<Value>,
) -> Result<Vec<Spanned<OsString>>, ShellError> {
let cwd = engine_state.cwd(Some(stack))?;
let mut args: Vec<Spanned<OsString>> = Vec::with_capacity(call_args.len());
for arg in call_args {
let span = arg.span();
match arg {
// Expand globs passed to run-external
Value::Glob { val, no_expand, .. } if !no_expand => args.extend(
expand_glob(
&val,
cwd.as_std_path(),
span,
engine_state.signals().clone(),
)?
.into_iter()
.map(|s| s.into_spanned(span)),
),
other => args
.push(OsString::from(coerce_into_string(engine_state, other)?).into_spanned(span)),
}
}
Ok(args)
}
/// Custom `coerce_into_string()`, including globs, since those are often args to `run-external`
/// as well
fn coerce_into_string(engine_state: &EngineState, val: Value) -> Result<String, ShellError> {
match val {
Value::List { .. } => Err(ShellError::CannotPassListToExternal {
arg: String::from_utf8_lossy(engine_state.get_span_contents(val.span())).into_owned(),
span: val.span(),
}),
Value::Glob { val, .. } => Ok(val),
_ => val.coerce_into_string(),
}
}
/// Performs glob expansion on `arg`. If the expansion found no matches or the pattern
/// is not a valid glob, then this returns the original string as the expansion result.
///
/// Note: This matches the default behavior of Bash, but is known to be
/// error-prone. We might want to change this behavior in the future.
fn expand_glob(
arg: &str,
cwd: &Path,
span: Span,
signals: Signals,
) -> Result<Vec<OsString>, ShellError> {
// For an argument that isn't a glob, just do the `expand_tilde`
// and `expand_ndots` expansion
if !nu_glob::is_glob(arg) {
let path = expand_ndots_safe(expand_tilde(arg));
return Ok(vec![path.into()]);
}
// We must use `nu_engine::glob_from` here, in order to ensure we get paths from the correct
// dir
let glob = NuGlob::Expand(arg.to_owned()).into_spanned(span);
if let Ok((prefix, matches)) = nu_engine::glob_from(&glob, cwd, span, None, signals.clone()) {
let mut result: Vec<OsString> = vec![];
for m in matches {
signals.check(span)?;
if let Ok(arg) = m {
let arg = resolve_globbed_path_to_cwd_relative(arg, prefix.as_ref(), cwd);
result.push(arg.into());
} else {
result.push(arg.into());
}
}
// FIXME: do we want to special-case this further? We might accidentally expand when they don't
// intend to
if result.is_empty() {
result.push(arg.into());
}
Ok(result)
} else {
Ok(vec![arg.into()])
}
}
fn resolve_globbed_path_to_cwd_relative(
path: PathBuf,
prefix: Option<&PathBuf>,
cwd: &Path,
) -> PathBuf {
if let Some(prefix) = prefix {
if let Ok(remainder) = path.strip_prefix(prefix) {
let new_prefix = if let Some(pfx) = diff_paths(prefix, cwd) {
pfx
} else {
prefix.to_path_buf()
};
new_prefix.join(remainder)
} else {
path
}
} else {
path
}
}
/// Write `PipelineData` into `writer`. If `PipelineData` is not binary, it is
/// first rendered using the `table` command.
///
/// Note: Avoid using this function when piping data from an external command to
/// another external command, because it copies data unnecessarily. Instead,
/// extract the pipe from the `PipelineData::ByteStream` of the first command
/// and hand it to the second command directly.
fn write_pipeline_data(
mut engine_state: EngineState,
mut stack: Stack,
data: PipelineData,
mut writer: impl Write,
) -> Result<(), ShellError> {
if let PipelineData::ByteStream(stream, ..) = data {
stream.write_to(writer)?;
} else if let PipelineData::Value(Value::Binary { val, .. }, ..) = data {
writer.write_all(&val).map_err(|err| {
IoError::new_internal(
err.kind(),
"Could not write pipeline data",
nu_protocol::location!(),
)
})?;
} else {
stack.start_collect_value();
// Turn off color as we pass data through
Arc::make_mut(&mut engine_state.config).use_ansi_coloring = UseAnsiColoring::False;
// Invoke the `table` command.
let output =
crate::Table.run(&engine_state, &mut stack, &Call::new(Span::unknown()), data)?;
// Write the output.
for value in output {
let bytes = value.coerce_into_binary()?;
writer.write_all(&bytes).map_err(|err| {
IoError::new_internal(
err.kind(),
"Could not write pipeline data",
nu_protocol::location!(),
)
})?;
}
}
Ok(())
}
/// Returns a helpful error message given an invalid command name,
pub fn command_not_found(
name: &str,
span: Span,
engine_state: &EngineState,
stack: &mut Stack,
cwd: &AbsolutePath,
) -> ShellError {
// Run the `command_not_found` hook if there is one.
if let Some(hook) = &stack.get_config(engine_state).hooks.command_not_found {
let mut stack = stack.start_collect_value();
// Set a special environment variable to avoid infinite loops when the
// `command_not_found` hook triggers itself.
let canary = "ENTERED_COMMAND_NOT_FOUND";
if stack.has_env_var(engine_state, canary) {
return ShellError::ExternalCommand {
label: format!(
"Command {name} not found while running the `command_not_found` hook"
),
help: "Make sure the `command_not_found` hook itself does not use unknown commands"
.into(),
span,
};
}
stack.add_env_var(canary.into(), Value::bool(true, Span::unknown()));
let output = eval_hook(
&mut engine_state.clone(),
&mut stack,
None,
vec![("cmd_name".into(), Value::string(name, span))],
hook,
"command_not_found",
);
// Remove the special environment variable that we just set.
stack.remove_env_var(engine_state, canary);
match output {
Ok(PipelineData::Value(Value::String { val, .. }, ..)) => {
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: val,
span,
};
}
Err(err) => {
return err;
}
_ => {
// The hook did not return a string, so ignore it.
}
}
}
// If the name is one of the removed commands, recommend a replacement.
if let Some(replacement) = crate::removed_commands().get(&name.to_lowercase()) {
return ShellError::RemovedCommand {
removed: name.to_lowercase(),
replacement: replacement.clone(),
span,
};
}
// The command might be from another module. Try to find it.
if let Some(module) = engine_state.which_module_has_decl(name.as_bytes(), &[]) {
let module = String::from_utf8_lossy(module);
// Is the command already imported?
let full_name = format!("{module} {name}");
if engine_state.find_decl(full_name.as_bytes(), &[]).is_some() {
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: format!("Did you mean `{full_name}`?"),
span,
};
} else {
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: format!("A command with that name exists in module `{module}`. Try importing it with `use`"),
span,
};
}
}
// Try to match the name with the search terms of existing commands.
let signatures = engine_state.get_signatures_and_declids(false);
if let Some((sig, _)) = signatures.iter().find(|(sig, _)| {
sig.search_terms
.iter()
.any(|term| term.to_folded_case() == name.to_folded_case())
}) {
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: format!("Did you mean `{}`?", sig.name),
span,
};
}
// Try a fuzzy search on the names of all existing commands.
if let Some(cmd) = did_you_mean(signatures.iter().map(|(sig, _)| &sig.name), name) {
// The user is invoking an external command with the same name as a
// built-in command. Remind them of this.
if cmd == name {
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: "There is a built-in command with the same name".into(),
span,
};
}
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: format!("Did you mean `{cmd}`?"),
span,
};
}
// If we find a file, it's likely that the user forgot to set permissions
if cwd.join(name).is_file() {
return ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: format!("`{name}` refers to a file that is not executable. Did you forget to set execute permissions?"),
span,
};
}
// We found nothing useful. Give up and return a generic error message.
ShellError::ExternalCommand {
label: format!("Command `{name}` not found"),
help: format!("`{name}` is neither a Nushell built-in or a known external command"),
span,
}
}
/// Searches for the absolute path of an executable by name. `.bat` and `.cmd`
/// files are recognized as executables on Windows.
///
/// This is a wrapper around `which::which_in()` except that, on Windows, it
/// also searches the current directory before any PATH entries.
///
/// Note: the `which.rs` crate always uses PATHEXT from the environment. As
/// such, changing PATHEXT within Nushell doesn't work without updating the
/// actual environment of the Nushell process.
pub fn which(name: impl AsRef<OsStr>, paths: &str, cwd: &Path) -> Option<PathBuf> {
#[cfg(windows)]
let paths = format!("{};{}", cwd.display(), paths);
which::which_in(name, Some(paths), cwd).ok()
}
/// Returns true if `name` is a (somewhat useful) CMD internal command. The full
/// list can be found at <https://ss64.com/nt/syntax-internal.html>
fn is_cmd_internal_command(name: &str) -> bool {
const COMMANDS: &[&str] = &[
"ASSOC", "CLS", "ECHO", "FTYPE", "MKLINK", "PAUSE", "START", "VER", "VOL",
];
COMMANDS.iter().any(|cmd| cmd.eq_ignore_ascii_case(name))
}
/// Returns true if a string contains CMD special characters.
fn has_cmd_special_character(s: impl AsRef<[u8]>) -> bool {
s.as_ref()
.iter()
.any(|b| matches!(b, b'<' | b'>' | b'&' | b'|' | b'^'))
}
/// Escape an argument for CMD internal commands. The result can be safely passed to `raw_arg()`.
#[cfg_attr(not(windows), allow(dead_code))]
fn escape_cmd_argument(arg: &Spanned<OsString>) -> Result<Cow<'_, OsStr>, ShellError> {
let Spanned { item: arg, span } = arg;
let bytes = arg.as_encoded_bytes();
if bytes.iter().any(|b| matches!(b, b'\r' | b'\n' | b'%')) {
// \r and \n truncate the rest of the arguments and % can expand environment variables
Err(ShellError::ExternalCommand {
label:
"Arguments to CMD internal commands cannot contain new lines or percent signs '%'"
.into(),
help: "some characters currently cannot be securely escaped".into(),
span: *span,
})
} else if bytes.contains(&b'"') {
// If `arg` is already quoted by double quotes, confirm there's no
// embedded double quotes, then leave it as is.
if bytes.iter().filter(|b| **b == b'"').count() == 2
&& bytes.starts_with(b"\"")
&& bytes.ends_with(b"\"")
{
Ok(Cow::Borrowed(arg))
} else {
Err(ShellError::ExternalCommand {
label: "Arguments to CMD internal commands cannot contain embedded double quotes"
.into(),
help: "this case currently cannot be securely handled".into(),
span: *span,
})
}
} else if bytes.contains(&b' ') || has_cmd_special_character(bytes) {
// If `arg` contains space or special characters, quote the entire argument by double quotes.
let mut new_str = OsString::new();
new_str.push("\"");
new_str.push(arg);
new_str.push("\"");
Ok(Cow::Owned(new_str))
} else {
// FIXME?: what if `arg.is_empty()`?
Ok(Cow::Borrowed(arg))
}
}
#[cfg(test)]
mod test {
use super::*;
use nu_test_support::{fs::Stub, playground::Playground};
#[test]
fn test_expand_glob() {
Playground::setup("test_expand_glob", |dirs, play| {
play.with_files(&[Stub::EmptyFile("a.txt"), Stub::EmptyFile("b.txt")]);
let cwd = dirs.test().as_std_path();
let actual = expand_glob("*.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
let expected = &["a.txt", "b.txt"];
assert_eq!(actual, expected);
let actual = expand_glob("./*.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
assert_eq!(actual, expected);
let actual = expand_glob("'*.txt'", cwd, Span::unknown(), Signals::empty()).unwrap();
let expected = &["'*.txt'"];
assert_eq!(actual, expected);
let actual = expand_glob(".", cwd, Span::unknown(), Signals::empty()).unwrap();
let expected = &["."];
assert_eq!(actual, expected);
let actual = expand_glob("./a.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
let expected = &["./a.txt"];
assert_eq!(actual, expected);
let actual = expand_glob("[*.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
let expected = &["[*.txt"];
assert_eq!(actual, expected);
let actual = expand_glob("~/foo.txt", cwd, Span::unknown(), Signals::empty()).unwrap();
let home = dirs::home_dir().expect("failed to get home dir");
let expected: Vec<OsString> = vec![home.join("foo.txt").into()];
assert_eq!(actual, expected);
})
}
#[test]
fn test_write_pipeline_data() {
let mut engine_state = EngineState::new();
let stack = Stack::new();
let cwd = std::env::current_dir()
.unwrap()
.into_os_string()
.into_string()
.unwrap();
// set the PWD environment variable as it's required now
engine_state.add_env_var("PWD".into(), Value::string(cwd, Span::test_data()));
let mut buf = vec![];
let input = PipelineData::Empty;
write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
assert_eq!(buf, b"");
let mut buf = vec![];
let input = PipelineData::Value(Value::string("foo", Span::unknown()), None);
write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
assert_eq!(buf, b"foo");
let mut buf = vec![];
let input = PipelineData::Value(Value::binary(b"foo", Span::unknown()), None);
write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
assert_eq!(buf, b"foo");
let mut buf = vec![];
let input = PipelineData::ByteStream(
ByteStream::read(
b"foo".as_slice(),
Span::unknown(),
Signals::empty(),
ByteStreamType::Unknown,
),
None,
);
write_pipeline_data(engine_state.clone(), stack.clone(), input, &mut buf).unwrap();
assert_eq!(buf, b"foo");
}
}