Skip to main content

nu_cli/
config_files.rs

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