nu_cmd_plugin/commands/plugin/
list.rs1use 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<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 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 engine_state
176 .plugins()
177 .iter()
178 .map(|plugin| {
179 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
248fn 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 EitherOrBoth::Left(info) => PluginInfo {
263 status: match info.status {
264 PluginStatus::Running => info.status,
265 _ => PluginStatus::Removed,
267 },
268 ..info
269 },
270 EitherOrBoth::Right(info) => info,
272 EitherOrBoth::Both(info_engine, info_registry) => PluginInfo {
274 status: match (info_engine.status, info_registry.status) {
275 (PluginStatus::Running, _) => PluginStatus::Running,
277 (_, PluginStatus::Invalid) => PluginStatus::Invalid,
280 _ if info_engine.is_modified(&info_registry) => PluginStatus::Modified,
282 _ => PluginStatus::Loaded,
284 },
285 ..info_engine
286 },
287 })
288 .sorted()
289 .collect()
290}
291
292impl PluginInfo {
293 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}