Deprecate register and add plugin use (#12607)

# Description

Adds a new keyword, `plugin use`. Unlike `register`, this merely loads
the signatures from the plugin cache file. The file is configurable with
the `--plugin-config` option either to `nu` or to `plugin use` itself,
just like the other `plugin` family of commands. At the REPL, one might
do this to replace `register`:

```nushell
> plugin add ~/.cargo/bin/nu_plugin_foo
> plugin use foo
```

This will not work in a script, because `plugin use` is a keyword and
`plugin add` does not evaluate at parse time (intentionally). This means
we no longer run random binaries during parse.

The `--plugins` option has been added to allow running `nu` with certain
plugins in one step. This is used especially for the `nu_with_plugins!`
test macro, but I'd imagine is generally useful. The only weird quirk is
that it has to be a list, and we don't really do this for any of our
other CLI args at the moment.

`register` now prints a deprecation parse warning.

This should fix #11923, as we now have a complete alternative to
`register`.

# User-Facing Changes

- Add `plugin use` command
- Deprecate `register`
- Add `--plugins` option to `nu` to replace a common use of `register`

# Tests + Formatting

I think I've tested it thoroughly enough and every existing test passes.
Testing nu CLI options and alternate config files is a little hairy and
I wish there were some more generic helpers for this, so this will go on
my TODO list for refactoring.

- 🟢 `toolkit fmt`
- 🟢 `toolkit clippy`
- 🟢 `toolkit test`
- 🟢 `toolkit test stdlib`

# After Submitting

- [ ] Update plugins sections of book
- [ ] Release notes
This commit is contained in:
Devyn Cairns
2024-04-23 04:37:50 -07:00
committed by GitHub
parent 5c7f7883c8
commit 1f4131532d
27 changed files with 759 additions and 172 deletions

View File

@ -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<Example> {

View File

@ -29,6 +29,10 @@ impl Command for PluginList {
"List installed plugins."
}
fn search_terms(&self) -> Vec<&str> {
vec!["scope"]
}
fn examples(&self) -> Vec<nu_protocol::Example> {
vec![
Example {

View File

@ -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<Example> {
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.",

View File

@ -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<Example> {

View File

@ -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<PipelineData, ShellError> {
Ok(PipelineData::empty())
}
fn examples(&self) -> Vec<Example> {
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,
},
]
}
}

View File

@ -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 {

View File

@ -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,
);

View File

@ -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"))
}

View File

@ -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<PersistentPlugin> = 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<Call>) -> 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::<String>::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::<String>::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<VarId> {
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<ParserPath> {
pub fn find_in_dirs_with_id(
filename: &str,
working_set: &StateWorkingSet,
cwd: &str,
dirs_var_name: &str,
dirs_var_name: Option<&str>,
) -> Option<ParserPath> {
// 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<PathBuf> {
// 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() {

View File

@ -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);

View File

@ -103,17 +103,33 @@ impl ParserPath {
}
}
pub fn read<'a>(&'a self, working_set: &'a StateWorkingSet) -> Option<Vec<u8>> {
pub fn open<'a>(
&'a self,
working_set: &'a StateWorkingSet,
) -> std::io::Result<Box<dyn std::io::Read + 'a>> {
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<dyn std::io::Read>)
}
ParserPath::VirtualFile(_, file_id) => working_set
.get_contents_of_file(*file_id)
.map(|bytes| bytes.to_vec()),
.map(|bytes| Box::new(bytes) as Box<dyn std::io::Read>)
.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<Vec<u8>> {
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,

View File

@ -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};

View File

@ -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![],
})?;

View File

@ -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<Span>,
) -> Result<(), ShellError> {
) -> Result<Arc<PersistentPlugin>, 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<PersistentPlugin> =
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<Arc<PersistentPlugin>, 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(),
})
}

View File

@ -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<ImportPattern> {
match &self.expr {
Expr::ImportPattern(pattern) => Some(*pattern.clone()),

View File

@ -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<Span>,
},
#[error("Invalid literal")] // <problem> in <entity>.
#[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,

View File

@ -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,
}
}
}

View File

@ -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<Span>,
#[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.

View File

@ -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<Spanned<InvalidPluginFilename>> for ParseError {
}
}
impl From<Spanned<InvalidPluginFilename>> for ShellError {
fn from(error: Spanned<InvalidPluginFilename>) -> 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<PathBuf>,
shell: Option<PathBuf>,
) -> Result<PluginIdentity, InvalidPluginFilename> {
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");
}

View File

@ -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<String> = 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()