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