mirror of
https://github.com/nushell/nushell
synced 2024-12-28 14:03:09 +00:00
58b96fdede
this adds a `--no-std-lib` flag. Moves `nu!` to use the `--no-std-lib`. Adds a new `nu_with_std!` macro for future tests that need the std-lib. # 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.)_ # User-Facing Changes _(List of all changes that impact the user experience here. This helps us keep track of breaking changes.)_ # 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 -A clippy::needless_collect` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass - `cargo run -- crates/nu-std/tests/run.nu` 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.
472 lines
15 KiB
Rust
472 lines
15 KiB
Rust
/// Run a command in nu and get its output
|
|
///
|
|
/// The `nu!` macro accepts a number of options like the `cwd` in which the
|
|
/// command should be run. It is also possible to specify a different `locale`
|
|
/// to test locale dependent commands.
|
|
///
|
|
/// Pass options as the first arguments in the form of `key_1: value_1, key_1:
|
|
/// value_2, ...`. The options are defined in the `NuOpts` struct inside the
|
|
/// `nu!` macro.
|
|
///
|
|
/// The command can be formatted using `{}` just like `println!` or `format!`.
|
|
/// Pass the format arguments comma separated after the command itself.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```no_run
|
|
/// # // NOTE: The `nu!` macro needs the `nu` binary to exist. The test are
|
|
/// # // therefore only compiled but not run (that's what the `no_run` at
|
|
/// # // the beginning of this code block is for).
|
|
/// #
|
|
/// use nu_test_support::nu;
|
|
///
|
|
/// let outcome = nu!(
|
|
/// "date now | date to-record | get year"
|
|
/// );
|
|
///
|
|
/// let dir = "/";
|
|
/// let outcome = nu!(
|
|
/// "ls {} | get name",
|
|
/// dir,
|
|
/// );
|
|
///
|
|
/// let outcome = nu!(
|
|
/// cwd: "/",
|
|
/// "ls | get name",
|
|
/// );
|
|
///
|
|
/// let cell = "size";
|
|
/// let outcome = nu!(
|
|
/// locale: "de_DE.UTF-8",
|
|
/// "ls | into int {}",
|
|
/// cell,
|
|
/// );
|
|
///
|
|
/// let decimals = 2;
|
|
/// let outcome = nu!(
|
|
/// locale: "de_DE.UTF-8",
|
|
/// "10 | into string --decimals {}",
|
|
/// decimals,
|
|
/// );
|
|
/// ```
|
|
#[macro_export]
|
|
macro_rules! nu {
|
|
// In the `@options` phase, we restucture all the
|
|
// `$field_1: $value_1, $field_2: $value_2, ...`
|
|
// pairs to a structure like
|
|
// `@options[ $field_1 => $value_1 ; $field_2 => $value_2 ; ... ]`.
|
|
// We do this to later distinguish the options from the `$path` and `$part`s.
|
|
// (See
|
|
// https://users.rust-lang.org/t/i-dont-think-this-local-ambiguity-when-calling-macro-is-ambiguous/79401?u=x3ro
|
|
// )
|
|
//
|
|
// If there is any special treatment needed for the `$value`, we can just
|
|
// match for the specific `field` name.
|
|
(
|
|
@options [ $($options:tt)* ]
|
|
cwd: $value:expr,
|
|
$($rest:tt)*
|
|
) => {
|
|
nu!(@options [ $($options)* cwd => $crate::fs::in_directory($value) ; ] $($rest)*)
|
|
};
|
|
// For all other options, we call `.into()` on the `$value` and hope for the best. ;)
|
|
(
|
|
@options [ $($options:tt)* ]
|
|
$field:ident : $value:expr,
|
|
$($rest:tt)*
|
|
) => {
|
|
nu!(@options [ $($options)* $field => $value.into() ; ] $($rest)*)
|
|
};
|
|
|
|
// When the `$field: $value,` pairs are all parsed, the next tokens are the `$path` and any
|
|
// number of `$part`s, potentially followed by a trailing comma.
|
|
(
|
|
@options [ $($options:tt)* ]
|
|
$path:expr
|
|
$(, $part:expr)*
|
|
$(,)*
|
|
) => {{
|
|
// Here we parse the options into a `NuOpts` struct
|
|
let opts = nu!(@nu_opts $($options)*);
|
|
// and format the `$path` using the `$part`s
|
|
let path = nu!(@format_path $path, $($part),*);
|
|
// Then finally we go to the `@main` phase, where the actual work is done.
|
|
nu!(@main opts, path)
|
|
}};
|
|
|
|
// Create the NuOpts struct from the `field => value ;` pairs
|
|
(@nu_opts $( $field:ident => $value:expr ; )*) => {
|
|
NuOpts{
|
|
$(
|
|
$field: Some($value),
|
|
)*
|
|
..Default::default()
|
|
}
|
|
};
|
|
|
|
// Helper to format `$path`.
|
|
(@format_path $path:expr $(,)?) => {
|
|
// When there are no `$part`s, do not format anything
|
|
$path
|
|
};
|
|
(@format_path $path:expr, $($part:expr),* $(,)?) => {{
|
|
format!($path, $( $part ),*)
|
|
}};
|
|
|
|
// Do the actual work.
|
|
(@main $opts:expr, $path:expr) => {{
|
|
pub use std::error::Error;
|
|
pub use std::io::prelude::*;
|
|
pub use std::process::{Command, Stdio};
|
|
pub use $crate::NATIVE_PATH_ENV_VAR;
|
|
|
|
pub fn escape_quote_string(input: String) -> String {
|
|
let mut output = String::with_capacity(input.len() + 2);
|
|
output.push('"');
|
|
|
|
for c in input.chars() {
|
|
if c == '"' || c == '\\' {
|
|
output.push('\\');
|
|
}
|
|
output.push(c);
|
|
}
|
|
|
|
output.push('"');
|
|
output
|
|
}
|
|
|
|
let test_bins = $crate::fs::binaries();
|
|
|
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
|
let test_bins = $crate::nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| {
|
|
panic!(
|
|
"Couldn't canonicalize dummy binaries path {}: {:?}",
|
|
test_bins.display(),
|
|
e
|
|
)
|
|
});
|
|
|
|
let mut paths = $crate::shell_os_paths();
|
|
paths.insert(0, test_bins);
|
|
|
|
let path = $path.lines().collect::<Vec<_>>().join("; ");
|
|
|
|
let paths_joined = match std::env::join_paths(paths) {
|
|
Ok(all) => all,
|
|
Err(_) => panic!("Couldn't join paths for PATH var."),
|
|
};
|
|
|
|
let target_cwd = $opts.cwd.unwrap_or(".".to_string());
|
|
let locale = $opts.locale.unwrap_or("en_US.UTF-8".to_string());
|
|
|
|
let mut command = Command::new($crate::fs::executable_path());
|
|
command
|
|
.env("PWD", &target_cwd)
|
|
.env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale)
|
|
.current_dir(target_cwd)
|
|
.env(NATIVE_PATH_ENV_VAR, paths_joined)
|
|
// .arg("--skip-plugins")
|
|
// .arg("--no-history")
|
|
// .arg("--config-file")
|
|
// .arg($crate::fs::DisplayPath::display_path(&$crate::fs::fixtures().join("playground/config/default.toml")))
|
|
.arg("--no-std-lib")
|
|
.arg(format!("-c {}", escape_quote_string(path)))
|
|
.stdout(Stdio::piped())
|
|
// .stdin(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut process = match command.spawn()
|
|
{
|
|
Ok(child) => child,
|
|
Err(why) => panic!("Can't run test {:?} {}", $crate::fs::executable_path(), why.to_string()),
|
|
};
|
|
|
|
// let stdin = process.stdin.as_mut().expect("couldn't open stdin");
|
|
// stdin
|
|
// .write_all(b"exit\n")
|
|
// .expect("couldn't write to stdin");
|
|
|
|
let output = process
|
|
.wait_with_output()
|
|
.expect("couldn't read from stdout/stderr");
|
|
|
|
let out = $crate::macros::read_std(&output.stdout);
|
|
let err = String::from_utf8_lossy(&output.stderr);
|
|
|
|
println!("=== stderr\n{}", err);
|
|
|
|
$crate::Outcome::new(out,err.into_owned())
|
|
}};
|
|
|
|
// This is the entrypoint for this macro.
|
|
($($token:tt)*) => {{
|
|
#[derive(Default)]
|
|
struct NuOpts {
|
|
cwd: Option<String>,
|
|
locale: Option<String>,
|
|
}
|
|
|
|
nu!(@options [ ] $($token)*)
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! nu_with_std {
|
|
// In the `@options` phase, we restucture all the
|
|
// `$field_1: $value_1, $field_2: $value_2, ...`
|
|
// pairs to a structure like
|
|
// `@options[ $field_1 => $value_1 ; $field_2 => $value_2 ; ... ]`.
|
|
// We do this to later distinguish the options from the `$path` and `$part`s.
|
|
// (See
|
|
// https://users.rust-lang.org/t/i-dont-think-this-local-ambiguity-when-calling-macro-is-ambiguous/79401?u=x3ro
|
|
// )
|
|
//
|
|
// If there is any special treatment needed for the `$value`, we can just
|
|
// match for the specific `field` name.
|
|
(
|
|
@options [ $($options:tt)* ]
|
|
cwd: $value:expr,
|
|
$($rest:tt)*
|
|
) => {
|
|
nu!(@options [ $($options)* cwd => $crate::fs::in_directory($value) ; ] $($rest)*)
|
|
};
|
|
// For all other options, we call `.into()` on the `$value` and hope for the best. ;)
|
|
(
|
|
@options [ $($options:tt)* ]
|
|
$field:ident : $value:expr,
|
|
$($rest:tt)*
|
|
) => {
|
|
nu!(@options [ $($options)* $field => $value.into() ; ] $($rest)*)
|
|
};
|
|
|
|
// When the `$field: $value,` pairs are all parsed, the next tokens are the `$path` and any
|
|
// number of `$part`s, potentially followed by a trailing comma.
|
|
(
|
|
@options [ $($options:tt)* ]
|
|
$path:expr
|
|
$(, $part:expr)*
|
|
$(,)*
|
|
) => {{
|
|
// Here we parse the options into a `NuOpts` struct
|
|
let opts = nu!(@nu_opts $($options)*);
|
|
// and format the `$path` using the `$part`s
|
|
let path = nu!(@format_path $path, $($part),*);
|
|
// Then finally we go to the `@main` phase, where the actual work is done.
|
|
nu!(@main opts, path)
|
|
}};
|
|
|
|
// Create the NuOpts struct from the `field => value ;` pairs
|
|
(@nu_opts $( $field:ident => $value:expr ; )*) => {
|
|
NuOpts{
|
|
$(
|
|
$field: Some($value),
|
|
)*
|
|
..Default::default()
|
|
}
|
|
};
|
|
|
|
// Helper to format `$path`.
|
|
(@format_path $path:expr $(,)?) => {
|
|
// When there are no `$part`s, do not format anything
|
|
$path
|
|
};
|
|
(@format_path $path:expr, $($part:expr),* $(,)?) => {{
|
|
format!($path, $( $part ),*)
|
|
}};
|
|
|
|
// Do the actual work.
|
|
(@main $opts:expr, $path:expr) => {{
|
|
pub use std::error::Error;
|
|
pub use std::io::prelude::*;
|
|
pub use std::process::{Command, Stdio};
|
|
pub use $crate::NATIVE_PATH_ENV_VAR;
|
|
|
|
pub fn escape_quote_string(input: String) -> String {
|
|
let mut output = String::with_capacity(input.len() + 2);
|
|
output.push('"');
|
|
|
|
for c in input.chars() {
|
|
if c == '"' || c == '\\' {
|
|
output.push('\\');
|
|
}
|
|
output.push(c);
|
|
}
|
|
|
|
output.push('"');
|
|
output
|
|
}
|
|
|
|
let test_bins = $crate::fs::binaries();
|
|
|
|
let cwd = std::env::current_dir().expect("Could not get current working directory.");
|
|
let test_bins = $crate::nu_path::canonicalize_with(&test_bins, cwd).unwrap_or_else(|e| {
|
|
panic!(
|
|
"Couldn't canonicalize dummy binaries path {}: {:?}",
|
|
test_bins.display(),
|
|
e
|
|
)
|
|
});
|
|
|
|
let mut paths = $crate::shell_os_paths();
|
|
paths.insert(0, test_bins);
|
|
|
|
let path = $path.lines().collect::<Vec<_>>().join("; ");
|
|
|
|
let paths_joined = match std::env::join_paths(paths) {
|
|
Ok(all) => all,
|
|
Err(_) => panic!("Couldn't join paths for PATH var."),
|
|
};
|
|
|
|
let target_cwd = $opts.cwd.unwrap_or(".".to_string());
|
|
let locale = $opts.locale.unwrap_or("en_US.UTF-8".to_string());
|
|
|
|
let mut command = Command::new($crate::fs::executable_path());
|
|
command
|
|
.env("PWD", &target_cwd)
|
|
.env(nu_utils::locale::LOCALE_OVERRIDE_ENV_VAR, locale)
|
|
.current_dir(target_cwd)
|
|
.env(NATIVE_PATH_ENV_VAR, paths_joined)
|
|
// .arg("--skip-plugins")
|
|
// .arg("--no-history")
|
|
// .arg("--config-file")
|
|
// .arg($crate::fs::DisplayPath::display_path(&$crate::fs::fixtures().join("playground/config/default.toml")))
|
|
.arg(format!("-c {}", escape_quote_string(path)))
|
|
.stdout(Stdio::piped())
|
|
// .stdin(Stdio::piped())
|
|
.stderr(Stdio::piped());
|
|
|
|
let mut process = match command.spawn()
|
|
{
|
|
Ok(child) => child,
|
|
Err(why) => panic!("Can't run test {:?} {}", $crate::fs::executable_path(), why.to_string()),
|
|
};
|
|
|
|
// let stdin = process.stdin.as_mut().expect("couldn't open stdin");
|
|
// stdin
|
|
// .write_all(b"exit\n")
|
|
// .expect("couldn't write to stdin");
|
|
|
|
let output = process
|
|
.wait_with_output()
|
|
.expect("couldn't read from stdout/stderr");
|
|
|
|
let out = $crate::macros::read_std(&output.stdout);
|
|
let err = String::from_utf8_lossy(&output.stderr);
|
|
|
|
println!("=== stderr\n{}", err);
|
|
|
|
$crate::Outcome::new(out,err.into_owned())
|
|
}};
|
|
|
|
// This is the entrypoint for this macro.
|
|
($($token:tt)*) => {{
|
|
#[derive(Default)]
|
|
struct NuOpts {
|
|
cwd: Option<String>,
|
|
locale: Option<String>,
|
|
}
|
|
|
|
nu!(@options [ ] $($token)*)
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! with_exe {
|
|
($name:literal) => {{
|
|
#[cfg(windows)]
|
|
{
|
|
concat!($name, ".exe")
|
|
}
|
|
#[cfg(not(windows))]
|
|
{
|
|
$name
|
|
}
|
|
}};
|
|
}
|
|
|
|
#[macro_export]
|
|
macro_rules! nu_with_plugins {
|
|
(cwd: $cwd:expr, plugins: [$(($plugin_name:expr)),+$(,)?], $command:expr) => {{
|
|
nu_with_plugins!($cwd, [$(("", $plugin_name)),+], $command)
|
|
}};
|
|
(cwd: $cwd:expr, plugin: ($plugin_name:expr), $command:expr) => {{
|
|
nu_with_plugins!($cwd, [("", $plugin_name)], $command)
|
|
}};
|
|
|
|
($cwd:expr, [$(($format:expr, $plugin_name:expr)),+$(,)?], $command:expr) => {{
|
|
pub use std::error::Error;
|
|
pub use std::io::prelude::*;
|
|
pub use std::process::{Command, Stdio};
|
|
pub use tempfile::tempdir;
|
|
pub use $crate::{NATIVE_PATH_ENV_VAR, with_exe};
|
|
|
|
let test_bins = $crate::fs::binaries();
|
|
let test_bins = nu_path::canonicalize_with(&test_bins, ".").unwrap_or_else(|e| {
|
|
panic!(
|
|
"Couldn't canonicalize dummy binaries path {}: {:?}",
|
|
test_bins.display(),
|
|
e
|
|
)
|
|
});
|
|
|
|
let temp = tempdir().expect("couldn't create a temporary directory");
|
|
let temp_plugin_file = temp.path().join("plugin.nu");
|
|
std::fs::File::create(&temp_plugin_file).expect("couldn't create temporary plugin file");
|
|
|
|
$crate::commands::ensure_plugins_built();
|
|
|
|
// TODO: the `$format` is a dummy empty string, but `plugin_name` is repeatable
|
|
// just keep it here for now. Need to find a way to remove it.
|
|
let registrations = format!(
|
|
concat!($(concat!("register ", $format, " {};")),+),
|
|
$(
|
|
nu_path::canonicalize_with(with_exe!($plugin_name), &test_bins)
|
|
.unwrap_or_else(|e| {
|
|
panic!("failed to canonicalize plugin {} path", $plugin_name)
|
|
})
|
|
.display()
|
|
),+
|
|
);
|
|
let commands = format!("{registrations}{}", $command);
|
|
|
|
let target_cwd = $crate::fs::in_directory(&$cwd);
|
|
// In plugin testing, we need to use installed nushell to drive
|
|
// plugin commands.
|
|
let mut executable_path = $crate::fs::executable_path();
|
|
if !executable_path.exists() {
|
|
executable_path = $crate::fs::installed_nu_path();
|
|
}
|
|
let mut process = match Command::new(executable_path)
|
|
.current_dir(&target_cwd)
|
|
.env("PWD", &target_cwd) // setting PWD is enough to set cwd
|
|
.arg("--commands")
|
|
.arg(commands)
|
|
.arg("--plugin-config")
|
|
.arg(temp_plugin_file)
|
|
.stdout(Stdio::piped())
|
|
.stderr(Stdio::piped())
|
|
.spawn()
|
|
{
|
|
Ok(child) => child,
|
|
Err(why) => panic!("Can't run test {}", why.to_string()),
|
|
};
|
|
|
|
let output = process
|
|
.wait_with_output()
|
|
.expect("couldn't read from stdout/stderr");
|
|
|
|
let out = $crate::macros::read_std(&output.stdout);
|
|
let err = String::from_utf8_lossy(&output.stderr);
|
|
|
|
println!("=== stderr\n{}", err);
|
|
|
|
$crate::Outcome::new(out, err.into_owned())
|
|
}};
|
|
}
|
|
|
|
pub fn read_std(std: &[u8]) -> String {
|
|
let out = String::from_utf8_lossy(std);
|
|
let out = out.lines().collect::<Vec<_>>().join("\n");
|
|
let out = out.replace("\r\n", "");
|
|
out.replace('\n', "")
|
|
}
|