Skip to main content

nu_cli/
config_files.rs

1use crate::util::eval_source;
2#[cfg(feature = "plugin")]
3use nu_path::absolute_with;
4#[cfg(feature = "plugin")]
5use nu_protocol::shell_error::generic::GenericError;
6#[cfg(feature = "plugin")]
7use nu_protocol::{ParseError, PluginRegistryFile, Spanned, engine::StateWorkingSet};
8use nu_protocol::{
9    PipelineData,
10    engine::{EngineState, Stack},
11    report_shell_error,
12};
13#[cfg(feature = "plugin")]
14use nu_utils::perf;
15#[cfg(feature = "plugin")]
16use nu_utils::time::Instant;
17use std::path::PathBuf;
18
19#[cfg(feature = "plugin")]
20const PLUGIN_FILE: &str = "plugin.msgpackz";
21#[cfg(feature = "plugin")]
22const OLD_PLUGIN_FILE: &str = "plugin.nu";
23
24#[cfg(feature = "plugin")]
25pub fn read_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
26    use nu_protocol::{ShellError, shell_error::io::IoError};
27    use std::path::Path;
28
29    let span = plugin_file.as_ref().map(|s| s.span);
30
31    // Check and warn + abort if this is a .nu plugin file
32    if plugin_file
33        .as_ref()
34        .and_then(|p| Path::new(&p.item).extension())
35        .is_some_and(|ext| ext == "nu")
36    {
37        let error = "Wrong plugin file format";
38        let msg = ".nu plugin files are no longer supported";
39        report_shell_error(
40            None,
41            engine_state,
42            &ShellError::Generic(
43                match span {
44                    Some(span) => GenericError::new(error, msg, span),
45                    None => GenericError::new_internal(error, msg),
46                }
47                .with_help("please recreate this file in the new .msgpackz format"),
48            ),
49        );
50        return;
51    }
52
53    let mut start_time = Instant::now();
54    // Reading signatures from plugin registry file
55    // The plugin.msgpackz file stores the parsed signature collected from each registered plugin
56    add_plugin_file(engine_state, plugin_file.clone());
57    perf!(
58        "add plugin file to engine_state",
59        start_time,
60        engine_state
61            .get_config()
62            .use_ansi_coloring
63            .get(engine_state)
64    );
65
66    start_time = Instant::now();
67    let plugin_path = engine_state.plugin_path.clone();
68    if let Some(plugin_path) = plugin_path {
69        // Open the plugin file
70        let mut file = match std::fs::File::open(&plugin_path) {
71            Ok(file) => file,
72            Err(err) => {
73                if err.kind() == std::io::ErrorKind::NotFound {
74                    log::warn!("Plugin file not found: {}", plugin_path.display());
75
76                    // Try migration of an old plugin file if this wasn't a custom plugin file
77                    if plugin_file.is_none() && migrate_old_plugin_file(engine_state) {
78                        let Ok(file) = std::fs::File::open(&plugin_path) else {
79                            log::warn!("Failed to load newly migrated plugin file");
80                            return;
81                        };
82                        file
83                    } else {
84                        return;
85                    }
86                } else {
87                    report_shell_error(
88                        None,
89                        engine_state,
90                        &ShellError::Io(IoError::new_internal_with_path(
91                            err,
92                            "Could not open plugin registry file",
93                            plugin_path,
94                        )),
95                    );
96                    return;
97                }
98            }
99        };
100
101        // Abort if the file is empty.
102        if file.metadata().is_ok_and(|m| m.len() == 0) {
103            log::warn!(
104                "Not reading plugin file because it's empty: {}",
105                plugin_path.display()
106            );
107            return;
108        }
109
110        // Read the contents of the plugin file
111        let contents = match PluginRegistryFile::read_from(&mut file, span) {
112            Ok(contents) => contents,
113            Err(err) => {
114                log::warn!("Failed to read plugin registry file: {err:?}");
115                let error = format!(
116                    "Error while reading plugin registry file: {}",
117                    plugin_path.display()
118                );
119                let msg = "plugin path defined here";
120                report_shell_error(
121                    None,
122                    engine_state,
123                    &ShellError::Generic(
124                        match span {
125                            Some(span) => GenericError::new(error, msg, span),
126                            None => GenericError::new_internal(error, msg),
127                        }
128                        .with_help(
129                            "you might try deleting the file and registering all of your plugins again",
130                        ),
131                    ),
132                );
133                return;
134            }
135        };
136
137        perf!(
138            &format!("read plugin file {}", plugin_path.display()),
139            start_time,
140            engine_state
141                .get_config()
142                .use_ansi_coloring
143                .get(engine_state)
144        );
145        start_time = Instant::now();
146
147        let mut working_set = StateWorkingSet::new(engine_state);
148
149        let plugin_load_errors =
150            nu_plugin_engine::load_plugin_file(&mut working_set, &contents, span);
151
152        if plugin_load_errors > 0 {
153            let error = format!(
154                "Failed to load {plugin_load_errors} plugin entr{} from {}",
155                if plugin_load_errors == 1 { "y" } else { "ies" },
156                plugin_path.display(),
157            );
158            let msg = "plugins with incompatible or invalid registry data were skipped";
159            let help = "run `plugin list` and re-add outdated plugins with `plugin add`";
160            let generic_error = match span {
161                Some(span) => GenericError::new(error, msg, span),
162                None => GenericError::new_internal(error, msg),
163            };
164            report_shell_error(
165                None,
166                engine_state,
167                &ShellError::Generic(generic_error.with_help(help)),
168            );
169        }
170
171        if let Err(err) = engine_state.merge_delta(working_set.render()) {
172            report_shell_error(None, engine_state, &err);
173            return;
174        }
175
176        perf!(
177            &format!("load plugin file {}", plugin_path.display()),
178            start_time,
179            engine_state
180                .get_config()
181                .use_ansi_coloring
182                .get(engine_state)
183        );
184    }
185}
186
187#[cfg(feature = "plugin")]
188pub fn add_plugin_file(engine_state: &mut EngineState, plugin_file: Option<Spanned<String>>) {
189    use std::path::Path;
190
191    use nu_protocol::report_parse_error;
192
193    if let Ok(cwd) = engine_state.cwd_as_string(None) {
194        if let Some(plugin_file) = plugin_file {
195            let path = Path::new(&plugin_file.item);
196            let path_dir = path.parent().unwrap_or(path);
197            // Just try the absolute directory of the plugin file first.
198            if let Ok(path_dir) = absolute_with(path_dir, &cwd)
199                && path_dir.exists()
200            {
201                // Get an absolute path to the file.
202                // We don't need to check if it exists.
203                let path = path_dir.join(path.file_name().unwrap_or(path.as_os_str()));
204                let path = absolute_with(&path, &cwd).unwrap_or(path);
205                engine_state.plugin_path = Some(path)
206            } else {
207                // It's an error if the directory for the plugin file doesn't exist.
208                report_parse_error(
209                    None,
210                    &StateWorkingSet::new(engine_state),
211                    &ParseError::FileNotFound(
212                        path_dir.to_string_lossy().into_owned(),
213                        plugin_file.span,
214                    ),
215                );
216            }
217        } else if let Some(plugin_path) = nu_path::nu_config_dir() {
218            // Path to store plugins signatures
219            let mut plugin_path = absolute_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
220            plugin_path.push(PLUGIN_FILE);
221            let plugin_path = absolute_with(&plugin_path, &cwd).unwrap_or(plugin_path);
222            engine_state.plugin_path = Some(plugin_path);
223        }
224    }
225}
226
227pub fn eval_config_contents(
228    config_path: PathBuf,
229    engine_state: &mut EngineState,
230    stack: &mut Stack,
231    strict_mode: bool,
232) {
233    if config_path.exists() & config_path.is_file() {
234        let config_filename = config_path.to_string_lossy();
235
236        if let Ok(contents) = std::fs::read(&config_path) {
237            // Set the current active file to the config file.
238            let prev_file = engine_state.file.take();
239            engine_state.file = Some(config_path.clone());
240
241            // TODO: ignore this error?
242            let exit_code = eval_source(
243                engine_state,
244                stack,
245                &contents,
246                &config_filename,
247                PipelineData::empty(),
248                false,
249            );
250            if exit_code != 0 && strict_mode {
251                std::process::exit(exit_code)
252            }
253
254            // Restore the current active file.
255            engine_state.file = prev_file;
256
257            // Merge the environment in case env vars changed in the config
258            if let Err(e) = engine_state.merge_env(stack) {
259                report_shell_error(Some(stack), engine_state, &e);
260            }
261        }
262    }
263}
264
265#[cfg(feature = "plugin")]
266pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
267    use nu_protocol::{
268        PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
269        ShellError, shell_error::io::IoError,
270    };
271    use std::collections::BTreeMap;
272
273    let start_time = Instant::now();
274
275    let Ok(cwd) = engine_state.cwd_as_string(None) else {
276        return false;
277    };
278
279    let Some(config_dir) =
280        nu_path::nu_config_dir().and_then(|dir| nu_path::absolute_with(dir, &cwd).ok())
281    else {
282        return false;
283    };
284
285    let Ok(old_plugin_file_path) = nu_path::absolute_with(OLD_PLUGIN_FILE, &config_dir) else {
286        return false;
287    };
288
289    if !config_dir.exists() || !old_plugin_file_path.exists() {
290        return false;
291    }
292
293    let old_contents = match std::fs::read(&old_plugin_file_path) {
294        Ok(old_contents) => old_contents,
295        Err(err) => {
296            report_shell_error(
297                None,
298                engine_state,
299                &ShellError::Generic(
300                    GenericError::new_internal("Can't read old plugin file to migrate", "")
301                        .with_help(err.to_string()),
302                ),
303            );
304            return false;
305        }
306    };
307
308    // Make a copy of the engine state, because we'll read the newly generated file
309    let mut engine_state = engine_state.clone();
310    let mut stack = Stack::new();
311
312    if eval_source(
313        &mut engine_state,
314        &mut stack,
315        &old_contents,
316        &old_plugin_file_path.to_string_lossy(),
317        PipelineData::empty(),
318        false,
319    ) != 0
320    {
321        return false;
322    }
323
324    // Now that the plugin commands are loaded, we just have to generate the file
325    let mut contents = PluginRegistryFile::new();
326
327    let mut groups = BTreeMap::<PluginIdentity, Vec<PluginSignature>>::new();
328
329    for decl in engine_state.plugin_decls() {
330        if let Some(identity) = decl.plugin_identity() {
331            groups
332                .entry(identity.clone())
333                .or_default()
334                .push(PluginSignature {
335                    sig: decl.signature(),
336                    examples: decl
337                        .examples()
338                        .into_iter()
339                        .map(PluginExample::from)
340                        .collect(),
341                })
342        }
343    }
344
345    for (identity, commands) in groups {
346        contents.upsert_plugin(PluginRegistryItem {
347            name: identity.name().to_owned(),
348            filename: identity.filename().to_owned(),
349            shell: identity.shell().map(|p| p.to_owned()),
350            data: PluginRegistryItemData::Valid {
351                metadata: Default::default(),
352                commands,
353            },
354        });
355    }
356
357    // Write the new file
358    let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
359    if let Err(err) = std::fs::File::create(&new_plugin_file_path)
360        .map_err(|err| {
361            IoError::new_internal_with_path(
362                err,
363                "Could not create new plugin file",
364                new_plugin_file_path.clone(),
365            )
366        })
367        .map_err(ShellError::from)
368        .and_then(|file| contents.write_to(file, None))
369    {
370        report_shell_error(
371            None,
372            &engine_state,
373            &ShellError::Generic(
374                GenericError::new_internal("Failed to save migrated plugin file", "")
375                    .with_help("ensure `$nu.plugin-path` is writable")
376                    .with_inner([err]),
377            ),
378        );
379        return false;
380    }
381
382    if engine_state.is_interactive {
383        eprintln!(
384            "Your old plugin.nu file has been migrated to the new format: {}",
385            new_plugin_file_path.display()
386        );
387        eprintln!(
388            "The plugin.nu file has not been removed. If `plugin list` looks okay, \
389            you may do so manually."
390        );
391    }
392
393    perf!(
394        "migrate old plugin file",
395        start_time,
396        engine_state
397            .get_config()
398            .use_ansi_coloring
399            .get(&engine_state)
400    );
401    true
402}