diff --git a/crates/nu-cmd-plugin/src/commands/plugin/add.rs b/crates/nu-cmd-plugin/src/commands/plugin/add.rs index d615ed8de6..150b007eeb 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/add.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/add.rs @@ -54,7 +54,7 @@ apparent the next time `nu` is next launched with that plugin cache file. } fn search_terms(&self) -> Vec<&str> { - vec!["plugin", "add", "register", "load", "signature"] + vec!["load", "register", "signature"] } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-plugin/src/commands/plugin/list.rs b/crates/nu-cmd-plugin/src/commands/plugin/list.rs index dfdb4bb3ee..94b53ef6d4 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/list.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/list.rs @@ -29,6 +29,10 @@ impl Command for PluginList { "List installed plugins." } + fn search_terms(&self) -> Vec<&str> { + vec!["scope"] + } + fn examples(&self) -> Vec { vec![ Example { diff --git a/crates/nu-cmd-plugin/src/commands/plugin/mod.rs b/crates/nu-cmd-plugin/src/commands/plugin/mod.rs index 7ec2d77731..14a5f32ee5 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/mod.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/mod.rs @@ -4,11 +4,13 @@ mod add; mod list; mod rm; mod stop; +mod use_; pub use add::PluginAdd; pub use list::PluginList; pub use rm::PluginRm; pub use stop::PluginStop; +pub use use_::PluginUse; #[derive(Clone)] pub struct PluginCommand; @@ -28,10 +30,6 @@ impl Command for PluginCommand { "Commands for managing plugins." } - fn extra_usage(&self) -> &str { - "To load a plugin, see `register`." - } - fn run( &self, engine_state: &EngineState, @@ -54,6 +52,20 @@ impl Command for PluginCommand { fn examples(&self) -> Vec { vec![ + Example { + example: "plugin add nu_plugin_inc", + description: "Run the `nu_plugin_inc` plugin from the current directory and install its signatures.", + result: None, + }, + Example { + example: "plugin use inc", + description: " +Load (or reload) the `inc` plugin from the plugin cache file and put its commands in scope. +The plugin must already be in the cache file at parse time. +" + .trim(), + result: None, + }, Example { example: "plugin list", description: "List installed plugins", @@ -64,11 +76,6 @@ impl Command for PluginCommand { description: "Stop the plugin named `inc`.", result: None, }, - Example { - example: "plugin add nu_plugin_inc", - description: "Run the `nu_plugin_inc` plugin from the current directory and install its signatures.", - result: None, - }, Example { example: "plugin rm inc", description: "Remove the installed signatures for the `inc` plugin.", diff --git a/crates/nu-cmd-plugin/src/commands/plugin/rm.rs b/crates/nu-cmd-plugin/src/commands/plugin/rm.rs index 9845c35342..f2ed58bd9b 100644 --- a/crates/nu-cmd-plugin/src/commands/plugin/rm.rs +++ b/crates/nu-cmd-plugin/src/commands/plugin/rm.rs @@ -51,7 +51,7 @@ fixed with `plugin add`. } fn search_terms(&self) -> Vec<&str> { - vec!["plugin", "rm", "remove", "delete", "signature"] + vec!["remove", "delete", "signature"] } fn examples(&self) -> Vec { diff --git a/crates/nu-cmd-plugin/src/commands/plugin/use_.rs b/crates/nu-cmd-plugin/src/commands/plugin/use_.rs new file mode 100644 index 0000000000..9959b326ce --- /dev/null +++ b/crates/nu-cmd-plugin/src/commands/plugin/use_.rs @@ -0,0 +1,81 @@ +use nu_engine::command_prelude::*; + +#[derive(Clone)] +pub struct PluginUse; + +impl Command for PluginUse { + fn name(&self) -> &str { + "plugin use" + } + + fn usage(&self) -> &str { + "Load a plugin from the plugin cache file into scope." + } + + fn signature(&self) -> nu_protocol::Signature { + Signature::build(self.name()) + .input_output_types(vec![(Type::Nothing, Type::Nothing)]) + .named( + "plugin-config", + SyntaxShape::Filepath, + "Use a plugin cache file other than the one set in `$nu.plugin-path`", + None, + ) + .required( + "name", + SyntaxShape::String, + "The name of the plugin to load (not the filename)", + ) + .category(Category::Plugin) + } + + fn extra_usage(&self) -> &str { + r#" +This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html + +The plugin definition must be available in the plugin cache file at parse time. +Run `plugin add` first in the REPL to do this, or from a script consider +preparing a plugin cache file and passing `--plugin-config`, or using the +`--plugin` option to `nu` instead. + +If the plugin was already loaded, this will reload the latest definition from +the cache file into scope. +"# + .trim() + } + + fn search_terms(&self) -> Vec<&str> { + vec!["add", "register", "scope"] + } + + fn is_parser_keyword(&self) -> bool { + true + } + + fn run( + &self, + _engine_state: &EngineState, + _stack: &mut Stack, + _call: &Call, + _input: PipelineData, + ) -> Result { + Ok(PipelineData::empty()) + } + + fn examples(&self) -> Vec { + vec![ + Example { + description: "Load the commands for the `query` plugin from $nu.plugin-path", + example: r#"plugin use query"#, + result: None, + }, + Example { + description: + "Load the commands for the `query` plugin from a custom plugin cache file", + example: r#"plugin use --plugin-config local-plugins.msgpackz query"#, + result: None, + }, + ] + } +} diff --git a/crates/nu-cmd-plugin/src/commands/register.rs b/crates/nu-cmd-plugin/src/commands/register.rs index df0fe62361..924ab00d62 100644 --- a/crates/nu-cmd-plugin/src/commands/register.rs +++ b/crates/nu-cmd-plugin/src/commands/register.rs @@ -35,12 +35,17 @@ impl Command for Register { } fn extra_usage(&self) -> &str { - r#"This command is a parser keyword. For details, check: - https://www.nushell.sh/book/thinking_in_nu.html"# + r#" +Deprecated in favor of `plugin add` and `plugin use`. + +This command is a parser keyword. For details, check: + https://www.nushell.sh/book/thinking_in_nu.html +"# + .trim() } fn search_terms(&self) -> Vec<&str> { - vec!["plugin", "add", "register"] + vec!["add"] } fn is_parser_keyword(&self) -> bool { diff --git a/crates/nu-cmd-plugin/src/default_context.rs b/crates/nu-cmd-plugin/src/default_context.rs index 32f48ad037..601dd52cfc 100644 --- a/crates/nu-cmd-plugin/src/default_context.rs +++ b/crates/nu-cmd-plugin/src/default_context.rs @@ -12,11 +12,12 @@ pub fn add_plugin_command_context(mut engine_state: EngineState) -> EngineState } bind_command!( - PluginCommand, PluginAdd, + PluginCommand, PluginList, PluginRm, PluginStop, + PluginUse, Register, ); diff --git a/crates/nu-command/tests/format_conversions/nuon.rs b/crates/nu-command/tests/format_conversions/nuon.rs index 7fd3854cdf..8262ee8dfc 100644 --- a/crates/nu-command/tests/format_conversions/nuon.rs +++ b/crates/nu-command/tests/format_conversions/nuon.rs @@ -479,5 +479,5 @@ fn read_code_should_fail_rather_than_panic() { let actual = nu!(cwd: "tests/fixtures/formats", pipeline( r#"open code.nu | from nuon"# )); - assert!(actual.err.contains("error when parsing")) + assert!(actual.err.contains("Error when loading")) } diff --git a/crates/nu-parser/src/parse_keywords.rs b/crates/nu-parser/src/parse_keywords.rs index 84121e5aee..300012807c 100644 --- a/crates/nu-parser/src/parse_keywords.rs +++ b/crates/nu-parser/src/parse_keywords.rs @@ -71,6 +71,7 @@ pub const UNALIASABLE_PARSER_KEYWORDS: &[&[u8]] = &[ b"source", b"where", b"register", + b"plugin use", ]; /// Check whether spans start with a parser keyword that can be aliased @@ -93,11 +94,14 @@ pub fn is_unaliasable_parser_keyword(working_set: &StateWorkingSet, spans: &[Spa /// This is a new more compact method of calling parse_xxx() functions without repeating the /// parse_call() in each function. Remaining keywords can be moved here. pub fn parse_keyword(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline { + let orig_parse_errors_len = working_set.parse_errors.len(); + let call_expr = parse_call(working_set, &lite_command.parts, lite_command.parts[0]); - // if err.is_some() { - // return (Pipeline::from_vec(vec![call_expr]), err); - // } + // If an error occurred, don't invoke the keyword-specific functionality + if working_set.parse_errors.len() > orig_parse_errors_len { + return Pipeline::from_vec(vec![call_expr]); + } if let Expression { expr: Expr::Call(call), @@ -121,6 +125,8 @@ pub fn parse_keyword(working_set: &mut StateWorkingSet, lite_command: &LiteComma "overlay hide" => parse_overlay_hide(working_set, call), "overlay new" => parse_overlay_new(working_set, call), "overlay use" => parse_overlay_use(working_set, call), + #[cfg(feature = "plugin")] + "plugin use" => parse_plugin_use(working_set, call), _ => Pipeline::from_vec(vec![call_expr]), } } else { @@ -1946,7 +1952,7 @@ pub fn parse_module_file_or_dir( let cwd = working_set.get_cwd(); let module_path = - if let Some(path) = find_in_dirs(&module_path_str, working_set, &cwd, LIB_DIRS_VAR) { + if let Some(path) = find_in_dirs(&module_path_str, working_set, &cwd, Some(LIB_DIRS_VAR)) { path } else { working_set.error(ParseError::ModuleNotFound(path_span, module_path_str)); @@ -3402,7 +3408,7 @@ pub fn parse_source(working_set: &mut StateWorkingSet, lite_command: &LiteComman } }; - if let Some(path) = find_in_dirs(&filename, working_set, &cwd, LIB_DIRS_VAR) { + if let Some(path) = find_in_dirs(&filename, working_set, &cwd, Some(LIB_DIRS_VAR)) { if let Some(contents) = path.read(working_set) { // Add the file to the stack of files being processed. if let Err(e) = working_set.files.push(path.clone().path_buf(), spans[1]) { @@ -3546,11 +3552,13 @@ pub fn parse_where(working_set: &mut StateWorkingSet, lite_command: &LiteCommand } } +/// `register` is deprecated and will be removed in 0.94. Use `plugin add` and `plugin use` instead. #[cfg(feature = "plugin")] pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteCommand) -> Pipeline { - use nu_plugin::{get_signature, PersistentPlugin, PluginDeclaration}; + use nu_plugin::{get_signature, PluginDeclaration}; use nu_protocol::{ - engine::Stack, ErrSpan, PluginCacheItem, PluginIdentity, PluginSignature, RegisteredPlugin, + engine::Stack, ErrSpan, ParseWarning, PluginCacheItem, PluginIdentity, PluginSignature, + RegisteredPlugin, }; let spans = &lite_command.parts; @@ -3561,7 +3569,7 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm // Maybe this is not necessary but it is a sanity check if working_set.get_span_contents(spans[0]) != b"register" { working_set.error(ParseError::UnknownState( - "internal error: Wrong call name for parse plugin function".into(), + "internal error: Wrong call name for 'register' function".into(), span(spans), )); return garbage_pipeline(spans); @@ -3609,6 +3617,16 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm } }; + // Now that the call is parsed, add the deprecation warning + working_set + .parse_warnings + .push(ParseWarning::DeprecatedWarning { + old_command: "register".into(), + new_suggestion: "use `plugin add` and `plugin use`".into(), + span: call.head, + url: "https://www.nushell.sh/book/plugins.html".into(), + }); + // Extracting the required arguments from the call and keeping them together in a tuple let arguments = call .positional_nth(0) @@ -3619,7 +3637,8 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm .coerce_into_string() .map_err(|err| err.wrap(working_set, call.head))?; - let Some(path) = find_in_dirs(&filename, working_set, &cwd, PLUGIN_DIRS_VAR) else { + let Some(path) = find_in_dirs(&filename, working_set, &cwd, Some(PLUGIN_DIRS_VAR)) + else { return Err(ParseError::RegisteredFileNotFound(filename, expr.span)); }; @@ -3695,27 +3714,8 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm // Create the plugin identity. This validates that the plugin name starts with `nu_plugin_` let identity = PluginIdentity::new(path, shell).err_span(path_span)?; - // Find garbage collection config - let gc_config = working_set - .get_config() - .plugin_gc - .get(identity.name()) - .clone(); - - // Add it to the working set - let plugin = working_set.find_or_create_plugin(&identity, || { - Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone())) - }); - - // Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed. The - // trait object only exists so that nu-protocol can contain plugins without knowing anything - // about their implementation, but we only use `PersistentPlugin` in practice. - let plugin: Arc = plugin.as_any().downcast().map_err(|_| { - ParseError::InternalError( - "encountered unexpected RegisteredPlugin type".into(), - spans[0], - ) - })?; + let plugin = nu_plugin::add_plugin_to_working_set(working_set, &identity) + .map_err(|err| err.wrap(working_set, call.head))?; let signatures = signature.map_or_else( || { @@ -3731,8 +3731,6 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm ) })?; - plugin.set_gc_config(&gc_config); - let signatures = get_signature(plugin.clone(), get_envs).map_err(|err| { log::warn!("Error getting signatures: {err:?}"); ParseError::LabeledError( @@ -3776,6 +3774,100 @@ pub fn parse_register(working_set: &mut StateWorkingSet, lite_command: &LiteComm }]) } +#[cfg(feature = "plugin")] +pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box) -> Pipeline { + use nu_protocol::{FromValue, PluginCacheFile}; + + let cwd = working_set.get_cwd(); + + if let Err(err) = (|| { + let name = call + .positional_nth(0) + .map(|expr| { + eval_constant(working_set, expr) + .and_then(Spanned::::from_value) + .map_err(|err| err.wrap(working_set, call.head)) + }) + .expect("required positional should have been checked")?; + + let plugin_config = call + .named_iter() + .find(|(arg_name, _, _)| arg_name.item == "plugin-config") + .map(|(_, _, expr)| { + let expr = expr + .as_ref() + .expect("--plugin-config arg should have been checked already"); + eval_constant(working_set, expr) + .and_then(Spanned::::from_value) + .map_err(|err| err.wrap(working_set, call.head)) + }) + .transpose()?; + + // Find the actual plugin config path location. We don't have a const/env variable for this, + // it either lives in the current working directory or in the script's directory + let plugin_config_path = if let Some(custom_path) = &plugin_config { + find_in_dirs(&custom_path.item, working_set, &cwd, None).ok_or_else(|| { + ParseError::FileNotFound(custom_path.item.clone(), custom_path.span) + })? + } else { + ParserPath::RealPath( + working_set + .permanent_state + .plugin_path + .as_ref() + .ok_or_else(|| ParseError::LabeledErrorWithHelp { + error: "Plugin cache file not set".into(), + label: "can't load plugin without cache file".into(), + span: call.head, + help: + "pass --plugin-config to `plugin use` when $nu.plugin-path is not set" + .into(), + })? + .to_owned(), + ) + }; + + let file = plugin_config_path.open(working_set).map_err(|err| { + ParseError::LabeledError( + "Plugin cache file can't be opened".into(), + err.to_string(), + plugin_config.as_ref().map(|p| p.span).unwrap_or(call.head), + ) + })?; + + // The file is now open, so we just have to parse the contents and find the plugin + let contents = PluginCacheFile::read_from(file, Some(call.head)) + .map_err(|err| err.wrap(working_set, call.head))?; + + let plugin_item = contents + .plugins + .iter() + .find(|plugin| plugin.name == name.item) + .ok_or_else(|| ParseError::PluginNotFound { + name: name.item.clone(), + name_span: name.span, + plugin_config_span: plugin_config.as_ref().map(|p| p.span), + })?; + + // Now add the signatures to the working set + nu_plugin::load_plugin_cache_item(working_set, plugin_item, Some(call.head)) + .map_err(|err| err.wrap(working_set, call.head))?; + + Ok(()) + })() { + working_set.error(err); + } + + let call_span = call.span(); + + Pipeline::from_vec(vec![Expression { + expr: Expr::Call(call), + span: call_span, + ty: Type::Nothing, + custom_completion: None, + }]) +} + pub fn find_dirs_var(working_set: &StateWorkingSet, var_name: &str) -> Option { working_set .find_variable(format!("${}", var_name).as_bytes()) @@ -3799,13 +3891,13 @@ pub fn find_in_dirs( filename: &str, working_set: &StateWorkingSet, cwd: &str, - dirs_var_name: &str, + dirs_var_name: Option<&str>, ) -> Option { pub fn find_in_dirs_with_id( filename: &str, working_set: &StateWorkingSet, cwd: &str, - dirs_var_name: &str, + dirs_var_name: Option<&str>, ) -> Option { // Choose whether to use file-relative or PWD-relative path let actual_cwd = working_set @@ -3845,8 +3937,10 @@ pub fn find_in_dirs( } // Look up relative path from NU_LIB_DIRS - working_set - .get_variable(find_dirs_var(working_set, dirs_var_name)?) + dirs_var_name + .as_ref() + .and_then(|dirs_var_name| find_dirs_var(working_set, dirs_var_name)) + .map(|var_id| working_set.get_variable(var_id))? .const_val .as_ref()? .as_list() @@ -3868,7 +3962,7 @@ pub fn find_in_dirs( filename: &str, working_set: &StateWorkingSet, cwd: &str, - dirs_env: &str, + dirs_env: Option<&str>, ) -> Option { // Choose whether to use file-relative or PWD-relative path let actual_cwd = working_set @@ -3882,7 +3976,9 @@ pub fn find_in_dirs( let path = Path::new(filename); if path.is_relative() { - if let Some(lib_dirs) = working_set.get_env_var(dirs_env) { + if let Some(lib_dirs) = + dirs_env.and_then(|dirs_env| working_set.get_env_var(dirs_env)) + { if let Ok(dirs) = lib_dirs.as_list() { for lib_dir in dirs { if let Ok(dir) = lib_dir.to_path() { diff --git a/crates/nu-parser/src/parser.rs b/crates/nu-parser/src/parser.rs index ff171c07be..af34b6e23a 100644 --- a/crates/nu-parser/src/parser.rs +++ b/crates/nu-parser/src/parser.rs @@ -5149,12 +5149,24 @@ pub fn parse_expression(working_set: &mut StateWorkingSet, spans: &[Span]) -> Ex #[cfg(feature = "plugin")] b"register" => { working_set.error(ParseError::BuiltinCommandInPipeline( - "plugin".into(), + "register".into(), spans[0], )); parse_call(working_set, &spans[pos..], spans[0]) } + #[cfg(feature = "plugin")] + b"plugin" => { + if spans.len() > 1 && working_set.get_span_contents(spans[1]) == b"use" { + // only 'plugin use' is banned + working_set.error(ParseError::BuiltinCommandInPipeline( + "plugin use".into(), + spans[0], + )); + } + + parse_call(working_set, &spans[pos..], spans[0]) + } _ => parse_call(working_set, &spans[pos..], spans[0]), } @@ -5286,6 +5298,20 @@ pub fn parse_builtin_commands( b"where" => parse_where(working_set, lite_command), #[cfg(feature = "plugin")] b"register" => parse_register(working_set, lite_command), + // Only "plugin use" is a keyword + #[cfg(feature = "plugin")] + b"plugin" + if lite_command + .parts + .get(1) + .is_some_and(|span| working_set.get_span_contents(*span) == b"use") => + { + if let Some(redirection) = lite_command.redirection.as_ref() { + working_set.error(redirecting_builtin_error("plugin use", redirection)); + return garbage_pipeline(&lite_command.parts); + } + parse_keyword(working_set, lite_command) + } _ => { let element = parse_pipeline_element(working_set, lite_command); diff --git a/crates/nu-parser/src/parser_path.rs b/crates/nu-parser/src/parser_path.rs index 6d5cd7b3cf..2d0fbce2a2 100644 --- a/crates/nu-parser/src/parser_path.rs +++ b/crates/nu-parser/src/parser_path.rs @@ -103,17 +103,33 @@ impl ParserPath { } } - pub fn read<'a>(&'a self, working_set: &'a StateWorkingSet) -> Option> { + pub fn open<'a>( + &'a self, + working_set: &'a StateWorkingSet, + ) -> std::io::Result> { match self { - ParserPath::RealPath(p) => std::fs::read(p).ok(), + ParserPath::RealPath(p) => { + std::fs::File::open(p).map(|f| Box::new(f) as Box) + } ParserPath::VirtualFile(_, file_id) => working_set .get_contents_of_file(*file_id) - .map(|bytes| bytes.to_vec()), + .map(|bytes| Box::new(bytes) as Box) + .ok_or(std::io::ErrorKind::NotFound.into()), - ParserPath::VirtualDir(..) => None, + ParserPath::VirtualDir(..) => Err(std::io::ErrorKind::NotFound.into()), } } + pub fn read<'a>(&'a self, working_set: &'a StateWorkingSet) -> Option> { + self.open(working_set) + .and_then(|mut reader| { + let mut vec = vec![]; + reader.read_to_end(&mut vec)?; + Ok(vec) + }) + .ok() + } + pub fn from_virtual_path( working_set: &StateWorkingSet, name: &str, diff --git a/crates/nu-plugin/src/lib.rs b/crates/nu-plugin/src/lib.rs index 7ed3b67004..b5ecd57b61 100644 --- a/crates/nu-plugin/src/lib.rs +++ b/crates/nu-plugin/src/lib.rs @@ -78,10 +78,11 @@ pub use serializers::{json::JsonSerializer, msgpack::MsgPackSerializer}; // Used by other nu crates. #[doc(hidden)] pub use plugin::{ - create_plugin_signature, get_signature, load_plugin_cache_item, load_plugin_file, - serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface, InterfaceManager, - PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext, PluginExecutionContext, - PluginInterface, PluginInterfaceManager, PluginSource, ServePluginError, + add_plugin_to_working_set, create_plugin_signature, get_signature, load_plugin_cache_item, + load_plugin_file, serve_plugin_io, EngineInterfaceManager, GetPlugin, Interface, + InterfaceManager, PersistentPlugin, PluginDeclaration, PluginExecutionCommandContext, + PluginExecutionContext, PluginInterface, PluginInterfaceManager, PluginSource, + ServePluginError, }; #[doc(hidden)] pub use protocol::{PluginCustomValue, PluginInput, PluginOutput}; diff --git a/crates/nu-plugin/src/plugin/interface/plugin.rs b/crates/nu-plugin/src/plugin/interface/plugin.rs index 9e773665a0..a01bdd49f9 100644 --- a/crates/nu-plugin/src/plugin/interface/plugin.rs +++ b/crates/nu-plugin/src/plugin/interface/plugin.rs @@ -748,9 +748,9 @@ impl PluginInterface { PluginCall::CustomValueOp(val, _) => Some(val.span), }, help: Some(format!( - "the plugin may have experienced an error. Try registering the plugin again \ + "the plugin may have experienced an error. Try loading the plugin again \ with `{}`", - self.state.source.identity.register_command(), + self.state.source.identity.use_command(), )), inner: vec![], })?; diff --git a/crates/nu-plugin/src/plugin/mod.rs b/crates/nu-plugin/src/plugin/mod.rs index e59b64a38f..9dcc5f6a8a 100644 --- a/crates/nu-plugin/src/plugin/mod.rs +++ b/crates/nu-plugin/src/plugin/mod.rs @@ -25,7 +25,7 @@ use nu_engine::documentation::get_flags_section; use nu_protocol::{ ast::Operator, engine::StateWorkingSet, report_error_new, CustomValue, IntoSpanned, LabeledError, PipelineData, PluginCacheFile, PluginCacheItem, PluginCacheItemData, - PluginIdentity, PluginSignature, ShellError, Span, Spanned, Value, + PluginIdentity, PluginSignature, RegisteredPlugin, ShellError, Span, Spanned, Value, }; use thiserror::Error; @@ -942,7 +942,7 @@ pub fn load_plugin_cache_item( working_set: &mut StateWorkingSet, plugin: &PluginCacheItem, span: Option, -) -> Result<(), ShellError> { +) -> Result, ShellError> { let identity = PluginIdentity::new(plugin.filename.clone(), plugin.shell.clone()).map_err(|_| { ShellError::GenericError { @@ -960,39 +960,54 @@ pub fn load_plugin_cache_item( match &plugin.data { PluginCacheItemData::Valid { commands } => { - // Find garbage collection config for the plugin - let gc_config = working_set - .get_config() - .plugin_gc - .get(identity.name()) - .clone(); + let plugin = add_plugin_to_working_set(working_set, &identity)?; - // Add it to / get it from the working set - let plugin = working_set.find_or_create_plugin(&identity, || { - Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone())) - }); - - // Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed. - // The trait object only exists so that nu-protocol can contain plugins without knowing - // anything about their implementation, but we only use `PersistentPlugin` in practice. - let plugin: Arc = - plugin - .as_any() - .downcast() - .map_err(|_| ShellError::NushellFailed { - msg: "encountered unexpected RegisteredPlugin type".into(), - })?; + // Ensure that the plugin is reset. We're going to load new signatures, so we want to + // make sure the running plugin reflects those new signatures, and it's possible that it + // doesn't. + plugin.reset()?; // Create the declarations from the commands for signature in commands { let decl = PluginDeclaration::new(plugin.clone(), signature.clone()); working_set.add_decl(Box::new(decl)); } - Ok(()) + Ok(plugin) } PluginCacheItemData::Invalid => Err(ShellError::PluginCacheDataInvalid { plugin_name: identity.name().to_owned(), - register_command: identity.register_command(), + span, + add_command: identity.add_command(), }), } } + +#[doc(hidden)] +pub fn add_plugin_to_working_set( + working_set: &mut StateWorkingSet, + identity: &PluginIdentity, +) -> Result, ShellError> { + // Find garbage collection config for the plugin + let gc_config = working_set + .get_config() + .plugin_gc + .get(identity.name()) + .clone(); + + // Add it to / get it from the working set + let plugin = working_set.find_or_create_plugin(identity, || { + Arc::new(PersistentPlugin::new(identity.clone(), gc_config.clone())) + }); + + plugin.set_gc_config(&gc_config); + + // Downcast the plugin to `PersistentPlugin` - we generally expect this to succeed. + // The trait object only exists so that nu-protocol can contain plugins without knowing + // anything about their implementation, but we only use `PersistentPlugin` in practice. + plugin + .as_any() + .downcast() + .map_err(|_| ShellError::NushellFailed { + msg: "encountered unexpected RegisteredPlugin type".into(), + }) +} diff --git a/crates/nu-protocol/src/ast/expression.rs b/crates/nu-protocol/src/ast/expression.rs index eba8b24e14..3b98f4679e 100644 --- a/crates/nu-protocol/src/ast/expression.rs +++ b/crates/nu-protocol/src/ast/expression.rs @@ -108,6 +108,13 @@ impl Expression { } } + pub fn as_filepath(&self) -> Option<(String, bool)> { + match &self.expr { + Expr::Filepath(string, quoted) => Some((string.clone(), *quoted)), + _ => None, + } + } + pub fn as_import_pattern(&self) -> Option { match &self.expr { Expr::ImportPattern(pattern) => Some(*pattern.clone()), diff --git a/crates/nu-protocol/src/errors/parse_error.rs b/crates/nu-protocol/src/errors/parse_error.rs index d9928df58f..8f94bc5d52 100644 --- a/crates/nu-protocol/src/errors/parse_error.rs +++ b/crates/nu-protocol/src/errors/parse_error.rs @@ -439,6 +439,19 @@ pub enum ParseError { #[diagnostic(code(nu::parser::file_not_found))] FileNotFound(String, #[label("File not found: {0}")] Span), + #[error("Plugin not found")] + #[diagnostic( + code(nu::parser::plugin_not_found), + help("plugins need to be added to the plugin cache file before your script is run (see `plugin add`)"), + )] + PluginNotFound { + name: String, + #[label("Plugin not found: {name}")] + name_span: Span, + #[label("in this cache file")] + plugin_config_span: Option, + }, + #[error("Invalid literal")] // in . #[diagnostic()] InvalidLiteral(String, String, #[label("{0} in {1}")] Span), @@ -544,6 +557,7 @@ impl ParseError { ParseError::SourcedFileNotFound(_, s) => *s, ParseError::RegisteredFileNotFound(_, s) => *s, ParseError::FileNotFound(_, s) => *s, + ParseError::PluginNotFound { name_span, .. } => *name_span, ParseError::LabeledError(_, _, s) => *s, ParseError::ShellAndAnd(s) => *s, ParseError::ShellOrOr(s) => *s, diff --git a/crates/nu-protocol/src/errors/parse_warning.rs b/crates/nu-protocol/src/errors/parse_warning.rs index 1e1f969daa..0213d6889f 100644 --- a/crates/nu-protocol/src/errors/parse_warning.rs +++ b/crates/nu-protocol/src/errors/parse_warning.rs @@ -5,19 +5,21 @@ use thiserror::Error; #[derive(Clone, Debug, Error, Diagnostic, Serialize, Deserialize)] pub enum ParseWarning { - #[error("Deprecated: {0}")] - DeprecatedWarning( - String, - String, - #[label = "`{0}` is deprecated and will be removed in 0.90. Please use `{1}` instead, more info: https://www.nushell.sh/book/custom_commands.html"] - Span, - ), + #[error("Deprecated: {old_command}")] + #[diagnostic(help("for more info: {url}"))] + DeprecatedWarning { + old_command: String, + new_suggestion: String, + #[label("`{old_command}` is deprecated and will be removed in 0.94. Please {new_suggestion} instead")] + span: Span, + url: String, + }, } impl ParseWarning { pub fn span(&self) -> Span { match self { - ParseWarning::DeprecatedWarning(_, _, s) => *s, + ParseWarning::DeprecatedWarning { span, .. } => *span, } } } diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs index 61559232e5..51c857a252 100644 --- a/crates/nu-protocol/src/errors/shell_error.rs +++ b/crates/nu-protocol/src/errors/shell_error.rs @@ -750,17 +750,19 @@ pub enum ShellError { span: Span, }, - /// The cached plugin data (in `$nu.plugin-path`) for a plugin is invalid. + /// The cached plugin data for a plugin is invalid. /// /// ## Resolution /// - /// `register` the plugin again to update the data, or remove it. + /// `plugin add` the plugin again to update the data, or remove it with `plugin rm`. #[error("The cached plugin data for `{plugin_name}` is invalid")] #[diagnostic(code(nu::shell::plugin_cache_data_invalid))] PluginCacheDataInvalid { plugin_name: String, - #[help("try registering the plugin again with `{}`")] - register_command: String, + #[label("plugin `{plugin_name}` loaded here")] + span: Option, + #[help("the format in the plugin cache file is not compatible with this version of Nushell.\n\nTry adding the plugin again with `{}`")] + add_command: String, }, /// A plugin failed to load. diff --git a/crates/nu-protocol/src/plugin/identity.rs b/crates/nu-protocol/src/plugin/identity.rs index e969335497..c959e1526e 100644 --- a/crates/nu-protocol/src/plugin/identity.rs +++ b/crates/nu-protocol/src/plugin/identity.rs @@ -1,11 +1,10 @@ use std::path::{Path, PathBuf}; -use crate::{ParseError, Spanned}; +use crate::{ParseError, ShellError, Spanned}; -/// Error when an invalid plugin filename was encountered. This can be converted to [`ParseError`] -/// if a span is added. +/// Error when an invalid plugin filename was encountered. #[derive(Debug, Clone)] -pub struct InvalidPluginFilename; +pub struct InvalidPluginFilename(PathBuf); impl std::fmt::Display for InvalidPluginFilename { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -23,6 +22,18 @@ impl From> for ParseError { } } +impl From> for ShellError { + fn from(error: Spanned) -> ShellError { + ShellError::GenericError { + error: format!("Invalid plugin filename: {}", error.item.0.display()), + msg: "not a valid plugin filename".into(), + span: Some(error.span), + help: Some("valid Nushell plugin filenames must start with `nu_plugin_`".into()), + inner: vec![], + } + } +} + #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct PluginIdentity { /// The filename used to start the plugin @@ -35,17 +46,25 @@ pub struct PluginIdentity { impl PluginIdentity { /// Create a new plugin identity from a path to plugin executable and shell option. + /// + /// The `filename` must be an absolute path. Canonicalize before trying to construct the + /// [`PluginIdentity`]. pub fn new( filename: impl Into, shell: Option, ) -> Result { - let filename = filename.into(); + let filename: PathBuf = filename.into(); + + // Must pass absolute path. + if filename.is_relative() { + return Err(InvalidPluginFilename(filename)); + } let name = filename .file_stem() .map(|stem| stem.to_string_lossy().into_owned()) .and_then(|stem| stem.strip_prefix("nu_plugin_").map(|s| s.to_owned())) - .ok_or(InvalidPluginFilename)?; + .ok_or_else(|| InvalidPluginFilename(filename.clone()))?; Ok(PluginIdentity { filename, @@ -89,30 +108,42 @@ impl PluginIdentity { .expect("fake plugin identity path is invalid") } - /// A command that could be used to register the plugin, for suggesting in errors. - pub fn register_command(&self) -> String { + /// A command that could be used to add the plugin, for suggesting in errors. + pub fn add_command(&self) -> String { if let Some(shell) = self.shell() { format!( - "register --shell '{}' '{}'", + "plugin add --shell '{}' '{}'", shell.display(), self.filename().display(), ) } else { - format!("register '{}'", self.filename().display()) + format!("plugin add '{}'", self.filename().display()) } } + + /// A command that could be used to reload the plugin, for suggesting in errors. + pub fn use_command(&self) -> String { + format!("plugin use '{}'", self.name()) + } } #[test] fn parses_name_from_path() { assert_eq!("test", PluginIdentity::new_fake("test").name()); assert_eq!("test_2", PluginIdentity::new_fake("test_2").name()); + let absolute_path = if cfg!(windows) { + r"C:\path\to\nu_plugin_foo.sh" + } else { + "/path/to/nu_plugin_foo.sh" + }; assert_eq!( "foo", - PluginIdentity::new("nu_plugin_foo.sh", Some("sh".into())) + PluginIdentity::new(absolute_path, Some("sh".into())) .expect("should be valid") .name() ); + // Relative paths should be invalid + PluginIdentity::new("nu_plugin_foo.sh", Some("sh".into())).expect_err("should be invalid"); PluginIdentity::new("other", None).expect_err("should be invalid"); PluginIdentity::new("", None).expect_err("should be invalid"); } diff --git a/crates/nu-test-support/src/macros.rs b/crates/nu-test-support/src/macros.rs index 6d8b8a161f..1f8d797eb9 100644 --- a/crates/nu-test-support/src/macros.rs +++ b/crates/nu-test-support/src/macros.rs @@ -235,7 +235,6 @@ macro_rules! nu_with_plugins { use crate::{Outcome, NATIVE_PATH_ENV_VAR}; use std::ffi::OsStr; -use std::fmt::Write; use std::{ path::Path, process::{Command, Stdio}, @@ -340,17 +339,17 @@ where crate::commands::ensure_plugins_built(); - let registrations: String = plugins + let plugin_paths_quoted: Vec = plugins .iter() - .fold(String::new(), |mut output, plugin_name| { + .map(|plugin_name| { let plugin = with_exe(plugin_name); let plugin_path = nu_path::canonicalize_with(&plugin, &test_bins) .unwrap_or_else(|_| panic!("failed to canonicalize plugin {} path", &plugin)); let plugin_path = plugin_path.to_string_lossy(); - let _ = write!(output, "register {plugin_path};"); - output - }); - let commands = format!("{registrations}{command}"); + escape_quote_string(plugin_path.into_owned()) + }) + .collect(); + let plugins_arg = format!("[{}]", plugin_paths_quoted.join(",")); let target_cwd = crate::fs::in_directory(&cwd); // In plugin testing, we need to use installed nushell to drive @@ -362,13 +361,15 @@ where let process = match setup_command(&executable_path, &target_cwd) .envs(envs) .arg("--commands") - .arg(commands) + .arg(command) .arg("--config") .arg(temp_config_file) .arg("--env-config") .arg(temp_env_config_file) .arg("--plugin-config") .arg(temp_plugin_file) + .arg("--plugins") + .arg(plugins_arg) .stdout(Stdio::piped()) .stderr(Stdio::piped()) .spawn() diff --git a/src/command.rs b/src/command.rs index 60eb1568f3..f23907d179 100644 --- a/src/command.rs +++ b/src/command.rs @@ -36,6 +36,8 @@ pub(crate) fn gather_commandline_args() -> (Vec, String, Vec) { "--log-level" | "--log-target" | "--testbin" | "--threads" | "-t" | "--include-path" | "--lsp" | "--ide-goto-def" | "--ide-hover" | "--ide-complete" | "--ide-check" => args.next(), + #[cfg(feature = "plugin")] + "--plugins" => args.next(), _ => None, }; @@ -87,6 +89,8 @@ pub(crate) fn parse_commandline_args( let testbin = call.get_flag_expr("testbin"); #[cfg(feature = "plugin")] let plugin_file = call.get_flag_expr("plugin-config"); + #[cfg(feature = "plugin")] + let plugins = call.get_flag_expr("plugins"); let no_config_file = call.get_named_arg("no-config-file"); let no_history = call.get_named_arg("no-history"); let no_std_lib = call.get_named_arg("no-std-lib"); @@ -131,17 +135,60 @@ pub(crate) fn parse_commandline_args( } } + fn extract_path( + expression: Option<&Expression>, + ) -> Result>, ShellError> { + if let Some(expr) = expression { + let tuple = expr.as_filepath(); + if let Some((str, _)) = tuple { + Ok(Some(Spanned { + item: str, + span: expr.span, + })) + } else { + Err(ShellError::TypeMismatch { + err_message: "path".into(), + span: expr.span, + }) + } + } else { + Ok(None) + } + } + let commands = extract_contents(commands)?; let testbin = extract_contents(testbin)?; #[cfg(feature = "plugin")] - let plugin_file = extract_contents(plugin_file)?; - let config_file = extract_contents(config_file)?; - let env_file = extract_contents(env_file)?; + let plugin_file = extract_path(plugin_file)?; + let config_file = extract_path(config_file)?; + let env_file = extract_path(env_file)?; let log_level = extract_contents(log_level)?; let log_target = extract_contents(log_target)?; let execute = extract_contents(execute)?; let include_path = extract_contents(include_path)?; + #[cfg(feature = "plugin")] + let plugins = plugins + .map(|expr| match &expr.expr { + Expr::List(list) => list + .iter() + .map(|item| { + item.expr() + .as_filepath() + .map(|(s, _)| s.into_spanned(item.expr().span)) + .ok_or_else(|| ShellError::TypeMismatch { + err_message: "path".into(), + span: item.expr().span, + }) + }) + .collect::>, _>>(), + _ => Err(ShellError::TypeMismatch { + err_message: "list".into(), + span: expr.span, + }), + }) + .transpose()?; + let help = call.has_flag(engine_state, &mut stack, "help")?; if help { @@ -175,6 +222,8 @@ pub(crate) fn parse_commandline_args( testbin, #[cfg(feature = "plugin")] plugin_file, + #[cfg(feature = "plugin")] + plugins, no_config_file, no_history, no_std_lib, @@ -217,6 +266,8 @@ pub(crate) struct NushellCliArgs { pub(crate) testbin: Option>, #[cfg(feature = "plugin")] pub(crate) plugin_file: Option>, + #[cfg(feature = "plugin")] + pub(crate) plugins: Option>>, pub(crate) no_config_file: Option>, pub(crate) no_history: Option>, pub(crate) no_std_lib: Option>, @@ -294,13 +345,13 @@ impl Command for Nu { .switch("version", "print the version", Some('v')) .named( "config", - SyntaxShape::String, + SyntaxShape::Filepath, "start with an alternate config file", None, ) .named( "env-config", - SyntaxShape::String, + SyntaxShape::Filepath, "start with an alternate environment config file", None, ) @@ -337,12 +388,19 @@ impl Command for Nu { #[cfg(feature = "plugin")] { - signature = signature.named( - "plugin-config", - SyntaxShape::String, - "start with an alternate plugin cache file", - None, - ); + signature = signature + .named( + "plugin-config", + SyntaxShape::Filepath, + "start with an alternate plugin cache file", + None, + ) + .named( + "plugins", + SyntaxShape::List(Box::new(SyntaxShape::Filepath)), + "list of plugin executable files to load, separately from the cache file", + None, + ) } signature = signature diff --git a/src/main.rs b/src/main.rs index cbc195db48..061547825c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -389,6 +389,46 @@ fn main() -> Result<()> { use_color, ); + #[cfg(feature = "plugin")] + if let Some(plugins) = &parsed_nu_cli_args.plugins { + use nu_plugin::{GetPlugin, PluginDeclaration}; + use nu_protocol::{engine::StateWorkingSet, ErrSpan, PluginIdentity}; + + // Load any plugins specified with --plugins + start_time = std::time::Instant::now(); + + let mut working_set = StateWorkingSet::new(&engine_state); + for plugin_filename in plugins { + // Make sure the plugin filenames are canonicalized + let filename = canonicalize_with(&plugin_filename.item, &init_cwd) + .err_span(plugin_filename.span) + .map_err(ShellError::from)?; + + let identity = PluginIdentity::new(&filename, None) + .err_span(plugin_filename.span) + .map_err(ShellError::from)?; + + // Create the plugin and add it to the working set + let plugin = nu_plugin::add_plugin_to_working_set(&mut working_set, &identity)?; + + // Spawn the plugin to get its signatures, and then add the commands to the working set + for signature in plugin.clone().get_plugin(None)?.get_signature()? { + let decl = PluginDeclaration::new(plugin.clone(), signature); + working_set.add_decl(Box::new(decl)); + } + } + engine_state.merge_delta(working_set.render())?; + + perf( + "load plugins specified in --plugins", + start_time, + file!(), + line!(), + column!(), + use_color, + ) + } + start_time = std::time::Instant::now(); if parsed_nu_cli_args.lsp { perf( diff --git a/src/tests.rs b/src/tests.rs index 32b47bae19..487173b9b1 100644 --- a/src/tests.rs +++ b/src/tests.rs @@ -39,6 +39,7 @@ pub fn run_test_with_env(input: &str, expected: &str, env: &HashMap<&str, &str>) let name = file.path(); let mut cmd = Command::cargo_bin("nu")?; + cmd.arg("--no-config-file"); cmd.arg(name).envs(env); writeln!(file, "{input}")?; @@ -53,6 +54,7 @@ pub fn run_test(input: &str, expected: &str) -> TestResult { let mut cmd = Command::cargo_bin("nu")?; cmd.arg("--no-std-lib"); + cmd.arg("--no-config-file"); cmd.arg(name); cmd.env( "PWD", @@ -70,6 +72,7 @@ pub fn run_test_std(input: &str, expected: &str) -> TestResult { let name = file.path(); let mut cmd = Command::cargo_bin("nu")?; + cmd.arg("--no-config-file"); cmd.arg(name); cmd.env( "PWD", @@ -105,6 +108,7 @@ pub fn run_test_contains(input: &str, expected: &str) -> TestResult { let mut cmd = Command::cargo_bin("nu")?; cmd.arg("--no-std-lib"); + cmd.arg("--no-config-file"); cmd.arg(name); writeln!(file, "{input}")?; @@ -132,6 +136,7 @@ pub fn test_ide_contains(input: &str, ide_commands: &[&str], expected: &str) -> let mut cmd = Command::cargo_bin("nu")?; cmd.arg("--no-std-lib"); + cmd.arg("--no-config-file"); for ide_command in ide_commands { cmd.arg(ide_command); } @@ -162,6 +167,7 @@ pub fn fail_test(input: &str, expected: &str) -> TestResult { let mut cmd = Command::cargo_bin("nu")?; cmd.arg("--no-std-lib"); + cmd.arg("--no-config-file"); cmd.arg(name); cmd.env( "PWD", diff --git a/src/tests/test_parser.rs b/src/tests/test_parser.rs index 830b131b6e..0468437a7e 100644 --- a/src/tests/test_parser.rs +++ b/src/tests/test_parser.rs @@ -594,6 +594,42 @@ register $file fail_test(input, "expected string, found int") } +#[test] +fn plugin_use_with_string_literal() -> TestResult { + fail_test( + r#"plugin use 'nu-plugin-math'"#, + "Plugin cache file not set", + ) +} + +#[test] +fn plugin_use_with_string_constant() -> TestResult { + let input = "\ +const file = 'nu-plugin-math' +plugin use $file +"; + // should not fail with `not a constant` + fail_test(input, "Plugin cache file not set") +} + +#[test] +fn plugin_use_with_string_variable() -> TestResult { + let input = "\ +let file = 'nu-plugin-math' +plugin use $file +"; + fail_test(input, "Value is not a parse-time constant") +} + +#[test] +fn plugin_use_with_non_string_constant() -> TestResult { + let input = "\ +const file = 6 +plugin use $file +"; + fail_test(input, "expected string, found int") +} + #[test] fn extern_errors_with_no_space_between_params_and_name_1() -> TestResult { fail_test("extern cmd[]", "expected space") diff --git a/tests/fixtures/formats/code.nu b/tests/fixtures/formats/code.nu index 26263cf31e..5f3149085e 100644 --- a/tests/fixtures/formats/code.nu +++ b/tests/fixtures/formats/code.nu @@ -1 +1 @@ -register \ No newline at end of file +plugin use diff --git a/tests/plugins/cache_file.rs b/tests/plugins/cache_file.rs index c167390820..a5365fd3f0 100644 --- a/tests/plugins/cache_file.rs +++ b/tests/plugins/cache_file.rs @@ -1,8 +1,4 @@ -use std::{ - fs::File, - path::PathBuf, - process::{Command, Stdio}, -}; +use std::{fs::File, path::PathBuf}; use nu_protocol::{PluginCacheFile, PluginCacheItem, PluginCacheItemData}; use nu_test_support::{fs::Stub, nu, nu_with_plugins, playground::Playground}; @@ -69,16 +65,70 @@ fn plugin_add_to_custom_path() { #[test] fn plugin_rm_then_restart_nu() { - let result = nu_with_plugins!( - cwd: ".", - plugin: ("nu_plugin_example"), - r#" - plugin rm example - ^$nu.current-exe --config $nu.config-path --env-config $nu.env-path --plugin-config $nu.plugin-path --commands 'plugin list | get name | to json --raw' - "# - ); - assert!(result.status.success()); - assert_eq!(r#"[]"#, result.out); + let example_plugin_path = example_plugin_path(); + Playground::setup("plugin rm from custom path", |dirs, playground| { + playground.with_files(vec![ + Stub::FileWithContent("config.nu", ""), + Stub::FileWithContent("env.nu", ""), + ]); + + let file = File::create(dirs.test().join("test-plugin-file.msgpackz")) + .expect("failed to create file"); + let mut contents = PluginCacheFile::new(); + + contents.upsert_plugin(PluginCacheItem { + name: "example".into(), + filename: example_plugin_path, + shell: None, + data: PluginCacheItemData::Valid { commands: vec![] }, + }); + + contents.upsert_plugin(PluginCacheItem { + name: "foo".into(), + // this doesn't exist, but it should be ok + filename: dirs.test().join("nu_plugin_foo"), + shell: None, + data: PluginCacheItemData::Valid { commands: vec![] }, + }); + + contents + .write_to(file, None) + .expect("failed to write plugin file"); + + assert_cmd::Command::new(nu_test_support::fs::executable_path()) + .current_dir(dirs.test()) + .args([ + "--no-std-lib", + "--config", + "config.nu", + "--env-config", + "env.nu", + "--plugin-config", + "test-plugin-file.msgpackz", + "--commands", + "plugin rm example", + ]) + .assert() + .success() + .stderr(""); + + assert_cmd::Command::new(nu_test_support::fs::executable_path()) + .current_dir(dirs.test()) + .args([ + "--no-std-lib", + "--config", + "config.nu", + "--env-config", + "env.nu", + "--plugin-config", + "test-plugin-file.msgpackz", + "--commands", + "plugin list | get name | to json --raw", + ]) + .assert() + .success() + .stdout("[\"foo\"]\n"); + }) } #[test] @@ -176,7 +226,7 @@ fn warning_on_invalid_plugin_item() { .write_to(file, None) .expect("failed to write plugin file"); - let result = Command::new(nu_test_support::fs::executable_path()) + let result = assert_cmd::Command::new(nu_test_support::fs::executable_path()) .current_dir(dirs.test()) .args([ "--no-std-lib", @@ -189,9 +239,6 @@ fn warning_on_invalid_plugin_item() { "--commands", "plugin list | get name | to json --raw", ]) - .stdin(Stdio::null()) - .stdout(Stdio::piped()) - .stderr(Stdio::piped()) .output() .expect("failed to run nu"); @@ -209,3 +256,78 @@ fn warning_on_invalid_plugin_item() { assert!(err.contains("badtest")); }) } + +#[test] +fn plugin_use_error_not_found() { + Playground::setup("plugin use error not found", |dirs, playground| { + playground.with_files(vec![ + Stub::FileWithContent("config.nu", ""), + Stub::FileWithContent("env.nu", ""), + ]); + + // Make an empty msgpackz + let file = File::create(dirs.test().join("plugin.msgpackz")) + .expect("failed to open plugin.msgpackz"); + PluginCacheFile::default() + .write_to(file, None) + .expect("failed to write empty cache file"); + + let output = assert_cmd::Command::new(nu_test_support::fs::executable_path()) + .current_dir(dirs.test()) + .args(["--config", "config.nu"]) + .args(["--env-config", "env.nu"]) + .args(["--plugin-config", "plugin.msgpackz"]) + .args(["--commands", "plugin use custom_values"]) + .output() + .expect("failed to run nu"); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("Plugin not found")); + }) +} + +#[test] +fn plugin_add_and_then_use() { + let example_plugin_path = example_plugin_path(); + let result = nu_with_plugins!( + cwd: ".", + plugins: [], + &format!(r#" + plugin add '{}' + ( + ^$nu.current-exe + --config $nu.config-path + --env-config $nu.env-path + --plugin-config $nu.plugin-path + --commands 'plugin use example; plugin list | get name | to json --raw' + ) + "#, example_plugin_path.display()) + ); + assert!(result.status.success()); + assert_eq!(r#"["example"]"#, result.out); +} + +#[test] +fn plugin_add_then_use_with_custom_path() { + let example_plugin_path = example_plugin_path(); + Playground::setup("plugin add to custom path", |dirs, _playground| { + let result_add = nu!( + cwd: dirs.test(), + &format!(" + plugin add --plugin-config test-plugin-file.msgpackz '{}' + ", example_plugin_path.display()) + ); + + assert!(result_add.status.success()); + + let result_use = nu!( + cwd: dirs.test(), + r#" + plugin use --plugin-config test-plugin-file.msgpackz example + plugin list | get name | to json --raw + "# + ); + + assert!(result_use.status.success()); + assert_eq!(r#"["example"]"#, result_use.out); + }) +} diff --git a/tests/plugins/nu_plugin_nu_example.rs b/tests/plugins/nu_plugin_nu_example.rs index aa807b874e..f178c64316 100644 --- a/tests/plugins/nu_plugin_nu_example.rs +++ b/tests/plugins/nu_plugin_nu_example.rs @@ -1,26 +1,42 @@ -use nu_test_support::nu; - -#[test] -fn register() { - let out = nu!("register crates/nu_plugin_nu_example/nu_plugin_nu_example.nu"); - assert!(out.status.success()); - assert!(out.out.trim().is_empty()); - assert!(out.err.trim().is_empty()); -} +use assert_cmd::Command; #[test] fn call() { - let out = nu!(r#" - register crates/nu_plugin_nu_example/nu_plugin_nu_example.nu - nu_plugin_nu_example 4242 teststring - "#); - assert!(out.status.success()); + // Add the `nu` binaries to the path env + let path_env = std::env::join_paths( + std::iter::once(nu_test_support::fs::binaries()).chain( + std::env::var_os(nu_test_support::NATIVE_PATH_ENV_VAR) + .as_deref() + .map(std::env::split_paths) + .into_iter() + .flatten(), + ), + ) + .expect("failed to make path var"); - assert!(out.err.contains("name: nu_plugin_nu_example")); - assert!(out.err.contains("4242")); - assert!(out.err.contains("teststring")); + let assert = Command::new(nu_test_support::fs::executable_path()) + .env(nu_test_support::NATIVE_PATH_ENV_VAR, path_env) + .args([ + "--no-config-file", + "--no-std-lib", + "--plugins", + &format!( + "[crates{0}nu_plugin_nu_example{0}nu_plugin_nu_example.nu]", + std::path::MAIN_SEPARATOR + ), + "--commands", + "nu_plugin_nu_example 4242 teststring", + ]) + .assert() + .success(); - assert!(out.out.contains("one")); - assert!(out.out.contains("two")); - assert!(out.out.contains("three")); + let output = assert.get_output(); + let stdout = String::from_utf8_lossy(&output.stdout); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stdout.contains("one")); + assert!(stdout.contains("two")); + assert!(stdout.contains("three")); + assert!(stderr.contains("name: nu_plugin_nu_example")); + assert!(stderr.contains("4242")); + assert!(stderr.contains("teststring")); }