mirror of
https://github.com/nushell/nushell
synced 2024-12-26 13:03:07 +00:00
Use XDG_CONFIG_HOME before default config directory (#12118)
<!-- if this PR closes one or more issues, you can automatically link the PR with them by using one of the [*linking keywords*](https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword), e.g. - this PR should close #xxxx - fixes #xxxx you can also mention related issues, PRs or discussions! --> Closes #12103 # Description <!-- Thank you for improving Nushell. Please, check our [contributing guide](../CONTRIBUTING.md) and talk to the core team before making major changes. Description of your pull request goes here. **Provide examples and/or screenshots** if your changes affect the user experience. --> 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. <s>@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</s> 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 <!-- List of all changes that impact the user experience here. This helps us keep track of breaking 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 <!-- Don't forget to add tests that cover your changes. Make sure you've run and fixed any issues with these commands: - `cargo fmt --all -- --check` to check standard code formatting (`cargo fmt --all` applies these changes) - `cargo clippy --workspace -- -D warnings -D clippy::unwrap_used` to check that you're using the standard code style - `cargo test --workspace` to check that all tests pass (on Windows make sure to [enable developer mode](https://learn.microsoft.com/en-us/windows/apps/get-started/developer-mode-features-and-debugging)) - `cargo run -- -c "use std testing; testing run-tests --path crates/nu-std"` to run the tests for the standard library > **Note** > from `nushell` you can also use the `toolkit` as follows > ```bash > use toolkit.nu # or use an `env_change` hook to activate it automatically > toolkit check pr > ``` --> 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 <!-- If your PR had any user-facing changes, update [the documentation](https://github.com/nushell/nushell.github.io) after the PR is merged, if necessary. This will help us keep the docs up to date. --> 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.
This commit is contained in:
parent
afce380530
commit
f6853fd636
7 changed files with 113 additions and 43 deletions
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -2703,6 +2703,7 @@ dependencies = [
|
|||
"assert_cmd",
|
||||
"crossterm",
|
||||
"ctrlc",
|
||||
"dirs-next",
|
||||
"divan",
|
||||
"log",
|
||||
"miette",
|
||||
|
|
|
@ -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 }
|
||||
|
|
|
@ -7,7 +7,18 @@ pub fn home_dir() -> Option<PathBuf> {
|
|||
}
|
||||
|
||||
pub fn config_dir() -> Option<PathBuf> {
|
||||
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<PathBuf> {
|
||||
let path = dirs_next::config_dir()?;
|
||||
Some(canonicalize(&path).unwrap_or(path))
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1341,6 +1341,14 @@ On Windows, this would be %USERPROFILE%\AppData\Roaming"#
|
|||
#[label = "Could not find config directory"]
|
||||
span: Option<Span>,
|
||||
},
|
||||
|
||||
/// 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
|
||||
|
|
34
src/main.rs
34
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(
|
||||
|
|
|
@ -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: AsRef<Path>>(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()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue