Skip to content

Commit

Permalink
Accept filenames in other plugin management commands (#12639)
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
devyn committed Apr 24, 2024
1 parent 1633004 commit b576123
Show file tree
Hide file tree
Showing 11 changed files with 283 additions and 37 deletions.
24 changes: 5 additions & 19 deletions crates/nu-cmd-plugin/src/commands/plugin/add.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ use nu_engine::{command_prelude::*, current_dir};
use nu_plugin::{GetPlugin, PersistentPlugin};
use nu_protocol::{PluginCacheItem, PluginGcConfig, PluginIdentity, RegisteredPlugin};

use crate::util::modify_plugin_file;
use crate::util::{get_plugin_dirs, modify_plugin_file};

#[derive(Clone)]
pub struct PluginAdd;
Expand Down Expand Up @@ -85,24 +85,10 @@ apparent the next time `nu` is next launched with that plugin cache file.
let cwd = current_dir(engine_state, stack)?;

// Check the current directory, or fall back to NU_PLUGIN_DIRS
let filename_expanded = match nu_path::canonicalize_with(&filename.item, &cwd) {
Ok(path) => path,
Err(err) => {
// Try to find it in NU_PLUGIN_DIRS first, before giving up
let mut found = None;
if let Some(nu_plugin_dirs) = stack.get_env_var(engine_state, "NU_PLUGIN_DIRS") {
for dir in nu_plugin_dirs.into_list().unwrap_or(vec![]) {
if let Ok(path) = nu_path::canonicalize_with(dir.as_str()?, &cwd)
.and_then(|dir| nu_path::canonicalize_with(&filename.item, dir))
{
found = Some(path);
break;
}
}
}
found.ok_or(err.into_spanned(filename.span))?
}
};
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()
Expand Down
25 changes: 19 additions & 6 deletions crates/nu-cmd-plugin/src/commands/plugin/rm.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
use nu_engine::command_prelude::*;

use crate::util::modify_plugin_file;
use crate::util::{canonicalize_possible_filename_arg, modify_plugin_file};

#[derive(Clone)]
pub struct PluginRm;
Expand Down Expand Up @@ -28,7 +28,7 @@ impl Command for PluginRm {
.required(
"name",
SyntaxShape::String,
"The name of the plugin to remove (not the filename)",
"The name, or filename, of the plugin to remove",
)
.category(Category::Plugin)
}
Expand Down Expand Up @@ -61,6 +61,11 @@ fixed with `plugin add`.
description: "Remove the installed signatures for the `inc` plugin.",
result: None,
},
Example {
example: "plugin rm ~/.cargo/bin/nu_plugin_inc",
description: "Remove the installed signatures for the plugin with the filename `~/.cargo/bin/nu_plugin_inc`.",
result: None,
},
Example {
example: "plugin rm --plugin-config polars.msgpackz polars",
description: "Remove the installed signatures for the `polars` plugin from the \"polars.msgpackz\" plugin cache file.",
Expand All @@ -80,18 +85,26 @@ fixed with `plugin add`.
let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
let force = call.has_flag(engine_state, stack, "force")?;

let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item);

