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