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