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 (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 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 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 engine_state
182 .plugins()
183 .iter()
184 .map(|plugin| {
185 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
267fn 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 EitherOrBoth::Left(info) => PluginInfo {
282 status: match info.status {
283 PluginStatus::Running => info.status,
284 _ => PluginStatus::Removed,
286 },
287 ..info
288 },
289 EitherOrBoth::Right(info) => info,
291 EitherOrBoth::Both(info_engine, info_registry) => PluginInfo {
293 status: match (info_engine.status, info_registry.status) {
294 (PluginStatus::Running, _) => PluginStatus::Running,
296 (_, PluginStatus::Invalid) => PluginStatus::Invalid,
299 _ if info_engine.is_modified(&info_registry) => PluginStatus::Modified,
301 _ => PluginStatus::Loaded,
303 },
304 ..info_engine
305 },
306 })
307 .sorted()
308 .collect()
309}
310
311impl PluginInfo {
312 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}