nushell/crates/nu-cmd-plugin/src/commands/plugin/add.rs
Devyn Cairns b576123b0a
Accept filenames in other plugin management commands (#12639)
# Description

This allows the following commands to all accept a filename instead of a
plugin name:

- `plugin use`
- `plugin rm`
- `plugin stop`

Slightly complicated because of the need to also check against
`NU_PLUGIN_DIRS`, but I also fixed some issues with that at the same
time

Requested by @fdncred

# User-Facing Changes

The new commands are updated as described.

# Tests + Formatting

Tests for `NU_PLUGIN_DIRS` handling also made more robust.

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

# After Submitting

- [ ] Double check new docs to make sure they describe this capability
2024-04-24 06:28:45 -05:00

133 lines
4.6 KiB
Rust

use std::sync::Arc;
use nu_engine::{command_prelude::*, current_dir};
use nu_plugin::{GetPlugin, PersistentPlugin};
use nu_protocol::{PluginCacheItem, PluginGcConfig, PluginIdentity, RegisteredPlugin};
use crate::util::{get_plugin_dirs, modify_plugin_file};
#[derive(Clone)]
pub struct PluginAdd;
impl Command for PluginAdd {
fn name(&self) -> &str {
"plugin add"
}
fn signature(&self) -> Signature {
Signature::build(self.name())
.input_output_type(Type::Nothing, Type::Nothing)
// This matches the option to `nu`
.named(
"plugin-config",
SyntaxShape::Filepath,
"Use a plugin cache file other than the one set in `$nu.plugin-path`",
None,
)
.named(
"shell",
SyntaxShape::Filepath,
"Use an additional shell program (cmd, sh, python, etc.) to run the plugin",
Some('s'),
)
.required(
"filename",
SyntaxShape::Filepath,
"Path to the executable for the plugin",
)
.category(Category::Plugin)
}
fn usage(&self) -> &str {
"Add a plugin to the plugin cache file."
}
fn extra_usage(&self) -> &str {
r#"
This does not load the plugin commands into the scope - see `register` for that.
Instead, it runs the plugin to get its command signatures, and then edits the
plugin cache file (by default, `$nu.plugin-path`). The changes will be
apparent the next time `nu` is next launched with that plugin cache file.
"#
.trim()
}
fn search_terms(&self) -> Vec<&str> {
vec!["load", "register", "signature"]
}
fn examples(&self) -> Vec<Example> {
vec![
Example {
example: "plugin add nu_plugin_inc",
description: "Run the `nu_plugin_inc` plugin from the current directory or $env.NU_PLUGIN_DIRS and install its signatures.",
result: None,
},
Example {
example: "plugin add --plugin-config polars.msgpackz nu_plugin_polars",
description: "Run the `nu_plugin_polars` plugin from the current directory or $env.NU_PLUGIN_DIRS, and install its signatures to the \"polars.msgpackz\" plugin cache file.",
result: None,
},
]
}
fn run(
&self,
engine_state: &EngineState,
stack: &mut Stack,
call: &Call,
_input: PipelineData,
) -> Result<PipelineData, ShellError> {
let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
let cwd = current_dir(engine_state, stack)?;
// Check the current directory, or fall back to NU_PLUGIN_DIRS
let filename_expanded = nu_path::locate_in_dirs(&filename.item, &cwd, || {
get_plugin_dirs(engine_state, stack)
})
.err_span(filename.span)?;
let shell_expanded = shell
.as_ref()
.map(|s| nu_path::canonicalize_with(&s.item, &cwd).err_span(s.span))
.transpose()?;
// Parse the plugin filename so it can be used to spawn the plugin
let identity = PluginIdentity::new(filename_expanded, shell_expanded).map_err(|_| {
ShellError::GenericError {
error: "Plugin filename is invalid".into(),
msg: "plugin executable files must start with `nu_plugin_`".into(),
span: Some(filename.span),
help: None,
inner: vec![],
}
})?;
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
// Start the plugin manually, to get the freshest signatures and to not affect engine
// state. Provide a GC config that will stop it ASAP
let plugin = Arc::new(PersistentPlugin::new(
identity,
PluginGcConfig {
enabled: true,
stop_after: 0,
},
));
let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
let commands = interface.get_signature()?;
modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
// Update the file with the received signatures
let item = PluginCacheItem::new(plugin.identity(), commands);
contents.upsert_plugin(item);
Ok(())
})?;
Ok(Value::nothing(call.head).into_pipeline_data())
}
}