diff --git a/README.md b/README.md index aae963c7..87eb44c4 100644 --- a/README.md +++ b/README.md @@ -1415,6 +1415,23 @@ which will halt execution. `requirement`, e.g., `">=0.1.0"`, returning `"true"` if so and `"false"` otherwise. +##### XDG Directories + +These functions return paths to user-specific directories for things like +configuration, data, caches, executables, and the user's home directory. These +functions follow the +[XDG Base Directory Specification](https://specifications.freedesktop.org/basedir-spec/basedir-spec-latest.html), +and are implemented with the +[`dirs`](https://docs.rs/dirs/latest/dirs/index.html) crate. + +- `cache_directory()` - The user-specific cache directory. +- `config_directory()` - The user-specific configuration directory. +- `config_local_directory()` - The local user-specific configuration directory. +- `data_directory()` - The user-specific data directory. +- `data_local_directory()` - The local user-specific data directory. +- `executable_directory()` - The user-specific executable directory. +- `home_directory()` - The user's home directory. + ### Recipe Attributes Recipes may be annotated with attributes that change their behavior. diff --git a/src/function.rs b/src/function.rs index 086ede1b..c4923e59 100644 --- a/src/function.rs +++ b/src/function.rs @@ -21,15 +21,22 @@ pub(crate) fn get(name: &str) -> Option { let function = match name { "absolute_path" => Unary(absolute_path), "arch" => Nullary(arch), + "cache_directory" => Nullary(|_| dir("cache", dirs::cache_dir)), "capitalize" => Unary(capitalize), "clean" => Unary(clean), + "config_directory" => Nullary(|_| dir("config", dirs::config_dir)), + "config_local_directory" => Nullary(|_| dir("local config", dirs::config_local_dir)), + "data_directory" => Nullary(|_| dir("data", dirs::data_dir)), + "data_local_directory" => Nullary(|_| dir("local data", dirs::data_local_dir)), "env" => UnaryOpt(env), "env_var" => Unary(env_var), "env_var_or_default" => Binary(env_var_or_default), "error" => Unary(error), + "executable_directory" => Nullary(|_| dir("executable", dirs::executable_dir)), "extension" => Unary(extension), "file_name" => Unary(file_name), "file_stem" => Unary(file_stem), + "home_directory" => Nullary(|_| dir("home", dirs::home_dir)), "invocation_directory" => Nullary(invocation_directory), "invocation_directory_native" => Nullary(invocation_directory_native), "join" => BinaryPlus(join), @@ -114,6 +121,22 @@ fn clean(_context: &FunctionContext, path: &str) -> Result { Ok(Path::new(path).lexiclean().to_str().unwrap().to_owned()) } +fn dir(name: &'static str, f: fn() -> Option) -> Result { + match f() { + Some(path) => path + .as_os_str() + .to_str() + .map(str::to_string) + .ok_or_else(|| { + format!( + "unable to convert {name} directory path to string: {}", + path.display(), + ) + }), + None => Err(format!("{name} directory not found")), + } +} + fn env_var(context: &FunctionContext, key: &str) -> Result { use std::env::VarError::*; @@ -433,3 +456,23 @@ fn semver_matches( .to_string(), ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn dir_not_found() { + assert_eq!(dir("foo", || None).unwrap_err(), "foo directory not found"); + } + + #[cfg(unix)] + #[test] + fn dir_not_unicode() { + use std::os::unix::ffi::OsStrExt; + assert_eq!( + dir("foo", || Some(OsStr::from_bytes(b"\xe0\x80\x80").into())).unwrap_err(), + "unable to convert foo directory path to string: ���", + ); + } +} diff --git a/tests/directories.rs b/tests/directories.rs new file mode 100644 index 00000000..41a05cd6 --- /dev/null +++ b/tests/directories.rs @@ -0,0 +1,85 @@ +use super::*; + +#[test] +fn cache_directory() { + Test::new() + .justfile("x := cache_directory()") + .args(["--evaluate", "x"]) + .stdout(dirs::cache_dir().unwrap_or_default().to_string_lossy()) + .run(); +} + +#[test] +fn config_directory() { + Test::new() + .justfile("x := config_directory()") + .args(["--evaluate", "x"]) + .stdout(dirs::config_dir().unwrap_or_default().to_string_lossy()) + .run(); +} + +#[test] +fn config_local_directory() { + Test::new() + .justfile("x := config_local_directory()") + .args(["--evaluate", "x"]) + .stdout( + dirs::config_local_dir() + .unwrap_or_default() + .to_string_lossy(), + ) + .run(); +} + +#[test] +fn data_directory() { + Test::new() + .justfile("x := data_directory()") + .args(["--evaluate", "x"]) + .stdout(dirs::data_dir().unwrap_or_default().to_string_lossy()) + .run(); +} + +#[test] +fn data_local_directory() { + Test::new() + .justfile("x := data_local_directory()") + .args(["--evaluate", "x"]) + .stdout(dirs::data_local_dir().unwrap_or_default().to_string_lossy()) + .run(); +} + +#[test] +fn executable_directory() { + if let Some(executable_dir) = dirs::executable_dir() { + Test::new() + .justfile("x := executable_directory()") + .args(["--evaluate", "x"]) + .stdout(executable_dir.to_string_lossy()) + .run(); + } else { + Test::new() + .justfile("x := executable_directory()") + .args(["--evaluate", "x"]) + .stderr( + " + error: Call to function `executable_directory` failed: executable directory not found + ——▶ justfile:1:6 + │ + 1 │ x := executable_directory() + │ ^^^^^^^^^^^^^^^^^^^^ + ", + ) + .status(EXIT_FAILURE) + .run(); + } +} + +#[test] +fn home_directory() { + Test::new() + .justfile("x := home_directory()") + .args(["--evaluate", "x"]) + .stdout(dirs::home_dir().unwrap_or_default().to_string_lossy()) + .run(); +} diff --git a/tests/lib.rs b/tests/lib.rs index 24d3ffa2..a9f3c344 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -44,6 +44,7 @@ mod completions; mod conditional; mod confirm; mod delimiters; +mod directories; mod dotenv; mod edit; mod equals;