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    shell_error::io::IoError, PluginGcConfig, PluginIdentity, PluginRegistryItem, RegisteredPlugin,
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| IoError::new(err.kind(), filename.span, PathBuf::from(filename.item)))?;
92
93        let shell_expanded = shell
94            .as_ref()
95            .map(|s| {
96                nu_path::canonicalize_with(&s.item, &cwd)
97                    .map_err(|err| IoError::new(err.kind(), s.span, None))
98            })
99            .transpose()?;
100
101        // Parse the plugin filename so it can be used to spawn the plugin
102        let identity = PluginIdentity::new(filename_expanded, shell_expanded).map_err(|_| {
103            ShellError::GenericError {
104                error: "Plugin filename is invalid".into(),
105                msg: "plugin executable files must start with `nu_plugin_`".into(),
106                span: Some(filename.span),
107                help: None,
108                inner: vec![],
109            }
110        })?;
111
112        let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
113
114        // Start the plugin manually, to get the freshest signatures and to not affect engine
115        // state. Provide a GC config that will stop it ASAP
116        let plugin = Arc::new(PersistentPlugin::new(
117            identity,
118            PluginGcConfig {
119                enabled: true,
120                stop_after: 0,
121            },
122        ));
123        let interface = plugin.clone().get_plugin(Some((engine_state, stack)))?;
124        let metadata = interface.get_metadata()?;
125        let commands = interface.get_signature()?;
126
127        modify_plugin_file(engine_state, stack, call.head, &custom_path, |contents| {
128            // Update the file with the received metadata and signatures
129            let item = PluginRegistryItem::new(plugin.identity(), metadata, commands);
130            contents.upsert_plugin(item);
131            Ok(())
132        })?;
133
134        Ok(Value::nothing(call.head).into_pipeline_data())
135    }
136}