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