Skip to main content

nu_cmd_plugin/commands/plugin/
list.rs

1use itertools::{EitherOrBoth, Itertools};
2use nu_engine::command_prelude::*;
3use nu_protocol::{IntoValue, PluginRegistryItemData};
4
5use crate::util::read_plugin_file;
6
7#[derive(Clone)]
8pub struct PluginList;
9
10impl Command for PluginList {
11    fn name(&self) -> &str {
12        "plugin list"
13    }
14
15    fn signature(&self) -> Signature {
16        Signature::build("plugin list")
17            .input_output_type(
18                Type::Nothing,
19                Type::Table(
20                    vec![
21                        ("name".into(), Type::String),
22                        ("version".into(), Type::String),
23                        ("status".into(), Type::String),
24                        ("pid".into(), Type::Int),
25                        ("filename".into(), Type::String),
26                        ("shell".into(), Type::String),
27                        (
28                            "commands".into(),
29                            Type::List(Box::new(Type::Record(
30                                vec![
31                                    ("name".into(), Type::String),
32                                    ("description".into(), Type::String),
33                                ]
34                                .into(),
35                            ))),
36                        ),
37                    ]
38                    .into(),
39                ),
40            )
41            .named(
42                "plugin-config",
43                SyntaxShape::Filepath,
44                "Use a plugin registry file other than the one set in `$nu.plugin-path`.",
45                None,
46            )
47            .switch(
48                "engine",
49                "Show info for plugins that are loaded into the engine only.",
50                Some('e'),
51            )
52            .switch(
53                "registry",
54                "Show info for plugins from the registry file only.",
55                Some('r'),
56            )
57            .category(Category::Plugin)
58    }
59
60    fn description(&self) -> &str {
61        "List loaded and installed plugins."
62    }
63
64    fn extra_description(&self) -> &str {
65        "
66The `status` column will contain one of the following values:
67
68- `added`:    The plugin is present in the plugin registry file, but not in
69              the engine.
70- `loaded`:   The plugin is present both in the plugin registry file and in
71              the engine, but is not running.
72- `running`:  The plugin is currently running, and the `pid` column should
73              contain its process ID.
74- `modified`: The plugin state present in the plugin registry file is different
75              from the state in the engine.
76- `removed`:  The plugin is still loaded in the engine, but is not present in
77              the plugin registry file.
78- `invalid`:  The data in the plugin registry file couldn't be deserialized,
79              and the plugin most likely needs to be added again.
80
81`running` takes priority over any other status. Unless `--registry` is used
82or the plugin has not been loaded yet, the values of `version`, `filename`,
83`shell`, and `commands` reflect the values in the engine and not the ones in
84the plugin registry file.
85
86See also: `plugin use`
87"
88        .trim()
89    }
90
91    fn search_terms(&self) -> Vec<&str> {
92        vec!["scope"]
93    }
94
95    fn examples(&self) -> Vec<nu_protocol::Example<'_>> {
96        vec![
97            Example {
98                example: "plugin list",
99                description: "List installed plugins.",
100                result: Some(Value::test_list(vec![Value::test_record(record! {
101                    "name" => Value::test_string("inc"),
102                    "version" => Value::test_string(env!("CARGO_PKG_VERSION")),
103                    "status" => Value::test_string("running"),
104                    "pid" => Value::test_int(106480),
105                    "filename" => if cfg!(windows) {
106                        Value::test_string(r"C:\nu\plugins\nu_plugin_inc.exe")
107                    } else {
108                        Value::test_string("/opt/nu/plugins/nu_plugin_inc")
109                    },
110                    "shell" => Value::test_nothing(),
111                    "commands" => Value::test_list(vec![Value::test_record(record! {
112                        "name" => Value::test_string("inc"),
113                        "description" => Value::test_string("Increment a number by 1."),
114                    })]),
115                })])),
116            },
117            Example {
118                example: "ps | where pid in (plugin list).pid",
119                description: "Get process information for running plugins.",
120                result: None,
121            },
122        ]
123    }
124
125    fn run(
126        &self,
127        engine_state: &EngineState,
128        stack: &mut Stack,
129        call: &Call,
130        _input: PipelineData,
131    ) -> Result<PipelineData, ShellError> {
132        let custom_path = call.get_flag(engine_state, stack, "plugin-config")?;
133        let engine_mode = call.has_flag(engine_state, stack, "engine")?;
134        let registry_mode = call.has_flag(engine_state, stack, "registry")?;
135
136        let plugins_info = match (engine_mode, registry_mode) {
137            // --engine and --registry together is equivalent to the default.
138            (false, false) | (true, true) => {
139                if engine_state.plugin_path.is_some() || custom_path.is_some() {
140                    let plugins_in_engine = get_plugins_in_engine(engine_state);
141                    let plugins_in_registry =
142                        get_plugins_in_registry(engine_state, stack, call.head, &custom_path)?;
143                    merge_plugin_info(plugins_in_engine, plugins_in_registry)
144                } else {
145                    // Don't produce error when running nu --no-config-file
146                    get_plugins_in_engine(engine_state)
147                }
148            }
149            (true, false) => get_plugins_in_engine(engine_state),
150            (false, true) => get_plugins_in_registry(engine_state, stack, call.head, &custom_path)?,
151        };
152
153        Ok(plugins_info.into_value(call.head).into_pipeline_data())
154    }
155}
156
157#[derive(Debug, Clone, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
158struct PluginInfo {
159    name: String,
160    version: Option<String>,
161    status: PluginStatus,
162    pid: Option<u32>,
163    filename: String,
164    shell: Option<String>,
165    commands: Vec<CommandInfo>,
166}
167
168#[derive(Debug, Clone, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
169struct CommandInfo {
170    name: String,
171    description: String,
172}
173
174#[derive(Debug, Clone, Copy, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
175#[nu_value(rename_all = "snake_case")]
176enum PluginStatus {
177    Added,
178    Loaded,
179    Running,
180    Modified,
181    Removed,
182    Invalid,
183}
184
185fn get_plugins_in_engine(engine_state: &EngineState) -> Vec<PluginInfo> {
186    // Group plugin decls by plugin identity
187    let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
188        decl.plugin_identity()
189            .expect("plugin decl should have identity")
190    });
191
192    // Build plugins list
193    engine_state
194        .plugins()
195        .iter()
196        .map(|plugin| {
197            // Find commands that belong to the plugin
198            let commands: Vec<(String, String)> = decls
199                .get(plugin.identity())
200                .into_iter()
201                .flat_map(|decls| {
202                    decls
203                        .iter()
204                        .map(|decl| (decl.name().to_owned(), decl.description().to_owned()))
205                })
206                .sorted()
207                .collect();
208
209            PluginInfo {
210                name: plugin.identity().name().into(),
211                version: plugin.metadata().and_then(|m| m.version),
212                status: if plugin.pid().is_some() {
213                    PluginStatus::Running
214                } else {
215                    PluginStatus::Loaded
216                },
217                pid: plugin.pid(),
218                filename: plugin.identity().filename().to_string_lossy().into_owned(),
219                shell: plugin
220                    .identity()
221                    .shell()
222                    .map(|path| path.to_string_lossy().into_owned()),
223                commands: commands
224                    .iter()
225                    .map(|(name, desc)| CommandInfo {
226                        name: name.clone(),
227                        description: desc.clone(),
228                    })
229                    .collect(),
230            }
231        })
232        .sorted()
233        .collect()
234}
235
236fn get_plugins_in_registry(
237    engine_state: &EngineState,
238    stack: &mut Stack,
239    span: Span,
240    custom_path: &Option<Spanned<String>>,
241) -> Result<Vec<PluginInfo>, ShellError> {
242    let plugin_file_contents = read_plugin_file(engine_state, stack, span, custom_path)?;
243
244    let plugins_info = plugin_file_contents
245        .plugins
246        .into_iter()
247        .map(|plugin| {
248            let mut info = PluginInfo {
249                name: plugin.name,
250                version: None,
251                status: PluginStatus::Added,
252                pid: None,
253                filename: plugin.filename.to_string_lossy().into_owned(),
254                shell: plugin.shell.map(|path| path.to_string_lossy().into_owned()),
255                commands: vec![],
256            };
257
258            if let PluginRegistryItemData::Valid { metadata, commands } = plugin.data {
259                info.version = metadata.version;
260                info.commands = commands
261                    .into_iter()
262                    .map(|command| CommandInfo {
263                        name: command.sig.name.clone(),
264                        description: command.sig.description.clone(),
265                    })
266                    .sorted()
267                    .collect();
268            } else {
269                info.status = PluginStatus::Invalid;
270            }
271            info
272        })
273        .sorted()
274        .collect();
275
276    Ok(plugins_info)
277}
278
279/// If no options are provided, the command loads from both the plugin list in the engine and what's
280/// in the registry file. We need to reconcile the two to set the proper states and make sure that
281/// new plugins that were added to the plugin registry file show up.
282fn merge_plugin_info(
283    from_engine: Vec<PluginInfo>,
284    from_registry: Vec<PluginInfo>,
285) -> Vec<PluginInfo> {
286    from_engine
287        .into_iter()
288        .merge_join_by(from_registry, |info_a, info_b| {
289            info_a.name.cmp(&info_b.name)
290        })
291        .map(|either_or_both| match either_or_both {
292            // Exists in the engine, but not in the registry file
293            EitherOrBoth::Left(info) => PluginInfo {
294                status: match info.status {
295                    PluginStatus::Running => info.status,
296                    // The plugin is not in the registry file, so it should be marked as `removed`
297                    _ => PluginStatus::Removed,
298                },
299                ..info
300            },
301            // Exists in the registry file, but not in the engine
302            EitherOrBoth::Right(info) => info,
303            // Exists in both
304            EitherOrBoth::Both(info_engine, info_registry) => PluginInfo {
305                status: match (info_engine.status, info_registry.status) {
306                    // Above all, `running` should be displayed if the plugin is running
307                    (PluginStatus::Running, _) => PluginStatus::Running,
308                    // `invalid` takes precedence over other states because the user probably wants
309                    // to fix it
310                    (_, PluginStatus::Invalid) => PluginStatus::Invalid,
311                    // Display `modified` if the state in the registry is different somehow
312                    _ if info_engine.is_modified(&info_registry) => PluginStatus::Modified,
313                    // Otherwise, `loaded` (it's not running)
314                    _ => PluginStatus::Loaded,
315                },
316                ..info_engine
317            },
318        })
319        .sorted()
320        .collect()
321}
322
323impl PluginInfo {
324    /// True if the plugin info shows some kind of change (other than status/pid) relative to the
325    /// other
326    fn is_modified(&self, other: &PluginInfo) -> bool {
327        self.name != other.name
328            || self.filename != other.filename
329            || self.shell != other.shell
330            || self.commands != other.commands
331    }
332}