modify_plugin_file(engine_state, stack, call.head, custom_path, |contents| {
if !force && !contents.plugins.iter().any(|p| p.name == name.item) {
if let Some(index) = contents
.plugins
.iter()
.position(|p| p.name == name.item || p.filename == filename)
{
contents.plugins.remove(index);
Ok(())
} else if force {
Ok(())
} else {
Err(ShellError::GenericError {
error: format!("Failed to remove the `{}` plugin", name.item),
msg: "couldn't find a plugin with this name in the cache file".into(),
span: Some(name.span),
help: None,
inner: vec![],
})
} else {
contents.remove_plugin(&name.item);
Ok(())
}
})?;

Expand Down
14 changes: 12 additions & 2 deletions crates/nu-cmd-plugin/src/commands/plugin/stop.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use nu_engine::command_prelude::*;

use crate::util::canonicalize_possible_filename_arg;

#[derive(Clone)]
pub struct PluginStop;

Expand All @@ -14,7 +16,7 @@ impl Command for PluginStop {
.required(
"name",
SyntaxShape::String,
"The name of the plugin to stop.",
"The name, or filename, of the plugin to stop",
)
.category(Category::Plugin)
}
Expand All @@ -30,6 +32,11 @@ impl Command for PluginStop {
description: "Stop the plugin named `inc`.",
result: None,
},
Example {
example: "plugin stop ~/.cargo/bin/nu_plugin_inc",
description: "Stop the plugin with the filename `~/.cargo/bin/nu_plugin_inc`.",
result: None,
},
Example {
example: "plugin list | each { |p| plugin stop $p.name }",
description: "Stop all plugins.",
Expand All @@ -47,9 +54,12 @@ impl Command for PluginStop {
) -> Result<PipelineData, ShellError> {
let name: Spanned<String> = call.req(engine_state, stack, 0)?;

let filename = canonicalize_possible_filename_arg(engine_state, stack, &name.item);

let mut found = false;
for plugin in engine_state.plugins() {
if plugin.identity().name() == name.item {
let id = &plugin.identity();
if id.name() == name.item || id.filename() == filename {
plugin.stop()?;
found = true;
}
Expand Down
10 changes: 9 additions & 1 deletion crates/nu-cmd-plugin/src/commands/plugin/use_.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ impl Command for PluginUse {
.required(
"name",
SyntaxShape::String,
"The name of the plugin to load (not the filename)",
"The name, or filename, of the plugin to load",
)
.category(Category::Plugin)
}
Expand All @@ -41,6 +41,9 @@ preparing a plugin cache file and passing `--plugin-config`, or using the
If the plugin was already loaded, this will reload the latest definition from
the cache file into scope.
Note that even if the plugin filename is specified, it will only be loaded if
it was already previously registered with `plugin add`.
"#
.trim()
}
Expand Down Expand Up @@ -70,6 +73,11 @@ the cache file into scope.
example: r#"plugin use query"#,
result: None,
},
Example {
description: "Load the commands for the plugin with the filename `~/.cargo/bin/nu_plugin_query` from $nu.plugin-path",
example: r#"plugin use ~/.cargo/bin/nu_plugin_query"#,
result: None,
},
Example {
description:
"Load the commands for the `query` plugin from a custom plugin cache file",
Expand Down
43 changes: 41 additions & 2 deletions crates/nu-cmd-plugin/src/util.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
use std::fs::{self, File};
use std::{
fs::{self, File},
path::PathBuf,
};

use nu_engine::{command_prelude::*, current_dir};
use nu_protocol::PluginCacheFile;
use nu_protocol::{engine::StateWorkingSet, PluginCacheFile};

pub(crate) fn modify_plugin_file(
engine_state: &EngineState,
Expand Down Expand Up @@ -48,3 +51,39 @@ pub(crate) fn modify_plugin_file(

Ok(())
}

pub(crate) fn canonicalize_possible_filename_arg(
engine_state: &EngineState,
stack: &Stack,
arg: &str,
) -> PathBuf {
// This results in the best possible chance of a match with the plugin item
if let Ok(cwd) = nu_engine::current_dir(engine_state, stack) {
let path = nu_path::expand_path_with(arg, &cwd, true);
// Try to canonicalize
nu_path::locate_in_dirs(&path, &cwd, || get_plugin_dirs(engine_state, stack))
// If we couldn't locate it, return the expanded path alone
.unwrap_or(path)
} else {
arg.into()
}
}

pub(crate) fn get_plugin_dirs(
engine_state: &EngineState,
stack: &Stack,
) -> impl Iterator<Item = String> {
// Get the NU_PLUGIN_DIRS constant or env var
let working_set = StateWorkingSet::new(engine_state);
let value = working_set
.find_variable(b"$NU_PLUGIN_DIRS")
.and_then(|var_id| working_set.get_constant(var_id).ok().cloned())
.or_else(|| stack.get_env_var(engine_state, "NU_PLUGIN_DIRS"));

// Get all of the strings in the list, if possible
value
.into_iter()
.flat_map(|value| value.into_list().ok())
.flatten()
.flat_map(|list_item| list_item.coerce_into_string().ok())
}
13 changes: 12 additions & 1 deletion crates/nu-parser/src/parse_keywords.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3803,6 +3803,17 @@ pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box<Call>) -> P
})
.transpose()?;

// The name could also be a filename, so try our best to expand it for that match.
let filename_query = {
let path = nu_path::expand_path_with(&name.item, &cwd, true);
path.to_str()
.and_then(|path_str| {
find_in_dirs(path_str, working_set, &cwd, Some("NU_PLUGIN_DIRS"))
})
.map(|parser_path| parser_path.path_buf())
.unwrap_or(path)
};

// 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 {
Expand Down Expand Up @@ -3842,7 +3853,7 @@ pub fn parse_plugin_use(working_set: &mut StateWorkingSet, call: Box<Call>) -> P
let plugin_item = contents
.plugins
.iter()
.find(|plugin| plugin.name == name.item)
.find(|plugin| plugin.name == name.item || plugin.filename == filename_query)
.ok_or_else(|| ParseError::PluginNotFound {
name: name.item.clone(),
name_span: name.span,
Expand Down
32 changes: 32 additions & 0 deletions crates/nu-path/src/expansions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,35 @@ where
let path = expand_tilde(path);
expand_ndots(path)
}

/// Attempts to canonicalize the path against the current directory. Failing that, if
/// the path is relative, it attempts all of the dirs in `dirs`. If that fails, it returns
/// the original error.
pub fn locate_in_dirs<I, P>(
filename: impl AsRef<Path>,
cwd: impl AsRef<Path>,
dirs: impl FnOnce() -> I,
) -> std::io::Result<PathBuf>
where
I: IntoIterator<Item = P>,
P: AsRef<Path>,
{
let filename = filename.as_ref();
let cwd = cwd.as_ref();
match canonicalize_with(filename, cwd) {
Ok(path) => Ok(path),
Err(err) => {
// Try to find it in `dirs` first, before giving up
let mut found = None;
for dir in dirs() {
if let Ok(path) =
canonicalize_with(dir, cwd).and_then(|dir| canonicalize_with(filename, dir))
{
found = Some(path);
break;
}
}
found.ok_or(err)
}
}
}
2 changes: 1 addition & 1 deletion crates/nu-path/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ mod helpers;
mod tilde;
mod util;

pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path};
pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path, locate_in_dirs};
pub use helpers::{config_dir, config_dir_old, home_dir};
pub use tilde::expand_tilde;
pub use util::trim_trailing_slash;
5 changes: 0 additions & 5 deletions crates/nu-protocol/src/plugin/cache_file/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,11 +96,6 @@ impl PluginCacheFile {
.sort_by(|item1, item2| item1.name.cmp(&item2.name));
}
}

/// Remove a plugin from the plugin cache file by name.
pub fn remove_plugin(&mut self, name: &str) {
self.plugins.retain_mut(|item| item.name != name)
}
}

/// A single plugin definition from a [`PluginCacheFile`].
Expand Down
11 changes: 11 additions & 0 deletions tests/plugin_persistence/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,17 @@ fn plugin_process_exits_after_stop() {
);
}

#[test]
fn plugin_stop_can_find_by_filename() {
let result = nu_with_plugins!(
cwd: ".",
plugin: ("nu_plugin_inc"),
r#"plugin stop (plugin list | where name == inc).0.filename"#
);
assert!(result.status.success());
assert!(result.err.is_empty());
}

#[test]
fn plugin_process_exits_when_nushell_exits() {
let out = nu_with_plugins!(
Expand Down
Loading

0 comments on commit b576123

Please sign in to comment.