Skip to main content

nu_config/
overrides.rs

1use std::path::{Path, PathBuf};
2
3/// CLI flags that affect *where* configuration files are found.
4///
5/// Only path-resolution flags belong here. Flags that control *whether* config
6/// files are loaded (e.g. `--no-config-file`) stay in `NushellCliArgs`.
7///
8/// Relative paths are resolved against the provided cwd in
9/// [`CliOverrides::from_path_strings`] — that is the single place absolute-ization
10/// of CLI path overrides happens.
11#[derive(Debug, Clone, Default)]
12pub struct CliOverrides {
13    /// `--config-home <path>` — override the entire nushell config directory
14    pub config_home: Option<PathBuf>,
15
16    /// `--config <file>` — override `config.nu` path
17    pub config_file: Option<PathBuf>,
18
19    /// `--env-config <file>` — override `env.nu` path
20    pub env_file: Option<PathBuf>,
21
22    /// `--plugin-config <file>` — override plugin registry file path
23    #[cfg(feature = "plugin")]
24    pub plugin_file: Option<PathBuf>,
25}
26
27impl CliOverrides {
28    /// Build overrides from optional CLI path strings, resolving any relative
29    /// path against `cwd`.
30    ///
31    /// This is the **only** place CLI config path absolute-ization should live.
32    ///
33    /// Resolution is **logical** (not realpath): tilde is expanded via the
34    /// user's home directory, relative segments are joined to `cwd`, and `.` /
35    /// `..` are stripped lexically. Symlinks in the path are **not** followed.
36    pub fn from_path_strings(
37        config_home: Option<&str>,
38        config_file: Option<&str>,
39        env_file: Option<&str>,
40        #[cfg(feature = "plugin")] plugin_file: Option<&str>,
41        cwd: &Path,
42    ) -> Self {
43        Self {
44            config_home: config_home.map(|s| absolutize_cli_path(s, cwd)),
45            config_file: config_file.map(|s| absolutize_cli_path(s, cwd)),
46            env_file: env_file.map(|s| absolutize_cli_path(s, cwd)),
47            #[cfg(feature = "plugin")]
48            plugin_file: plugin_file.map(|s| absolutize_cli_path(s, cwd)),
49        }
50    }
51}
52
53/// Resolve a CLI path to a logical absolute path against `cwd`.
54///
55/// Uses [`nu_path::expand_path_with`]: expands a leading `~`, joins relative
56/// paths to `cwd`, and lexically normalizes `.` / `..`. Does **not**
57/// canonicalize or follow symlinks — so `~/.config/nushell` becomes
58/// `$HOME/.config/nushell` even when `$HOME` itself is a symlink chain.
59fn absolutize_cli_path(path: &str, cwd: &Path) -> PathBuf {
60    nu_path::expand_path_with(path, cwd, true)
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66    use std::path::Component;
67
68    #[test]
69    fn relative_paths_join_cwd() {
70        let cwd = PathBuf::from("/tmp/work");
71        let cli = CliOverrides::from_path_strings(
72            Some("cfg-home"),
73            Some("config.nu"),
74            Some("env.nu"),
75            #[cfg(feature = "plugin")]
76            Some("plugin.msgpackz"),
77            &cwd,
78        );
79        assert_eq!(
80            cli.config_home.as_deref(),
81            Some(Path::new("/tmp/work/cfg-home"))
82        );
83        assert_eq!(
84            cli.config_file.as_deref(),
85            Some(Path::new("/tmp/work/config.nu"))
86        );
87        assert_eq!(cli.env_file.as_deref(), Some(Path::new("/tmp/work/env.nu")));
88        #[cfg(feature = "plugin")]
89        assert_eq!(
90            cli.plugin_file.as_deref(),
91            Some(Path::new("/tmp/work/plugin.msgpackz"))
92        );
93    }
94
95    #[test]
96    fn absolute_paths_kept() {
97        let cwd = PathBuf::from("/tmp/work");
98        let abs = if cfg!(windows) {
99            r"C:\nushell\config.nu"
100        } else {
101            "/etc/nushell/config.nu"
102        };
103        let cli = CliOverrides::from_path_strings(
104            None,
105            Some(abs),
106            None,
107            #[cfg(feature = "plugin")]
108            None,
109            &cwd,
110        );
111        assert_eq!(cli.config_file.as_deref(), Some(Path::new(abs)));
112    }
113
114    #[test]
115    fn dot_config_home_is_cwd_without_trailing_curdir() {
116        let cwd = PathBuf::from("/tmp/work");
117        let cli = CliOverrides::from_path_strings(
118            Some("."),
119            None,
120            None,
121            #[cfg(feature = "plugin")]
122            None,
123            &cwd,
124        );
125        assert_eq!(cli.config_home.as_deref(), Some(cwd.as_path()));
126        assert!(
127            !cli.config_home
128                .as_ref()
129                .unwrap()
130                .components()
131                .any(|c| matches!(c, Component::CurDir))
132        );
133    }
134
135    #[test]
136    fn relative_dot_slash_subdir_normalized() {
137        let cwd = PathBuf::from("/tmp/work");
138        let cli = CliOverrides::from_path_strings(
139            Some("./subdir"),
140            Some("./cfg.nu"),
141            None,
142            #[cfg(feature = "plugin")]
143            None,
144            &cwd,
145        );
146        assert_eq!(
147            cli.config_home.as_deref(),
148            Some(Path::new("/tmp/work/subdir"))
149        );
150        assert_eq!(
151            cli.config_file.as_deref(),
152            Some(Path::new("/tmp/work/cfg.nu"))
153        );
154    }
155
156    #[test]
157    fn parent_dir_components_resolved_lexically() {
158        let cwd = PathBuf::from("/tmp/work/nested");
159        assert_eq!(
160            absolutize_cli_path("../sibling", &cwd),
161            PathBuf::from("/tmp/work/sibling")
162        );
163    }
164
165    #[test]
166    fn tilde_config_home_expands_to_user_home_without_cwd_join() {
167        let Some(home) = dirs::home_dir() else {
168            return; // environment without a home dir — skip
169        };
170        let cwd = PathBuf::from("/tmp/unrelated/cwd");
171        let cli = CliOverrides::from_path_strings(
172            Some("~/.config/nushell"),
173            None,
174            None,
175            #[cfg(feature = "plugin")]
176            None,
177            &cwd,
178        );
179        let expected = home.join(".config").join("nushell");
180        assert_eq!(
181            cli.config_home.as_deref(),
182            Some(expected.as_path()),
183            "tilde should expand via $HOME, not join under cwd"
184        );
185        // Must remain the logical home path (no accidental cwd prefix).
186        assert!(
187            !cli.config_home.as_ref().unwrap().starts_with(&cwd),
188            "must not resolve under cwd"
189        );
190    }
191
192    #[test]
193    fn bare_tilde_expands_to_home() {
194        let Some(home) = dirs::home_dir() else {
195            return;
196        };
197        let cwd = PathBuf::from("/tmp/unrelated");
198        assert_eq!(absolutize_cli_path("~", &cwd), home);
199    }
200}