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 (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 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 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 engine_state
194 .plugins()
195 .iter()
196 .map(|plugin| {
197 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
279fn 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 EitherOrBoth::Left(info) => PluginInfo {
294 status: match info.status {
295 PluginStatus::Running => info.status,
296 _ => PluginStatus::Removed,
298 },
299 ..info
300 },
301 EitherOrBoth::Right(info) => info,
303 EitherOrBoth::Both(info_engine, info_registry) => PluginInfo {
305 status: match (info_engine.status, info_registry.status) {
306 (PluginStatus::Running, _) => PluginStatus::Running,
308 (_, PluginStatus::Invalid) => PluginStatus::Invalid,
311 _ if info_engine.is_modified(&info_registry) => PluginStatus::Modified,
313 _ => PluginStatus::Loaded,
315 },
316 ..info_engine
317 },
318 })
319 .sorted()
320 .collect()
321}
322
323impl PluginInfo {
324 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}