Skip to main content

nu_cmd_plugin/commands/plugin/
add.rs

1use crate::util::{get_plugin_dirs, modify_plugin_file};
2use nu_engine::command_prelude::*;
3use nu_plugin_engine::{GetPlugin, PersistentPlugin};
4use nu_protocol::{
5    PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin,
6    shell_error::generic::GenericError, shell_error::io::IoError,
7};
8use std::{path::PathBuf, sync::Arc};
9
10#[derive(Clone)]
11pub struct PluginAdd;
12
13impl Command for PluginAdd {
14    fn name(&self) -> &str {
15        "plugin add"
16    }
17
18    fn signature(&self) -> Signature {
19        Signature::build(self.name())
20            .input_output_type(Type::Nothing, Type::Nothing)
21            // This matches the option to `nu`
22            .named(
23                "plugin-config",
24                SyntaxShape::Filepath,
25                "Use a plugin registry file other than the one set in `$nu.plugin-path`.",
26                None,
27            )
28            .named(
29                "shell",
30                SyntaxShape::Filepath,
31                "Use an additional shell program (cmd, sh, python, etc.) to run the plugin.",
32                Some('s'),
33            )
34            .required(
35                "filename",
36                SyntaxShape::String,
37                "Path to the executable for the plugin.",
38            )
39            .category(Category::Plugin)
40    }
41
42    fn description(&self) -> &str {
43        "Add a plugin to the plugin registry file."
44    }
45
46    fn extra_description(&self) -> &str {
47        "
48This does not load the plugin commands into the scope - see `plugin use` for
49that.
50
51Instead, it runs the plugin to get its command signatures, and then edits the
52plugin registry file (by default, `$nu.plugin-path`). The changes will be
53apparent the next time `nu` is next launched with that plugin registry file.
54"
55        .trim()
56    }
57
58    fn search_terms(&self) -> Vec<&str> {
59        vec!["load", "register", "signature"]
60    }
61
62    fn examples(&self) -> Vec<Example<'_>> {
63        vec![
64            Example {
65                example: "plugin add nu_plugin_inc",
66                description: "Run the `nu_plugin_inc` plugin from the current directory or $env.NU_PLUGIN_DIRS and install its signatures.",
67                result: None,
68            },
69            Example {
70                example: "plugin add --plugin-config polars.msgpackz nu_plugin_polars",
71                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 registry file.",
72                result: None,
73            },
74        ]
75    }
76
77    fn run(
78        &self,
79        engine_state: &EngineState,
80        stack: &mut Stack,
81        call: &Call,
82        _input: PipelineData,
83    ) -> Result<PipelineData, ShellError> {
84        let filename: Spanned<String> = call.req(engine_state, stack, 0)?;
85        let shell: Option<Spanned<String>> = call.get_flag(engine_state, stack, "shell")?;
86        let cwd = engine_state.cwd(Some(stack))?;
87
88        // Check the current directory, or fall back to NU_PLUGIN_DIRS
89        let filename_expanded = nu_path::locate_in_dirs(&filename.item, &cwd, || {
90            get_plugin_dirs(engine_state, stack)
91        })
92        .map_err(|err| {
93            IoError::new(
94                err.not_found_as(NotFound::File),
95                filename.span,
96                PathBuf::from(filename.item),
97            )
98        })?;
99
100        let shell_expanded = shell
101            .as_ref()
102            .map(|s| {
103                nu_path::absolute_with(&s.item, &cwd).map_err(|err| IoError::new(err, s.span, None))
104            })
105            .transpose()?;
106
107        // Parse the plugin filename so it can be used to spawn the plugin
108        let identity = PluginIdentity::new(filename_expanded, shell_expanded).map_err(|_| {
109            ShellError::Generic(GenericError::new(
110                "Plugin filename is invalid",
111                "plugin executable files must start with `nu_plugin_`",
112                filename.span,
113            ))
114        })?;
115
116        let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
117
118        // Start the plugin manually, to get the freshest signatures and to not affect engine
119        // state. Provide a GC config that will stop it ASAP
120        let plugin = Arc::new(PersistentPlugin::new(
121            identity,
122            PluginGcConfig {
123                enabled: true,
124                stop_after: 0,
125            },
126        ));
127        let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
128        let metadata = interface.get_metadata()?;
129        let commands = interface.get_signature()?;
130
131        modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| {
132            // Update the file with the received metadata and signatures
133            let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
134            contents.upsert_plugin(item);
135            Ok(())
136        })?;
137
138        Ok(Value::nothing(call.head).into_pipeline_data())
139    }
140}