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::{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 the absolute directory of the plugin file first.
173            if let Ok(path_dir) = absolute_with(path_dir, &cwd)
174                && path_dir.exists()
175            {
176                // Get an absolute path to the file.
177                // We don't need to check if it exists.
178                let path = path_dir.join(path.file_name().unwrap_or(path.as_os_str()));
179                let path = absolute_with(&path, &cwd).unwrap_or(path);
180                engine_state.plugin_path = Some(path)
181            } else {
182                // It's an error if the directory for the plugin file doesn't exist.
183                report_parse_error(
184                    None,
185                    &StateWorkingSet::new(engine_state),
186                    &ParseError::FileNotFound(
187                        path_dir.to_string_lossy().into_owned(),
188                        plugin_file.span,
189                    ),
190                );
191            }
192        } else if let Some(plugin_path) = nu_path::nu_config_dir() {
193            // Path to store plugins signatures
194            let mut plugin_path = absolute_with(&plugin_path, &cwd).unwrap_or(plugin_path.into());
195            plugin_path.push(PLUGIN_FILE);
196            let plugin_path = absolute_with(&plugin_path, &cwd).unwrap_or(plugin_path);
197            engine_state.plugin_path = Some(plugin_path);
198        }
199    }
200}
201
202pub fn eval_config_contents(
203    config_path: PathBuf,
204    engine_state: &mut EngineState,
205    stack: &mut Stack,
206    strict_mode: bool,
207) {
208    if config_path.exists() & config_path.is_file() {
209        let config_filename = config_path.to_string_lossy();
210
211        if let Ok(contents) = std::fs::read(&config_path) {
212            // Set the current active file to the config file.
213            let prev_file = engine_state.file.take();
214            engine_state.file = Some(config_path.clone());
215
216            // TODO: ignore this error?
217            let exit_code = eval_source(
218                engine_state,
219                stack,
220                &contents,
221                &config_filename,
222                PipelineData::empty(),
223                false,
224            );
225            if exit_code != 0 && strict_mode {
226                std::process::exit(exit_code)
227            }
228
229            // Restore the current active file.
230            engine_state.file = prev_file;
231
232            // Merge the environment in case env vars changed in the config
233            if let Err(e) = engine_state.merge_env(stack) {
234                report_shell_error(Some(stack), engine_state, &e);
235            }
236        }
237    }
238}
239
240#[cfg(feature = "plugin")]
241pub fn migrate_old_plugin_file(engine_state: &EngineState) -> bool {
242    use nu_protocol::{
243        PluginExample, PluginIdentity, PluginRegistryItem, PluginRegistryItemData, PluginSignature,
244        ShellError, shell_error::io::IoError,
245    };
246    use std::collections::BTreeMap;
247
248    let start_time = std::time::Instant::now();
249
250    let Ok(cwd) = engine_state.cwd_as_string(None) else {
251        return false;
252    };
253
254    let Some(config_dir) =
255        nu_path::nu_config_dir().and_then(|dir| nu_path::absolute_with(dir, &cwd).ok())
256    else {
257        return false;
258    };
259
260    let Ok(old_plugin_file_path) = nu_path::absolute_with(OLD_PLUGIN_FILE, &config_dir) else {
261        return false;
262    };
263
264    if !config_dir.exists() || !old_plugin_file_path.exists() {
265        return false;
266    }
267
268    let old_contents = match std::fs::read(&old_plugin_file_path) {
269        Ok(old_contents) => old_contents,
270        Err(err) => {
271            report_shell_error(
272                None,
273                engine_state,
274                &ShellError::GenericError {
275                    error: "Can't read old plugin file to migrate".into(),
276                    msg: "".into(),
277                    span: None,
278                    help: Some(err.to_string()),
279                    inner: vec![],
280                },
281            );
282            return false;
283        }
284    };
285
286    // Make a copy of the engine state, because we'll read the newly generated file
287    let mut engine_state = engine_state.clone();
288    let mut stack = Stack::new();
289
290    if eval_source(
291        &mut engine_state,
292        &mut stack,
293        &old_contents,
294        &old_plugin_file_path.to_string_lossy(),
295        PipelineData::empty(),
296        false,
297    ) != 0
298    {
299        return false;
300    }
301
302    // Now that the plugin commands are loaded, we just have to generate the file
303    let mut contents = PluginRegistryFile::new();
304
305    let mut groups = BTreeMap::<PluginIdentity, Vec<PluginSignature>>::new();
306
307    for decl in engine_state.plugin_decls() {
308        if let Some(identity) = decl.plugin_identity() {
309            groups
310                .entry(identity.clone())
311                .or_default()
312                .push(PluginSignature {
313                    sig: decl.signature(),
314                    examples: decl
315                        .examples()
316                        .into_iter()
317                        .map(PluginExample::from)
318                        .collect(),
319                })
320        }
321    }
322
323    for (identity, commands) in groups {
324        contents.upsert_plugin(PluginRegistryItem {
325            name: identity.name().to_owned(),
326            filename: identity.filename().to_owned(),
327            shell: identity.shell().map(|p| p.to_owned()),
328            data: PluginRegistryItemData::Valid {
329                metadata: Default::default(),
330                commands,
331            },
332        });
333    }
334
335    // Write the new file
336    let new_plugin_file_path = config_dir.join(PLUGIN_FILE);
337    if let Err(err) = std::fs::File::create(&new_plugin_file_path)
338        .map_err(|err| {
339            IoError::new_internal_with_path(
340                err,
341                "Could not create new plugin file",
342                nu_protocol::location!(),
343                new_plugin_file_path.clone(),
344            )
345        })
346        .map_err(ShellError::from)
347        .and_then(|file| contents.write_to(file, None))
348    {
349        report_shell_error(
350            None,
351            &engine_state,
352            &ShellError::GenericError {
353                error: "Failed to save migrated plugin file".into(),
354                msg: "".into(),
355                span: None,
356                help: Some("ensure `$nu.plugin-path` is writable".into()),
357                inner: vec![err],
358            },
359        );
360        return false;
361    }
362
363    if engine_state.is_interactive {
364        eprintln!(
365            "Your old plugin.nu file has been migrated to the new format: {}",
366            new_plugin_file_path.display()
367        );
368        eprintln!(
369            "The plugin.nu file has not been removed. If `plugin list` looks okay, \
370            you may do so manually."
371        );
372    }
373
374    perf!(
375        "migrate old plugin file",
376        start_time,
377        engine_state
378            .get_config()
379            .use_ansi_coloring
380            .get(&engine_state)
381    );
382    true
383}