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<String>,
154}
155
156#[derive(Debug, Clone, Copy, IntoValue, PartialOrd, Ord, PartialEq, Eq)]
157#[nu_value(rename_all = "snake_case")]
158enum PluginStatus {
159    Added,
160    Loaded,
161    Running,
162    Modified,
163    Removed,
164    Invalid,
165}
166
167fn get_plugins_in_engine(engine_state: &EngineState) -> Vec<PluginInfo> {
168    // Group plugin decls by plugin identity
169    let decls = engine_state.plugin_decls().into_group_map_by(|decl| {
170        decl.plugin_identity()
171            .expect("plugin decl should have identity")
172    });
173
174    // Build plugins list
175    engine_state
176        .plugins()
177        .iter()
178        .map(|plugin| {
179            // Find commands that belong to the plugin
180            let commands = decls
181                .get(plugin.identity())
182                .into_iter()
183                .flat_map(|decls| decls.iter().map(|decl| decl.name().to_owned()))
184                .sorted()
185                .collect();
186
187            PluginInfo {
188                name: plugin.identity().name().into(),
189                version: plugin.metadata().and_then(|m| m.version),
190                status: if plugin.pid().is_some() {
191                    PluginStatus::Running
192                } else {
193                    PluginStatus::Loaded
194                },
195                pid: plugin.pid(),
196                filename: plugin.identity().filename().to_string_lossy().into_owned(),
197                shell: plugin
198                    .identity()
199                    .shell()
200                    .map(|path| path.to_string_lossy().into_owned()),
201                commands,
202            }
203        })
204        .sorted()
205        .collect()
206}
207
208fn get_plugins_in_registry(
209    engine_state: &EngineState,
210    stack: &mut Stack,
211    span: Span,
212    custom_path: &Option<Spanned<String>>,
213) -> Result<Vec<PluginInfo>, ShellError> {
214    let plugin_file_contents = read_plugin_file(engine_state, stack, span, custom_path)?;
215
216    let plugins_info = plugin_file_contents
217        .plugins
218        .into_iter()
219        .map(|plugin| {
220            let mut info = PluginInfo {
221                name: plugin.name,
222                version: None,
223                status: PluginStatus::Added,
224                pid: None,
225                filename: plugin.filename.to_string_lossy().into_owned(),
226                shell: plugin.shell.map(|path| path.to_string_lossy().into_owned()),
227                commands: vec![],
228            };
229
230            if let PluginRegistryItemData::Valid { metadata, commands } = plugin.data {
231                info.version = metadata.version;
232                info.commands = commands
233                    .into_iter()
234                    .map(|command| command.sig.name)
235                    .sorted()
236                    .collect();
237            } else {
238                info.status = PluginStatus::Invalid;
239            }
240            info
241        })
242        .sorted()
243        .collect();
244
245    Ok(plugins_info)
246}
247
248/// If no options are provided, the command loads from both the plugin list in the engine and what's
249/// in the registry file. We need to reconcile the two to set the proper states and make sure that
250/// new plugins that were added to the plugin registry file show up.
251fn merge_plugin_info(
252    from_engine: Vec<PluginInfo>,
253    from_registry: Vec<PluginInfo>,
254) -> Vec<PluginInfo> {
255    from_engine
256        .into_iter()
257        .merge_join_by(from_registry, |info_a, info_b| {
258            info_a.name.cmp(&info_b.name)
259        })
260        .map(|either_or_both| match either_or_both {
261            // Exists in the engine, but not in the registry file
262            EitherOrBoth::Left(info) => PluginInfo {
263                status: match info.status {
264                    PluginStatus::Running => info.status,
265                    // The plugin is not in the registry file, so it should be marked as `removed`
266                    _ => PluginStatus::Removed,
267                },
268                ..info
269            },
270            // Exists in the registry file, but not in the engine
271            EitherOrBoth::Right(info) => info,
272            // Exists in both
273            EitherOrBoth::Both(info_engine, info_registry) => PluginInfo {
274                status: match (info_engine.status, info_registry.status) {
275                    // Above all, `running` should be displayed if the plugin is running
276                    (PluginStatus::Running, _) => PluginStatus::Running,
277                    // `invalid` takes precedence over other states because the user probably wants
278                    // to fix it
279                    (_, PluginStatus::Invalid) => PluginStatus::Invalid,
280                    // Display `modified` if the state in the registry is different somehow
281                    _ if info_engine.is_modified(&info_registry) => PluginStatus::Modified,
282                    // Otherwise, `loaded` (it's not running)
283                    _ => PluginStatus::Loaded,
284                },
285                ..info_engine
286            },
287        })
288        .sorted()
289        .collect()
290}
291
292impl PluginInfo {
293    /// True if the plugin info shows some kind of change (other than status/pid) relative to the
294    /// other
295    fn is_modified(&self, other: &PluginInfo) -> bool {
296        self.name != other.name
297            || self.filename != other.filename
298            || self.shell != other.shell
299            || self.commands != other.commands
300    }
301}