diff --git a/crates/nu-cli/src/cli.rs b/crates/nu-cli/src/cli.rs index ed445cbdb2..d5c0e96e1d 100644 --- a/crates/nu-cli/src/cli.rs +++ b/crates/nu-cli/src/cli.rs @@ -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 diff --git a/crates/nu-cli/src/commands.rs b/crates/nu-cli/src/commands.rs index e9743f5342..4fcbebb53b 100644 --- a/crates/nu-cli/src/commands.rs +++ b/crates/nu-cli/src/commands.rs @@ -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; diff --git a/crates/nu-cli/src/commands/classified/internal.rs b/crates/nu-cli/src/commands/classified/internal.rs index bb243fb7a7..5bce7fdb0c 100644 --- a/crates/nu-cli/src/commands/classified/internal.rs +++ b/crates/nu-cli/src/commands/classified/internal.rs @@ -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![])) diff --git a/crates/nu-cli/src/commands/nu/mod.rs b/crates/nu-cli/src/commands/nu/mod.rs new file mode 100644 index 0000000000..bec8cebfe6 --- /dev/null +++ b/crates/nu-cli/src/commands/nu/mod.rs @@ -0,0 +1,3 @@ +mod plugin; + +pub use plugin::SubCommand as NuPlugin; diff --git a/crates/nu-cli/src/commands/nu/plugin.rs b/crates/nu-cli/src/commands/nu/plugin.rs new file mode 100644 index 0000000000..32b9bbbde8 --- /dev/null +++ b/crates/nu-cli/src/commands/nu/plugin.rs @@ -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>, +} + +#[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 { + 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 { + let shell_manager = args.shell_manager.clone(); + let (Arguments { load_path }, _) = args.process(®istry).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, ®istry)) + .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 {}) + } +} diff --git a/crates/nu-cli/src/lib.rs b/crates/nu-cli/src/lib.rs index fe93b2333e..0a8038ee38 100644 --- a/crates/nu-cli/src/lib.rs +++ b/crates/nu-cli/src/lib.rs @@ -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, diff --git a/crates/nu-cli/src/plugin.rs b/crates/nu-cli/src/plugin.rs index c325f1d9e9..0427959f38 100644 --- a/crates/nu-cli/src/plugin.rs +++ b/crates/nu-cli/src/plugin.rs @@ -93,7 +93,7 @@ pub fn build_plugin_command( result } -pub fn scan() -> Result, ShellError> { +pub fn scan(paths: Vec) -> Result, ShellError> { let mut plugins = vec![]; let opts = glob::MatchOptions { @@ -102,7 +102,7 @@ pub fn scan() -> Result, 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]*")); diff --git a/crates/nu-protocol/src/return_value.rs b/crates/nu-protocol/src/return_value.rs index 37cf0f8b62..1fad050644 100644 --- a/crates/nu-protocol/src/return_value.rs +++ b/crates/nu-protocol/src/return_value.rs @@ -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"), diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index 4e13dd9a9d..08e2e091bd 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -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::*; diff --git a/src/main.rs b/src/main.rs index 0f22ea8271..4d0ee73d7e 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,13 @@ fn main() -> Result<(), Box> { .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> { 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))?; } } diff --git a/tests/plugins/core_inc.rs b/tests/plugins/core_inc.rs index c92cd60c47..193bb09874 100644 --- a/tests/plugins/core_inc.rs +++ b/tests/plugins/core_inc.rs @@ -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" );