From f6853fd636c5fd5acb7ee22dade9c1b643add8f9 Mon Sep 17 00:00:00 2001
From: Yash Thakur <45539777+ysthakur@users.noreply.github.com>
Date: Mon, 11 Mar 2024 07:15:46 -0400
Subject: [PATCH] Use XDG_CONFIG_HOME before default config directory (#12118)
Closes #12103
# Description
As described in #12103, this PR makes Nushell use `XDG_CONFIG_HOME` as
the config directory if it exists. Otherwise, it uses the old behavior,
which was to use `dirs_next::config_dir()`.
Edit: We discussed choosing between `XDG_CONFIG_HOME` and the default
config directory in Discord and decided against it, at least for now.
@kubouch also suggested letting users choose between
`XDG_CONFIG_HOME` and the default config directory if config files
aren't found on startup and `XDG_CONFIG_HOME` is set to a value
different from the default config directory
On Windows and MacOS, if the `XDG_CONFIG_HOME` variable is set but
`XDG_CONFIG_HOME` is either empty or doesn't exist *and* the old config
directory is non-empty, Nushell will issue a warning on startup saying
that it won't move files from the old config directory to the new one.
To do this, I had to add a `nu_path::config_dir_old()` function. I
assume that at some point, we will remove the warning message and the
function can be removed too. Alternatively, instead of having that
function there, `main.rs` could directly call `dirs_next::config_dir()`.
# User-Facing Changes
When `$env.XDG_CONFIG_HOME` is set to an absolute path, Nushell will use
`$"($env.XDG_CONFIG_HOME)/nushell"` as its config directory (previously,
this only worked on Linux).
To use `App Data\Roaming` (Windows) or `Library/Application Support`
(MacOS) instead (the old behavior), one can either leave
`XDG_CONFIG_HOME` unset or set it to an empty string.
If `XDG_CONFIG_HOME` is set, but to a non-absolute/invalid path, Nushell
will report an error on startup and use the default config directory
instead:
![image](https://github.com/nushell/nushell/assets/45539777/a434fe04-b7c8-4e95-b50c-80628008ad08)
On Windows and MacOS, if the `XDG_CONFIG_HOME` variable is set but
`XDG_CONFIG_HOME` is either empty or doesn't exist *and* the old config
directory is non-empty, Nushell will issue a warning on startup saying
that it won't move files from the old config directory to the new one.
![image](https://github.com/nushell/nushell/assets/45539777/1686cc17-4083-4c12-aecf-1d832460ca57)
# Tests + Formatting
The existing config path tests have been modified to use
`XDG_CONFIG_HOME` to change the config directory on all OSes, not just
Linux.
# After Submitting
The documentation will have to be updated to note that Nushell uses
`XDG_CONFIG_HOME` now. As @fdncred pointed out, it's possible for people
to set `XDG_CONFIG_HOME` to, say, `~/.config/nushell` rather than
`~/.config`, so the documentation could warn about that mistake.
---
Cargo.lock | 1 +
Cargo.toml | 1 +
crates/nu-path/src/helpers.rs | 13 ++-
crates/nu-path/src/lib.rs | 2 +-
crates/nu-protocol/src/errors/shell_error.rs | 8 ++
src/main.rs | 34 ++++++-
src/tests/test_config_path.rs | 97 ++++++++++++--------
7 files changed, 113 insertions(+), 43 deletions(-)
diff --git a/Cargo.lock b/Cargo.lock
index 66188da692..65bc3f5fd7 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2703,6 +2703,7 @@ dependencies = [
"assert_cmd",
"crossterm",
"ctrlc",
+ "dirs-next",
"divan",
"log",
"miette",
diff --git a/Cargo.toml b/Cargo.toml
index c0dbac1faa..4f4ed363ed 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -123,6 +123,7 @@ nix = { workspace = true, default-features = false, features = [
[dev-dependencies]
nu-test-support = { path = "./crates/nu-test-support", version = "0.91.1" }
assert_cmd = "2.0"
+dirs-next = "2.0"
divan = "0.1.14"
pretty_assertions = "1.4"
rstest = { workspace = true, default-features = false }
diff --git a/crates/nu-path/src/helpers.rs b/crates/nu-path/src/helpers.rs
index a3ce3b84cf..aaa53eab71 100644
--- a/crates/nu-path/src/helpers.rs
+++ b/crates/nu-path/src/helpers.rs
@@ -7,7 +7,18 @@ pub fn home_dir() -> Option {
}
pub fn config_dir() -> Option {
- dirs_next::config_dir().map(|path| canonicalize(&path).unwrap_or(path))
+ match std::env::var("XDG_CONFIG_HOME").map(PathBuf::from) {
+ Ok(xdg_config) if xdg_config.is_absolute() => {
+ Some(canonicalize(&xdg_config).unwrap_or(xdg_config))
+ }
+ _ => config_dir_old(),
+ }
+}
+
+/// Get the old default config directory. Outside of Linux, this will ignore `XDG_CONFIG_HOME`
+pub fn config_dir_old() -> Option {
+ let path = dirs_next::config_dir()?;
+ Some(canonicalize(&path).unwrap_or(path))
}
#[cfg(windows)]
diff --git a/crates/nu-path/src/lib.rs b/crates/nu-path/src/lib.rs
index 0ab69b9c67..63c0091892 100644
--- a/crates/nu-path/src/lib.rs
+++ b/crates/nu-path/src/lib.rs
@@ -5,6 +5,6 @@ mod tilde;
mod util;
pub use expansions::{canonicalize_with, expand_path_with, expand_to_real_path};
-pub use helpers::{config_dir, home_dir};
+pub use helpers::{config_dir, config_dir_old, home_dir};
pub use tilde::expand_tilde;
pub use util::trim_trailing_slash;
diff --git a/crates/nu-protocol/src/errors/shell_error.rs b/crates/nu-protocol/src/errors/shell_error.rs
index 1cc87a27cd..cf365947ed 100644
--- a/crates/nu-protocol/src/errors/shell_error.rs
+++ b/crates/nu-protocol/src/errors/shell_error.rs
@@ -1341,6 +1341,14 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
#[label = "Could not find config directory"]
span: Option,
},
+
+ /// XDG_CONFIG_HOME was set to an invalid path
+ #[error("$env.XDG_CONFIG_HOME ({xdg}) is invalid, using default config directory instead: {default}")]
+ #[diagnostic(
+ code(nu::shell::xdg_config_home_invalid),
+ help("Set XDG_CONFIG_HOME to an absolute path, or set it to an empty string to ignore it")
+ )]
+ InvalidXdgConfig { xdg: String, default: String },
}
// TODO: Implement as From trait
diff --git a/src/main.rs b/src/main.rs
index 4732058ad6..e292cbeba3 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -27,7 +27,7 @@ use nu_cmd_base::util::get_init_cwd;
use nu_lsp::LanguageServer;
use nu_protocol::{
engine::EngineState, eval_const::create_nu_constant, report_error_new, util::BufferedReader,
- PipelineData, RawStream, Span, Value, NU_VARIABLE_ID,
+ PipelineData, RawStream, ShellError, Span, Value, NU_VARIABLE_ID,
};
use nu_std::load_standard_library;
use nu_utils::utils::perf;
@@ -35,6 +35,7 @@ use run::{run_commands, run_file, run_repl};
use signals::ctrlc_protection;
use std::{
io::BufReader,
+ path::Path,
str::FromStr,
sync::{atomic::AtomicBool, Arc},
};
@@ -91,6 +92,37 @@ fn main() -> Result<()> {
std::path::PathBuf::new()
};
+ if let Ok(xdg_config_home) = std::env::var("XDG_CONFIG_HOME") {
+ if !xdg_config_home.is_empty() {
+ if nushell_config_path != Path::new(&xdg_config_home).join("nushell") {
+ report_error_new(
+ &engine_state,
+ &ShellError::InvalidXdgConfig {
+ xdg: xdg_config_home,
+ default: nushell_config_path.display().to_string(),
+ },
+ );
+ } else if let Some(old_config) = nu_path::config_dir_old().map(|p| p.join("nushell")) {
+ let xdg_config_empty = nushell_config_path
+ .read_dir()
+ .map_or(true, |mut dir| dir.next().is_none());
+ let old_config_empty = old_config
+ .read_dir()
+ .map_or(true, |mut dir| dir.next().is_none());
+ if !old_config_empty && xdg_config_empty {
+ eprintln!(
+ "WARNING: XDG_CONFIG_HOME has been set but {} is empty.\n",
+ nushell_config_path.display(),
+ );
+ eprintln!(
+ "Nushell will not move your configuration files from {}",
+ old_config.display()
+ );
+ }
+ }
+ }
+ }
+
let mut default_nu_lib_dirs_path = nushell_config_path.clone();
default_nu_lib_dirs_path.push("scripts");
engine_state.add_env_var(
diff --git a/src/tests/test_config_path.rs b/src/tests/test_config_path.rs
index 85d2e4ca4b..0b73df999e 100644
--- a/src/tests/test_config_path.rs
+++ b/src/tests/test_config_path.rs
@@ -1,7 +1,7 @@
use nu_test_support::nu;
use nu_test_support::playground::{Executable, Playground};
use pretty_assertions::assert_eq;
-use std::fs;
+use std::fs::{self, File};
use std::path::{Path, PathBuf};
#[cfg(not(target_os = "windows"))]
@@ -22,36 +22,16 @@ fn adjust_canonicalization>(p: P) -> String {
/// Make the config directory a symlink that points to a temporary folder.
/// Returns the path to the `nushell` config folder inside, via the symlink.
-///
-/// Need to figure out how to change config directory on Windows.
-#[cfg(any(target_os = "linux", target_os = "macos"))]
fn setup_fake_config(playground: &mut Playground) -> PathBuf {
- #[cfg(target_os = "linux")]
- {
- let config_dir = "config";
- let config_link = "config_link";
- playground.mkdir(&format!("{config_dir}/nushell"));
- playground.symlink(config_dir, config_link);
- playground.with_env(
- "XDG_CONFIG_HOME",
- &playground.cwd().join(config_link).display().to_string(),
- );
- playground.cwd().join(config_link).join("nushell")
- }
-
- #[cfg(target_os = "macos")]
- {
- let fake_home = "fake_home";
- let home_link = "home_link";
- let dir_end = "Library/Application Support/nushell";
- playground.mkdir(&format!("{fake_home}/{dir_end}"));
- playground.symlink(fake_home, home_link);
- playground.with_env(
- "HOME",
- &playground.cwd().join(home_link).display().to_string(),
- );
- playground.cwd().join(home_link).join(dir_end)
- }
+ let config_dir = "config";
+ let config_link = "config_link";
+ playground.mkdir(&format!("{config_dir}/nushell"));
+ playground.symlink(config_dir, config_link);
+ playground.with_env(
+ "XDG_CONFIG_HOME",
+ &playground.cwd().join(config_link).display().to_string(),
+ );
+ playground.cwd().join(config_link).join("nushell")
}
fn run(playground: &mut Playground, command: &str) -> String {
@@ -131,7 +111,6 @@ fn test_default_config_path() {
/// Make the config folder a symlink to a temporary folder without any config files
/// and see if the config files' paths are properly canonicalized
-#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_default_symlinked_config_path_empty() {
Playground::setup("symlinked_empty_config_dir", |_, playground| {
@@ -142,14 +121,16 @@ fn test_default_symlinked_config_path_empty() {
/// Like [[test_default_symlinked_config_path_empty]], but fill the temporary folder
/// with broken symlinks and see if they're properly canonicalized
-#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_default_symlink_config_path_broken_symlink_config_files() {
Playground::setup(
- "symlinked_cfg_dir_with_symlinked_cfg_files",
+ "symlinked_cfg_dir_with_symlinked_cfg_files_broken",
|_, playground| {
let fake_config_dir_nushell = setup_fake_config(playground);
+ let fake_dir = PathBuf::from("fake");
+ playground.mkdir(&fake_dir.display().to_string());
+
for config_file in [
"config.nu",
"env.nu",
@@ -158,12 +139,17 @@ fn test_default_symlink_config_path_broken_symlink_config_files() {
"login.nu",
"plugin.nu",
] {
- playground.symlink(
- format!("fake/{config_file}"),
- fake_config_dir_nushell.join(config_file),
- );
+ let fake_file = fake_dir.join(config_file);
+ File::create(playground.cwd().join(&fake_file)).unwrap();
+
+ playground.symlink(&fake_file, fake_config_dir_nushell.join(config_file));
}
+ // Windows doesn't allow creating a symlink without the file existing,
+ // so we first create original files for the symlinks, then delete them
+ // to break the symlinks
+ std::fs::remove_dir_all(playground.cwd().join(&fake_dir)).unwrap();
+
test_config_path_helper(playground, fake_config_dir_nushell);
},
);
@@ -171,11 +157,8 @@ fn test_default_symlink_config_path_broken_symlink_config_files() {
/// Like [[test_default_symlinked_config_path_empty]], but fill the temporary folder
/// with working symlinks to empty files and see if they're properly canonicalized
-#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn test_default_config_path_symlinked_config_files() {
- use std::fs::File;
-
Playground::setup(
"symlinked_cfg_dir_with_symlinked_cfg_files",
|_, playground| {
@@ -221,3 +204,37 @@ fn test_alternate_config_path() {
);
assert_eq!(actual.out, env_path.to_string_lossy().to_string());
}
+
+#[test]
+fn test_xdg_config_empty() {
+ Playground::setup("xdg_config_empty", |_, playground| {
+ playground.with_env("XDG_CONFIG_HOME", "");
+
+ let actual = nu!("$nu.default-config-dir");
+ assert_eq!(
+ actual.out,
+ dirs_next::config_dir()
+ .unwrap()
+ .join("nushell")
+ .display()
+ .to_string()
+ );
+ });
+}
+
+#[test]
+fn test_xdg_config_bad() {
+ Playground::setup("xdg_config_bad", |_, playground| {
+ playground.with_env("XDG_CONFIG_HOME", r#"mn2''6t\/k*((*&^//k//: "#);
+
+ let actual = nu!("$nu.default-config-dir");
+ assert_eq!(
+ actual.out,
+ dirs_next::config_dir()
+ .unwrap()
+ .join("nushell")
+ .display()
+ .to_string()
+ );
+ });
+}