nu_cli/
config_files.rs

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