mirror of
https://github.com/nushell/nushell.git
synced 2024-11-28 11:24:09 +01:00
Rewrite run_external.rs (#12921)
This PR is a complete rewrite of `run_external.rs`. The main goal of the rewrite is improving readability, but it also fixes some bugs related to argument handling and the PATH variable (fixes https://github.com/nushell/nushell/issues/6011). I'll discuss some technical details to make reviewing easier. ## Argument handling Quoting arguments for external commands is hard. Like, *really* hard. We've had more than a dozen issues and PRs dedicated to quoting arguments (see Appendix) but the current implementation is still buggy. Here's a demonstration of the buggy behavior: ```nu let foo = "'bar'" ^touch $foo # This creates a file named `bar`, but it should be `'bar'` ^touch ...[ "'bar'" ] # Same ``` I'll describe how this PR deals with argument handling. First, we'll introduce the concept of **bare strings**. Bare strings are **string literals** that are either **unquoted** or **quoted by backticks** [^1]. Strings within a list literal are NOT considered bare strings, even if they are unquoted or quoted by backticks. When a bare string is used as an argument to external process, we need to perform tilde-expansion, glob-expansion, and inner-quotes-removal, in that order. "Inner-quotes-removal" means transforming from `--option="value"` into `--option=value`. ## `.bat` files and CMD built-ins On Windows, `.bat` files and `.cmd` files are considered executable, but they need `CMD.exe` as the interpreter. The Rust standard library supports running `.bat` files directly and will spawn `CMD.exe` under the hood (see [documentation](https://doc.rust-lang.org/std/process/index.html#windows-argument-splitting)). However, other extensions are not supported [^2]. Nushell also supports a selected number of CMD built-ins. The problem with CMD is that it uses a different set of quoting rules. Correctly quoting for CMD requires using [Command::raw_arg()](https://doc.rust-lang.org/std/os/windows/process/trait.CommandExt.html#tymethod.raw_arg) and manually quoting CMD special characters, on top of quoting from the Nushell side. ~~I decided that this is too complex and chose to reject special characters in CMD built-ins instead [^3]. Hopefully this will not affact real-world use cases.~~ I've implemented escaping that works reasonably well. ## `which-support` feature The `which` crate is now a hard dependency of `nu-command`, making the `which-support` feature essentially useless. The `which` crate is already a hard dependency of `nu-cli`, and we should consider removing the `which-support` feature entirely. ## Appendix Here's a list of quoting-related issues and PRs in rough chronological order. * https://github.com/nushell/nushell/issues/4609 * https://github.com/nushell/nushell/issues/4631 * https://github.com/nushell/nushell/issues/4601 * https://github.com/nushell/nushell/pull/5846 * https://github.com/nushell/nushell/issues/5978 * https://github.com/nushell/nushell/pull/6014 * https://github.com/nushell/nushell/issues/6154 * https://github.com/nushell/nushell/pull/6161 * https://github.com/nushell/nushell/issues/6399 * https://github.com/nushell/nushell/pull/6420 * https://github.com/nushell/nushell/pull/6426 * https://github.com/nushell/nushell/issues/6465 * https://github.com/nushell/nushell/issues/6559 * https://github.com/nushell/nushell/pull/6560 [^1]: The idea that backtick-quoted strings act like bare strings was introduced by Kubouch and briefly mentioned in [the language reference](https://www.nushell.sh/lang-guide/chapters/strings_and_text.html#backtick-quotes). [^2]: The documentation also said "running .bat scripts in this way may be removed in the future and so should not be relied upon", which is another reason to move away from this. But again, quoting for CMD is hard. [^3]: If anyone wants to try, the best resource I found on the topic is [this](https://daviddeley.com/autohotkey/parameters/parameters.htm).
This commit is contained in:
parent
64afb52ffa
commit
6c649809d3
1
Cargo.lock
generated
1
Cargo.lock
generated
@ -3012,6 +3012,7 @@ dependencies = [
|
|||||||
"sha2",
|
"sha2",
|
||||||
"sysinfo",
|
"sysinfo",
|
||||||
"tabled",
|
"tabled",
|
||||||
|
"tempfile",
|
||||||
"terminal_size",
|
"terminal_size",
|
||||||
"titlecase",
|
"titlecase",
|
||||||
"toml 0.8.12",
|
"toml 0.8.12",
|
||||||
|
@ -99,7 +99,7 @@ uu_whoami = { workspace = true }
|
|||||||
uuid = { workspace = true, features = ["v4"] }
|
uuid = { workspace = true, features = ["v4"] }
|
||||||
v_htmlescape = { workspace = true }
|
v_htmlescape = { workspace = true }
|
||||||
wax = { workspace = true }
|
wax = { workspace = true }
|
||||||
which = { workspace = true, optional = true }
|
which = { workspace = true }
|
||||||
unicode-width = { workspace = true }
|
unicode-width = { workspace = true }
|
||||||
|
|
||||||
[target.'cfg(windows)'.dependencies]
|
[target.'cfg(windows)'.dependencies]
|
||||||
@ -134,7 +134,7 @@ workspace = true
|
|||||||
plugin = ["nu-parser/plugin"]
|
plugin = ["nu-parser/plugin"]
|
||||||
sqlite = ["rusqlite"]
|
sqlite = ["rusqlite"]
|
||||||
trash-support = ["trash"]
|
trash-support = ["trash"]
|
||||||
which-support = ["which"]
|
which-support = []
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.93.1" }
|
nu-cmd-lang = { path = "../nu-cmd-lang", version = "0.93.1" }
|
||||||
@ -146,3 +146,4 @@ quickcheck = { workspace = true }
|
|||||||
quickcheck_macros = { workspace = true }
|
quickcheck_macros = { workspace = true }
|
||||||
rstest = { workspace = true, default-features = false }
|
rstest = { workspace = true, default-features = false }
|
||||||
pretty_assertions = { workspace = true }
|
pretty_assertions = { workspace = true }
|
||||||
|
tempfile = { workspace = true }
|
||||||
|
61
crates/nu-command/src/env/config/config_env.rs
vendored
61
crates/nu-command/src/env/config/config_env.rs
vendored
@ -1,6 +1,7 @@
|
|||||||
use super::utils::gen_command;
|
|
||||||
use nu_cmd_base::util::get_editor;
|
use nu_cmd_base::util::get_editor;
|
||||||
use nu_engine::{command_prelude::*, env_to_strings};
|
use nu_engine::{command_prelude::*, env_to_strings};
|
||||||
|
use nu_protocol::{process::ChildProcess, ByteStream};
|
||||||
|
use nu_system::ForegroundChild;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigEnv;
|
pub struct ConfigEnv;
|
||||||
@ -47,7 +48,7 @@ impl Command for ConfigEnv {
|
|||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
// `--default` flag handling
|
// `--default` flag handling
|
||||||
if call.has_flag(engine_state, stack, "default")? {
|
if call.has_flag(engine_state, stack, "default")? {
|
||||||
@ -55,10 +56,18 @@ impl Command for ConfigEnv {
|
|||||||
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
return Ok(Value::string(nu_utils::get_default_env(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
let env_vars_str = env_to_strings(engine_state, stack)?;
|
// Find the editor executable.
|
||||||
let nu_config = match engine_state.get_config_path("env-path") {
|
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||||
Some(path) => path,
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
None => {
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
|
let editor_executable =
|
||||||
|
crate::which(&editor_name, &paths, &cwd).ok_or(ShellError::ExternalCommand {
|
||||||
|
label: format!("`{editor_name}` not found"),
|
||||||
|
help: "Failed to find the editor executable".into(),
|
||||||
|
span: call.head,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(env_path) = engine_state.get_config_path("env-path") else {
|
||||||
return Err(ShellError::GenericError {
|
return Err(ShellError::GenericError {
|
||||||
error: "Could not find $nu.env-path".into(),
|
error: "Could not find $nu.env-path".into(),
|
||||||
msg: "Could not find $nu.env-path".into(),
|
msg: "Could not find $nu.env-path".into(),
|
||||||
@ -66,16 +75,40 @@ impl Command for ConfigEnv {
|
|||||||
help: None,
|
help: None,
|
||||||
inner: vec![],
|
inner: vec![],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
let env_path = env_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
let (item, config_args) = get_editor(engine_state, stack, call.head)?;
|
// Create the command.
|
||||||
|
let mut command = std::process::Command::new(editor_executable);
|
||||||
|
|
||||||
gen_command(call.head, nu_config, item, config_args, env_vars_str).run_with_input(
|
// Configure PWD.
|
||||||
engine_state,
|
command.current_dir(cwd);
|
||||||
stack,
|
|
||||||
input,
|
// Configure environment variables.
|
||||||
true,
|
let envs = env_to_strings(engine_state, stack)?;
|
||||||
)
|
command.env_clear();
|
||||||
|
command.envs(envs);
|
||||||
|
|
||||||
|
// Configure args.
|
||||||
|
command.arg(env_path);
|
||||||
|
command.args(editor_args);
|
||||||
|
|
||||||
|
// 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.pipeline_externals_state,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Wrap the output into a `PipelineData::ByteStream`.
|
||||||
|
let child = ChildProcess::new(child, None, false, call.head)?;
|
||||||
|
Ok(PipelineData::ByteStream(
|
||||||
|
ByteStream::child(child, call.head),
|
||||||
|
None,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
61
crates/nu-command/src/env/config/config_nu.rs
vendored
61
crates/nu-command/src/env/config/config_nu.rs
vendored
@ -1,6 +1,7 @@
|
|||||||
use super::utils::gen_command;
|
|
||||||
use nu_cmd_base::util::get_editor;
|
use nu_cmd_base::util::get_editor;
|
||||||
use nu_engine::{command_prelude::*, env_to_strings};
|
use nu_engine::{command_prelude::*, env_to_strings};
|
||||||
|
use nu_protocol::{process::ChildProcess, ByteStream};
|
||||||
|
use nu_system::ForegroundChild;
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ConfigNu;
|
pub struct ConfigNu;
|
||||||
@ -51,7 +52,7 @@ impl Command for ConfigNu {
|
|||||||
engine_state: &EngineState,
|
engine_state: &EngineState,
|
||||||
stack: &mut Stack,
|
stack: &mut Stack,
|
||||||
call: &Call,
|
call: &Call,
|
||||||
input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
// `--default` flag handling
|
// `--default` flag handling
|
||||||
if call.has_flag(engine_state, stack, "default")? {
|
if call.has_flag(engine_state, stack, "default")? {
|
||||||
@ -59,10 +60,18 @@ impl Command for ConfigNu {
|
|||||||
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
return Ok(Value::string(nu_utils::get_default_config(), head).into_pipeline_data());
|
||||||
}
|
}
|
||||||
|
|
||||||
let env_vars_str = env_to_strings(engine_state, stack)?;
|
// Find the editor executable.
|
||||||
let nu_config = match engine_state.get_config_path("config-path") {
|
let (editor_name, editor_args) = get_editor(engine_state, stack, call.head)?;
|
||||||
Some(path) => path,
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
None => {
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
|
let editor_executable =
|
||||||
|
crate::which(&editor_name, &paths, &cwd).ok_or(ShellError::ExternalCommand {
|
||||||
|
label: format!("`{editor_name}` not found"),
|
||||||
|
help: "Failed to find the editor executable".into(),
|
||||||
|
span: call.head,
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let Some(config_path) = engine_state.get_config_path("config-path") else {
|
||||||
return Err(ShellError::GenericError {
|
return Err(ShellError::GenericError {
|
||||||
error: "Could not find $nu.config-path".into(),
|
error: "Could not find $nu.config-path".into(),
|
||||||
msg: "Could not find $nu.config-path".into(),
|
msg: "Could not find $nu.config-path".into(),
|
||||||
@ -70,16 +79,40 @@ impl Command for ConfigNu {
|
|||||||
help: None,
|
help: None,
|
||||||
inner: vec![],
|
inner: vec![],
|
||||||
});
|
});
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
let config_path = config_path.to_string_lossy().to_string();
|
||||||
|
|
||||||
let (item, config_args) = get_editor(engine_state, stack, call.head)?;
|
// Create the command.
|
||||||
|
let mut command = std::process::Command::new(editor_executable);
|
||||||
|
|
||||||
gen_command(call.head, nu_config, item, config_args, env_vars_str).run_with_input(
|
// Configure PWD.
|
||||||
engine_state,
|
command.current_dir(cwd);
|
||||||
stack,
|
|
||||||
input,
|
// Configure environment variables.
|
||||||
true,
|
let envs = env_to_strings(engine_state, stack)?;
|
||||||
)
|
command.env_clear();
|
||||||
|
command.envs(envs);
|
||||||
|
|
||||||
|
// Configure args.
|
||||||
|
command.arg(config_path);
|
||||||
|
command.args(editor_args);
|
||||||
|
|
||||||
|
// 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.pipeline_externals_state,
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Wrap the output into a `PipelineData::ByteStream`.
|
||||||
|
let child = ChildProcess::new(child, None, false, call.head)?;
|
||||||
|
Ok(PipelineData::ByteStream(
|
||||||
|
ByteStream::child(child, call.head),
|
||||||
|
None,
|
||||||
|
))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
1
crates/nu-command/src/env/config/mod.rs
vendored
1
crates/nu-command/src/env/config/mod.rs
vendored
@ -2,7 +2,6 @@ mod config_;
|
|||||||
mod config_env;
|
mod config_env;
|
||||||
mod config_nu;
|
mod config_nu;
|
||||||
mod config_reset;
|
mod config_reset;
|
||||||
mod utils;
|
|
||||||
pub use config_::ConfigMeta;
|
pub use config_::ConfigMeta;
|
||||||
pub use config_env::ConfigEnv;
|
pub use config_env::ConfigEnv;
|
||||||
pub use config_nu::ConfigNu;
|
pub use config_nu::ConfigNu;
|
||||||
|
36
crates/nu-command/src/env/config/utils.rs
vendored
36
crates/nu-command/src/env/config/utils.rs
vendored
@ -1,36 +0,0 @@
|
|||||||
use crate::ExternalCommand;
|
|
||||||
use nu_protocol::{OutDest, Span, Spanned};
|
|
||||||
use std::{collections::HashMap, path::Path};
|
|
||||||
|
|
||||||
pub(crate) fn gen_command(
|
|
||||||
span: Span,
|
|
||||||
config_path: &Path,
|
|
||||||
item: String,
|
|
||||||
config_args: Vec<String>,
|
|
||||||
env_vars_str: HashMap<String, String>,
|
|
||||||
) -> ExternalCommand {
|
|
||||||
let name = Spanned { item, span };
|
|
||||||
|
|
||||||
let mut args = vec![Spanned {
|
|
||||||
item: config_path.to_string_lossy().to_string(),
|
|
||||||
span: Span::unknown(),
|
|
||||||
}];
|
|
||||||
|
|
||||||
let number_of_args = config_args.len() + 1;
|
|
||||||
|
|
||||||
for arg in config_args {
|
|
||||||
args.push(Spanned {
|
|
||||||
item: arg,
|
|
||||||
span: Span::unknown(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
ExternalCommand {
|
|
||||||
name,
|
|
||||||
args,
|
|
||||||
arg_keep_raw: vec![false; number_of_args],
|
|
||||||
out: OutDest::Inherit,
|
|
||||||
err: OutDest::Inherit,
|
|
||||||
env_vars: env_vars_str,
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,7 +1,4 @@
|
|||||||
use super::run_external::create_external_command;
|
use nu_engine::{command_prelude::*, env_to_strings};
|
||||||
#[allow(deprecated)]
|
|
||||||
use nu_engine::{command_prelude::*, current_dir};
|
|
||||||
use nu_protocol::OutDest;
|
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct Exec;
|
pub struct Exec;
|
||||||
@ -35,7 +32,66 @@ On Windows based systems, Nushell will wait for the command to finish and then e
|
|||||||
call: &Call,
|
call: &Call,
|
||||||
_input: PipelineData,
|
_input: PipelineData,
|
||||||
) -> Result<PipelineData, ShellError> {
|
) -> Result<PipelineData, ShellError> {
|
||||||
exec(engine_state, stack, call)
|
let cwd = engine_state.cwd(Some(stack))?;
|
||||||
|
|
||||||
|
// Find the absolute path to the executable. If the command is not
|
||||||
|
// found, display a helpful error message.
|
||||||
|
let name: Spanned<String> = call.req(engine_state, stack, 0)?;
|
||||||
|
let executable = {
|
||||||
|
let paths = nu_engine::env::path_str(engine_state, stack, call.head)?;
|
||||||
|
let Some(executable) = crate::which(&name.item, &paths, &cwd) else {
|
||||||
|
return Err(crate::command_not_found(
|
||||||
|
&name.item,
|
||||||
|
call.head,
|
||||||
|
engine_state,
|
||||||
|
stack,
|
||||||
|
));
|
||||||
|
};
|
||||||
|
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 = crate::eval_arguments_from_call(engine_state, stack, call)?;
|
||||||
|
command.args(args.into_iter().map(|s| s.item));
|
||||||
|
|
||||||
|
// Execute the child process, replacing/terminating the current process
|
||||||
|
// depending on platform.
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::process::CommandExt;
|
||||||
|
|
||||||
|
let err = command.exec();
|
||||||
|
Err(ShellError::ExternalCommand {
|
||||||
|
label: "Failed to exec into new process".into(),
|
||||||
|
help: err.to_string(),
|
||||||
|
span: call.head,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
#[cfg(windows)]
|
||||||
|
{
|
||||||
|
let mut child = command.spawn().map_err(|err| ShellError::ExternalCommand {
|
||||||
|
label: "Failed to exec into new process".into(),
|
||||||
|
help: err.to_string(),
|
||||||
|
span: call.head,
|
||||||
|
})?;
|
||||||
|
let status = child.wait().map_err(|err| ShellError::ExternalCommand {
|
||||||
|
label: "Failed to wait for child process".into(),
|
||||||
|
help: err.to_string(),
|
||||||
|
span: call.head,
|
||||||
|
})?;
|
||||||
|
std::process::exit(status.code().expect("status.code() succeeds on Windows"))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn examples(&self) -> Vec<Example> {
|
fn examples(&self) -> Vec<Example> {
|
||||||
@ -53,57 +109,3 @@ On Windows based systems, Nushell will wait for the command to finish and then e
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn exec(
|
|
||||||
engine_state: &EngineState,
|
|
||||||
stack: &mut Stack,
|
|
||||||
call: &Call,
|
|
||||||
) -> Result<PipelineData, ShellError> {
|
|
||||||
let mut external_command = create_external_command(engine_state, stack, call)?;
|
|
||||||
external_command.out = OutDest::Inherit;
|
|
||||||
external_command.err = OutDest::Inherit;
|
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
let cwd = current_dir(engine_state, stack)?;
|
|
||||||
let mut command = external_command.spawn_simple_command(&cwd.to_string_lossy())?;
|
|
||||||
command.current_dir(cwd);
|
|
||||||
command.envs(external_command.env_vars);
|
|
||||||
|
|
||||||
// this either replaces our process and should not return,
|
|
||||||
// or the exec fails and we get an error back
|
|
||||||
exec_impl(command, call.head)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
fn exec_impl(mut command: std::process::Command, span: Span) -> Result<PipelineData, ShellError> {
|
|
||||||
use std::os::unix::process::CommandExt;
|
|
||||||
|
|
||||||
let error = command.exec();
|
|
||||||
|
|
||||||
Err(ShellError::GenericError {
|
|
||||||
error: "Error on exec".into(),
|
|
||||||
msg: error.to_string(),
|
|
||||||
span: Some(span),
|
|
||||||
help: None,
|
|
||||||
inner: vec![],
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(windows)]
|
|
||||||
fn exec_impl(mut command: std::process::Command, span: Span) -> Result<PipelineData, ShellError> {
|
|
||||||
match command.spawn() {
|
|
||||||
Ok(mut child) => match child.wait() {
|
|
||||||
Ok(status) => std::process::exit(status.code().unwrap_or(0)),
|
|
||||||
Err(e) => Err(ShellError::ExternalCommand {
|
|
||||||
label: "Error in external command".into(),
|
|
||||||
help: e.to_string(),
|
|
||||||
span,
|
|
||||||
}),
|
|
||||||
},
|
|
||||||
Err(e) => Err(ShellError::ExternalCommand {
|
|
||||||
label: "Error spawning external command".into(),
|
|
||||||
help: e.to_string(),
|
|
||||||
span,
|
|
||||||
}),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -33,7 +33,7 @@ pub use nu_check::NuCheck;
|
|||||||
pub use ps::Ps;
|
pub use ps::Ps;
|
||||||
#[cfg(windows)]
|
#[cfg(windows)]
|
||||||
pub use registry_query::RegistryQuery;
|
pub use registry_query::RegistryQuery;
|
||||||
pub use run_external::{External, ExternalCommand};
|
pub use run_external::{command_not_found, eval_arguments_from_call, which, External};
|
||||||
pub use sys::*;
|
pub use sys::*;
|
||||||
pub use uname::UName;
|
pub use uname::UName;
|
||||||
pub use which_::Which;
|
pub use which_::Which;
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -55,7 +55,9 @@ fn checks_if_all_returns_error_with_invalid_command() {
|
|||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(actual.err.contains("can't run executable") || actual.err.contains("did you mean"));
|
assert!(
|
||||||
|
actual.err.contains("Command `st` not found") && actual.err.contains("Did you mean `ast`?")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -41,7 +41,9 @@ fn checks_if_any_returns_error_with_invalid_command() {
|
|||||||
"#
|
"#
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(actual.err.contains("can't run executable") || actual.err.contains("did you mean"));
|
assert!(
|
||||||
|
actual.err.contains("Command `st` not found") && actual.err.contains("Did you mean `ast`?")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -24,7 +24,7 @@ fn basic_exit_code() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn error() {
|
fn error() {
|
||||||
let actual = nu!("not-found | complete");
|
let actual = nu!("not-found | complete");
|
||||||
assert!(actual.err.contains("executable was not found"));
|
assert!(actual.err.contains("Command `not-found` not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -283,13 +283,17 @@ fn source_env_is_scoped() {
|
|||||||
|
|
||||||
let actual = nu!(cwd: dirs.test(), &inp.join("; "));
|
let actual = nu!(cwd: dirs.test(), &inp.join("; "));
|
||||||
|
|
||||||
assert!(actual.err.contains("executable was not found"));
|
assert!(actual
|
||||||
|
.err
|
||||||
|
.contains("Command `no-name-similar-to-this` not found"));
|
||||||
|
|
||||||
let inp = &[r#"source-env spam.nu"#, r#"nor-similar-to-this"#];
|
let inp = &[r#"source-env spam.nu"#, r#"nor-similar-to-this"#];
|
||||||
|
|
||||||
let actual = nu!(cwd: dirs.test(), &inp.join("; "));
|
let actual = nu!(cwd: dirs.test(), &inp.join("; "));
|
||||||
|
|
||||||
assert!(actual.err.contains("executable was not found"));
|
assert!(actual
|
||||||
|
.err
|
||||||
|
.contains("Command `nor-similar-to-this` not found"));
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -189,9 +189,7 @@ fn use_module_creates_accurate_did_you_mean_1() {
|
|||||||
let actual = nu!(r#"
|
let actual = nu!(r#"
|
||||||
module spam { export def foo [] { "foo" } }; use spam; foo
|
module spam { export def foo [] { "foo" } }; use spam; foo
|
||||||
"#);
|
"#);
|
||||||
assert!(actual.err.contains(
|
assert!(actual.err.contains("Did you mean `spam foo`"));
|
||||||
"command 'foo' was not found but it was imported from module 'spam'; try using `spam foo`"
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -199,9 +197,9 @@ fn use_module_creates_accurate_did_you_mean_2() {
|
|||||||
let actual = nu!(r#"
|
let actual = nu!(r#"
|
||||||
module spam { export def foo [] { "foo" } }; foo
|
module spam { export def foo [] { "foo" } }; foo
|
||||||
"#);
|
"#);
|
||||||
assert!(actual.err.contains(
|
assert!(actual
|
||||||
"command 'foo' was not found but it exists in module 'spam'; try importing it with `use`"
|
.err
|
||||||
));
|
.contains("A command with that name exists in module `spam`"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -242,6 +242,7 @@ pub fn nu_repl() {
|
|||||||
let mut top_stack = Arc::new(Stack::new());
|
let mut top_stack = Arc::new(Stack::new());
|
||||||
|
|
||||||
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
engine_state.add_env_var("PWD".into(), Value::test_string(cwd.to_string_lossy()));
|
||||||
|
engine_state.add_env_var("PATH".into(), Value::test_string(""));
|
||||||
|
|
||||||
let mut last_output = String::new();
|
let mut last_output = String::new();
|
||||||
|
|
||||||
|
@ -108,14 +108,14 @@ fn known_external_misc_values() -> TestResult {
|
|||||||
/// GitHub issue #7822
|
/// GitHub issue #7822
|
||||||
#[test]
|
#[test]
|
||||||
fn known_external_subcommand_from_module() -> TestResult {
|
fn known_external_subcommand_from_module() -> TestResult {
|
||||||
let output = Command::new("cargo").arg("check").arg("-h").output()?;
|
let output = Command::new("cargo").arg("add").arg("-h").output()?;
|
||||||
run_test(
|
run_test(
|
||||||
r#"
|
r#"
|
||||||
module cargo {
|
module cargo {
|
||||||
export extern check []
|
export extern add []
|
||||||
};
|
};
|
||||||
use cargo;
|
use cargo;
|
||||||
cargo check -h
|
cargo add -h
|
||||||
"#,
|
"#,
|
||||||
String::from_utf8(output.stdout)?.trim(),
|
String::from_utf8(output.stdout)?.trim(),
|
||||||
)
|
)
|
||||||
@ -124,14 +124,14 @@ fn known_external_subcommand_from_module() -> TestResult {
|
|||||||
/// GitHub issue #7822
|
/// GitHub issue #7822
|
||||||
#[test]
|
#[test]
|
||||||
fn known_external_aliased_subcommand_from_module() -> TestResult {
|
fn known_external_aliased_subcommand_from_module() -> TestResult {
|
||||||
let output = Command::new("cargo").arg("check").arg("-h").output()?;
|
let output = Command::new("cargo").arg("add").arg("-h").output()?;
|
||||||
run_test(
|
run_test(
|
||||||
r#"
|
r#"
|
||||||
module cargo {
|
module cargo {
|
||||||
export extern check []
|
export extern add []
|
||||||
};
|
};
|
||||||
use cargo;
|
use cargo;
|
||||||
alias cc = cargo check;
|
alias cc = cargo add;
|
||||||
cc -h
|
cc -h
|
||||||
"#,
|
"#,
|
||||||
String::from_utf8(output.stdout)?.trim(),
|
String::from_utf8(output.stdout)?.trim(),
|
||||||
|
@ -647,7 +647,7 @@ fn duration_with_underscores_2() -> TestResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn duration_with_underscores_3() -> TestResult {
|
fn duration_with_underscores_3() -> TestResult {
|
||||||
fail_test("1_000_d_ay", "executable was not found")
|
fail_test("1_000_d_ay", "Command `1_000_d_ay` not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -667,7 +667,7 @@ fn filesize_with_underscores_2() -> TestResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn filesize_with_underscores_3() -> TestResult {
|
fn filesize_with_underscores_3() -> TestResult {
|
||||||
fail_test("42m_b", "executable was not found")
|
fail_test("42m_b", "Command `42m_b` not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -183,17 +183,14 @@ fn explain_spread_args() -> TestResult {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn disallow_implicit_spread_for_externals() -> TestResult {
|
fn disallow_implicit_spread_for_externals() -> TestResult {
|
||||||
fail_test(
|
fail_test(r#"^echo [1 2]"#, "Lists are not automatically spread")
|
||||||
r#"nu --testbin cococo [1 2]"#,
|
|
||||||
"Lists are not automatically spread",
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn respect_shape() -> TestResult {
|
fn respect_shape() -> TestResult {
|
||||||
fail_test(
|
fail_test(
|
||||||
"def foo [...rest] { ...$rest }; foo bar baz",
|
"def foo [...rest] { ...$rest }; foo bar baz",
|
||||||
"executable was not found",
|
"Command `...$rest` not found",
|
||||||
)
|
)
|
||||||
.unwrap();
|
.unwrap();
|
||||||
fail_test("module foo { ...$bar }", "expected_keyword").unwrap();
|
fail_test("module foo { ...$bar }", "expected_keyword").unwrap();
|
||||||
|
@ -72,7 +72,9 @@ fn correctly_escape_external_arguments() {
|
|||||||
fn escape_also_escapes_equals() {
|
fn escape_also_escapes_equals() {
|
||||||
let actual = nu!("^MYFOONAME=MYBARVALUE");
|
let actual = nu!("^MYFOONAME=MYBARVALUE");
|
||||||
|
|
||||||
assert!(actual.err.contains("executable was not found"));
|
assert!(actual
|
||||||
|
.err
|
||||||
|
.contains("Command `MYFOONAME=MYBARVALUE` not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -127,7 +129,7 @@ fn command_not_found_error_shows_not_found_1() {
|
|||||||
export extern "foo" [];
|
export extern "foo" [];
|
||||||
foo
|
foo
|
||||||
"#);
|
"#);
|
||||||
assert!(actual.err.contains("'foo' was not found"));
|
assert!(actual.err.contains("Command `foo` not found"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -989,7 +989,9 @@ fn hide_alias_hides_alias() {
|
|||||||
"
|
"
|
||||||
));
|
));
|
||||||
|
|
||||||
assert!(actual.err.contains("did you mean 'all'?"));
|
assert!(
|
||||||
|
actual.err.contains("Command `ll` not found") && actual.err.contains("Did you mean `all`?")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
mod parse {
|
mod parse {
|
||||||
@ -1035,7 +1037,7 @@ mod parse {
|
|||||||
fn ensure_backticks_are_bareword_command() {
|
fn ensure_backticks_are_bareword_command() {
|
||||||
let actual = nu!("`8abc123`");
|
let actual = nu!("`8abc123`");
|
||||||
|
|
||||||
assert!(actual.err.contains("was not found"),);
|
assert!(actual.err.contains("Command `8abc123` not found"),);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1146,5 +1148,8 @@ fn command_not_found_error_shows_not_found_2() {
|
|||||||
export def --wrapped my-foo [...rest] { foo };
|
export def --wrapped my-foo [...rest] { foo };
|
||||||
my-foo
|
my-foo
|
||||||
"#);
|
"#);
|
||||||
assert!(actual.err.contains("did you mean"));
|
assert!(
|
||||||
|
actual.err.contains("Command `foo` not found")
|
||||||
|
&& actual.err.contains("Did you mean `for`?")
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user