Core nu plugin load capability. (#2544)

We introduce the `plugin` nu sub command (`nu plugin`) with basic plugin
loading support. We can choose to load plugins from a directory. Originally
introduced to make integration tests faster (by not loading any plugins on startup at all)
but `nu plugin --load some_path ; test_pipeline_that_uses_plugins_just_loaded` does not see it.

Therefore, a `nu_with_plugins!` macro for tests was introduced on top of nu`s `--skip-plugins`
switch executable which is set to true when running the integration tests that use the `nu!` macro now..
This commit is contained in:
Andrés N. Robalino 2020-09-14 09:07:02 -05:00 committed by GitHub
parent e05e6b42fe
commit 0178b53289
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
11 changed files with 260 additions and 18 deletions

View File

@ -20,8 +20,8 @@ use std::iter::Iterator;
use std::path::{Path, PathBuf};
use std::sync::atomic::Ordering;
fn register_plugins(context: &mut Context) -> Result<(), ShellError> {
if let Ok(plugins) = crate::plugin::scan() {
pub fn register_plugins(context: &mut Context) -> Result<(), ShellError> {
if let Ok(plugins) = crate::plugin::scan(search_paths()) {
context.add_commands(
plugins
.into_iter()
@ -78,6 +78,7 @@ pub fn create_default_context(
use crate::commands::*;
context.add_commands(vec![
whole_stream_command(NuPlugin),
// System/file operations
whole_stream_command(Exec),
whole_stream_command(Pwd),
@ -568,8 +569,6 @@ pub async fn cli(
let configuration = nu_data::config::NuConfig::new();
let history_path = crate::commands::history::history_path(&configuration);
let _ = register_plugins(&mut context);
let (mut rl, config) = create_rustyline_configuration();
// we are ok if history does not exist

View File

@ -76,6 +76,7 @@ pub(crate) mod mkdir;
pub(crate) mod move_;
pub(crate) mod next;
pub(crate) mod nth;
pub(crate) mod nu;
pub(crate) mod open;
pub(crate) mod parse;
pub(crate) mod path;
@ -160,6 +161,7 @@ pub(crate) use each::EachWindow;
pub(crate) use echo::Echo;
pub(crate) use if_::If;
pub(crate) use is_empty::IsEmpty;
pub(crate) use nu::NuPlugin;
pub(crate) use update::Update;
pub(crate) mod kill;
pub(crate) use kill::Kill;

View File

@ -200,6 +200,28 @@ pub(crate) async fn run_internal_command(
)]);
InputStream::from_stream(futures::stream::iter(vec![]))
}
CommandAction::AddPlugins(path) => {
match crate::plugin::scan(vec![std::path::PathBuf::from(path)]) {
Ok(plugins) => {
context.add_commands(
plugins
.into_iter()
.filter(|p| {
!context.is_command_registered(p.name())
})
.collect(),
);
InputStream::from_stream(futures::stream::iter(vec![]))
}
Err(reason) => {
context.error(reason.clone());
InputStream::one(
UntaggedValue::Error(reason).into_untagged_value(),
)
}
}
}
CommandAction::PreviousShell => {
context.shell_manager.prev();
InputStream::from_stream(futures::stream::iter(vec![]))

View File

@ -0,0 +1,3 @@
mod plugin;
pub use plugin::SubCommand as NuPlugin;

View File

@ -0,0 +1,124 @@
use std::path::PathBuf;
use crate::commands::WholeStreamCommand;
use crate::context::CommandRegistry;
use crate::path::canonicalize;
use crate::prelude::*;
use nu_errors::ShellError;
use nu_protocol::{CommandAction, ReturnSuccess, Signature, SyntaxShape, UntaggedValue};
use nu_source::Tagged;
#[cfg(unix)]
use std::os::unix::fs::PermissionsExt;
pub struct SubCommand;
#[derive(Deserialize)]
pub struct Arguments {
#[serde(rename = "load")]
pub load_path: Option<Tagged<PathBuf>>,
}
#[async_trait]
impl WholeStreamCommand for SubCommand {
fn name(&self) -> &str {
"nu plugin"
}
fn signature(&self) -> Signature {
Signature::build("nu plugin").named(
"load",
SyntaxShape::Path,
"a path to load the plugins from",
Some('l'),
)
}
fn usage(&self) -> &str {
"Nu Plugin"
}
fn examples(&self) -> Vec<Example> {
vec![Example {
description: "Load all plugins in the current directory",
example: "nu plugin --load .",
result: Some(vec![]),
}]
}
async fn run(
&self,
args: CommandArgs,
registry: &CommandRegistry,
) -> Result<OutputStream, ShellError> {
let shell_manager = args.shell_manager.clone();
let (Arguments { load_path }, _) = args.process(&registry).await?;
if let Some(Tagged {
item: load_path,
tag,
}) = load_path
{
let path = canonicalize(shell_manager.path(), load_path).map_err(|_| {
ShellError::labeled_error(
"Cannot load plugins from directory",
"directory not found",
&tag,
)
})?;
if !path.is_dir() {
return Err(ShellError::labeled_error(
"Cannot load plugins from directory",
"is not a directory",
&tag,
));
}
#[cfg(unix)]
{
let has_exec = path
.metadata()
.map(|m| umask::Mode::from(m.permissions().mode()).has(umask::USER_READ))
.map_err(|e| {
ShellError::labeled_error(
"Cannot load plugins from directory",
format!("cannot stat ({})", e),
&tag,
)
})?;
if !has_exec {
return Err(ShellError::labeled_error(
"Cannot load plugins from directory",
"permission denied",
&tag,
));
}
}
return Ok(vec![ReturnSuccess::action(CommandAction::AddPlugins(
path.to_string_lossy().to_string(),
))]
.into());
}
Ok(OutputStream::one(ReturnSuccess::value(
UntaggedValue::string(crate::commands::help::get_help(&SubCommand, &registry))
.into_value(Tag::unknown()),
)))
}
}
#[cfg(test)]
mod tests {
use super::SubCommand;
#[test]
fn examples_work_as_expected() {
use crate::examples::test as test_examples;
test_examples(SubCommand {})
}
}

View File

@ -35,8 +35,8 @@ pub mod utils;
mod examples;
pub use crate::cli::{
cli, create_default_context, parse_and_eval, process_line, run_pipeline_standalone,
run_vec_of_pipelines, LineResult,
cli, create_default_context, parse_and_eval, process_line, register_plugins,
run_pipeline_standalone, run_vec_of_pipelines, LineResult,
};
pub use crate::commands::command::{
whole_stream_command, CommandArgs, EvaluatedWholeStreamCommandArgs, Example, WholeStreamCommand,

View File

@ -93,7 +93,7 @@ pub fn build_plugin_command(
result
}
pub fn scan() -> Result<Vec<crate::commands::Command>, ShellError> {
pub fn scan(paths: Vec<std::path::PathBuf>) -> Result<Vec<crate::commands::Command>, ShellError> {
let mut plugins = vec![];
let opts = glob::MatchOptions {
@ -102,7 +102,7 @@ pub fn scan() -> Result<Vec<crate::commands::Command>, ShellError> {
require_literal_leading_dot: false,
};
for path in crate::cli::search_paths() {
for path in paths {
let mut pattern = path.to_path_buf();
pattern.push(std::path::Path::new("nu_plugin_[a-z0-9][a-z0-9]*"));

View File

@ -22,8 +22,10 @@ pub enum CommandAction {
EnterValueShell(Value),
/// Enter the help shell, which allows exploring the help system
EnterHelpShell(Value),
/// Enter the help shell, which allows exploring the help system
/// Add an alias command
AddAlias(String, Vec<(String, SyntaxShape)>, Block),
/// Add plugins from path given
AddPlugins(String),
/// Go to the previous shell in the shell ring buffer
PreviousShell,
/// Go to the next shell in the shell ring buffer
@ -46,6 +48,7 @@ impl PrettyDebug for CommandAction {
CommandAction::EnterValueShell(v) => b::typed("enter value shell", v.pretty()),
CommandAction::EnterHelpShell(v) => b::typed("enter help shell", v.pretty()),
CommandAction::AddAlias(..) => b::description("add alias"),
CommandAction::AddPlugins(..) => b::description("add plugins"),
CommandAction::PreviousShell => b::description("previous shell"),
CommandAction::NextShell => b::description("next shell"),
CommandAction::LeaveShell => b::description("leave shell"),

View File

@ -14,6 +14,83 @@ macro_rules! nu {
nu!($cwd, $path)
}};
($cwd:expr, $path:expr) => {{
pub use std::error::Error;
pub use std::io::prelude::*;
pub use std::process::{Command, Stdio};
let commands = &*format!(
"
cd \"{}\"
{}
exit",
$crate::fs::in_directory($cwd),
$crate::fs::DisplayPath::display_path(&$path)
);
let test_bins = $crate::fs::binaries();
let test_bins = dunce::canonicalize(&test_bins).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 paths_joined = match std::env::join_paths(paths.iter()) {
Ok(all) => all,
Err(_) => panic!("Couldn't join paths for PATH var."),
};
let mut process = match Command::new($crate::fs::executable_path())
.env("PATH", paths_joined)
.arg("--skip-plugins")
.stdout(Stdio::piped())
.stdin(Stdio::piped())
.stderr(Stdio::piped())
.spawn()
{
Ok(child) => child,
Err(why) => panic!("Can't run test {}", why.to_string()),
};
let stdin = process.stdin.as_mut().expect("couldn't open stdin");
stdin
.write_all(commands.as_bytes())
.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::macros::Outcome::new(out,err.into_owned())
}};
}
#[macro_export]
macro_rules! nu_with_plugins {
(cwd: $cwd:expr, $path:expr, $($part:expr),*) => {{
use $crate::fs::DisplayPath;
let path = format!($path, $(
$part.display_path()
),*);
nu_with_plugins!($cwd, &path)
}};
(cwd: $cwd:expr, $path:expr) => {{
nu_with_plugins!($cwd, $path)
}};
($cwd:expr, $path:expr) => {{
pub use std::error::Error;
pub use std::io::prelude::*;

View File

@ -17,6 +17,13 @@ fn main() -> Result<(), Box<dyn Error>> {
.possible_values(&["error", "warn", "info", "debug", "trace"])
.takes_value(true),
)
.arg(
Arg::with_name("skip-plugins")
.hidden(true)
.long("skip-plugins")
.multiple(false)
.takes_value(false),
)
.arg(
Arg::with_name("testbin")
.hidden(true)
@ -154,7 +161,12 @@ fn main() -> Result<(), Box<dyn Error>> {
None => {
let mut syncer = EnvironmentSyncer::new();
let context = create_default_context(&mut syncer, true)?;
let mut context = create_default_context(&mut syncer, true)?;
if !matches.is_present("skip-plugins") {
let _ = nu_cli::register_plugins(&mut context);
}
futures::executor::block_on(nu_cli::cli(syncer, context))?;
}
}

View File

@ -1,10 +1,10 @@
use nu_test_support::fs::Stub::FileWithContent;
use nu_test_support::nu;
use nu_test_support::nu_with_plugins;
use nu_test_support::playground::Playground;
#[test]
fn can_only_apply_one() {
let actual = nu!(
let actual = nu_with_plugins!(
cwd: "tests/fixtures/formats",
"open cargo_sample.toml | first 1 | inc package.version --major --minor"
);
@ -25,7 +25,7 @@ fn by_one_with_field_passed() {
"#,
)]);
let actual = nu!(
let actual = nu_with_plugins!(
cwd: dirs.test(),
"open sample.toml | inc package.edition | get package.edition | echo $it"
);
@ -45,7 +45,7 @@ fn by_one_with_no_field_passed() {
"#,
)]);
let actual = nu!(
let actual = nu_with_plugins!(
cwd: dirs.test(),
"open sample.toml | get package.contributors | inc | echo $it"
);
@ -65,7 +65,7 @@ fn semversion_major_inc() {
"#,
)]);
let actual = nu!(
let actual = nu_with_plugins!(
cwd: dirs.test(),
"open sample.toml | inc package.version -M | get package.version | echo $it"
);
@ -85,7 +85,7 @@ fn semversion_minor_inc() {
"#,
)]);
let actual = nu!(
let actual = nu_with_plugins!(
cwd: dirs.test(),
"open sample.toml | inc package.version --minor | get package.version | echo $it"
);
@ -105,7 +105,7 @@ fn semversion_patch_inc() {
"#,
)]);
let actual = nu!(
let actual = nu_with_plugins!(
cwd: dirs.test(),
"open sample.toml | inc package.version --patch | get package.version | echo $it"
);
@ -125,7 +125,7 @@ fn semversion_without_passing_field() {
"#,
)]);
let actual = nu!(
let actual = nu_with_plugins!(
cwd: dirs.test(),
"open sample.toml | get package.version | inc --patch | echo $it"
);