From a343f5c80c88085e326bf39db7eed4286f9fb399 Mon Sep 17 00:00:00 2001 From: Greg Shuflin Date: Sun, 19 May 2024 02:29:13 -0700 Subject: [PATCH] Add `--global-justfile` flag (#1846) --- README.md | 27 ++++++++++--- completions/just.bash | 2 +- completions/just.elvish | 2 + completions/just.fish | 1 + completions/just.powershell | 2 + completions/just.zsh | 2 + src/config.rs | 75 ++++++++++++++++++++++--------------- src/search.rs | 51 ++++++++++++++++--------- src/search_config.rs | 2 + src/search_error.rs | 4 ++ src/source.rs | 1 + tests/global.rs | 65 ++++++++++++++++++++++++++++++++ tests/lib.rs | 2 + 13 files changed, 182 insertions(+), 54 deletions(-) create mode 100644 tests/global.rs diff --git a/README.md b/README.md index cb643ce..97b7d54 100644 --- a/README.md +++ b/README.md @@ -3268,13 +3268,30 @@ Before `just` was a fancy Rust program it was a tiny shell script that called `make`. You can find the old version in [contrib/just.sh](https://github.com/casey/just/blob/master/contrib/just.sh). -### User `justfile`s +### Global and User `justfile`s If you want some recipes to be available everywhere, you have a few options. -First, create a `justfile` in `~/.user.justfile` with some recipes. +#### Global Justfile -#### Recipe Aliases +`just --global-justfile`, or `just -g` for short, searches the following paths, +in-order, for a justfile: + +- `$XDG_CONFIG_HOME/just/justfile` +- `$HOME/.config/just/justfile` +- `$HOME/justfile` +- `$HOME/.justfile` + +You can put recipes that are used across many projects in a global justfile to +easily invoke them from any directory. + +#### User justfile tips + +You can also adopt some of the following workflows. These tips assume you've +created a `justfile` at `~/.user.justfile`, but you can put this `justfile` +at any convenient path on your system. + +##### Recipe Aliases If you want to call the recipes in `~/.user.justfile` by name, and don't mind creating an alias for every recipe, add the following to your shell's @@ -3293,7 +3310,7 @@ It took me way too long to realize that you could create recipe aliases like this. Notwithstanding my tardiness, I am very pleased to bring you this major advance in `justfile` technology. -#### Forwarding Alias +##### Forwarding Alias If you'd rather not create aliases for every recipe, you can create a single alias: @@ -3308,7 +3325,7 @@ I'm pretty sure that nobody actually uses this feature, but it's there. ¯\\\_(ツ)\_/¯ -#### Customization +##### Customization You can customize the above aliases with additional options. For example, if you'd prefer to have the recipes in your `justfile` run in your home directory, diff --git a/completions/just.bash b/completions/just.bash index 7006dba..abfa86d 100644 --- a/completions/just.bash +++ b/completions/just.bash @@ -30,7 +30,7 @@ _just() { case "${cmd}" in "$1") - opts="-n -f -q -u -v -d -c -e -l -s -E -h -V --check --chooser --color --command-color --yes --dry-run --dump-format --highlight --list-heading --list-prefix --no-aliases --no-deps --no-dotenv --no-highlight --justfile --quiet --set --shell --shell-arg --shell-command --clear-shell-args --unsorted --unstable --verbose --working-directory --changelog --choose --command --completions --dump --edit --evaluate --fmt --init --list --man --show --summary --variables --dotenv-filename --dotenv-path --help --version [ARGUMENTS]..." + opts="-n -f -q -u -v -d -c -e -l -s -E -g -h -V --check --chooser --color --command-color --yes --dry-run --dump-format --highlight --list-heading --list-prefix --no-aliases --no-deps --no-dotenv --no-highlight --justfile --quiet --set --shell --shell-arg --shell-command --clear-shell-args --unsorted --unstable --verbose --working-directory --changelog --choose --command --completions --dump --edit --evaluate --fmt --init --list --man --show --summary --variables --dotenv-filename --dotenv-path --global-justfile --help --version [ARGUMENTS]..." if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 diff --git a/completions/just.elvish b/completions/just.elvish index 461b75a..e1f29d9 100644 --- a/completions/just.elvish +++ b/completions/just.elvish @@ -69,6 +69,8 @@ set edit:completion:arg-completer[just] = {|@words| cand --man 'Print man page' cand --summary 'List names of available recipes' cand --variables 'List names of variables' + cand -g 'Use global justfile' + cand --global-justfile 'Use global justfile' cand -h 'Print help' cand --help 'Print help' cand -V 'Print version' diff --git a/completions/just.fish b/completions/just.fish index 073820a..8ff8f0c 100644 --- a/completions/just.fish +++ b/completions/just.fish @@ -76,5 +76,6 @@ complete -c just -s l -l list -d 'List available recipes and their arguments' complete -c just -l man -d 'Print man page' complete -c just -l summary -d 'List names of available recipes' complete -c just -l variables -d 'List names of variables' +complete -c just -s g -l global-justfile -d 'Use global justfile' complete -c just -s h -l help -d 'Print help' complete -c just -s V -l version -d 'Print version' diff --git a/completions/just.powershell b/completions/just.powershell index 2b02718..e19f9fb 100644 --- a/completions/just.powershell +++ b/completions/just.powershell @@ -72,6 +72,8 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page') [CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes') [CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables') + [CompletionResult]::new('-g', 'g', [CompletionResultType]::ParameterName, 'Use global justfile') + [CompletionResult]::new('--global-justfile', 'global-justfile', [CompletionResultType]::ParameterName, 'Use global justfile') [CompletionResult]::new('-h', 'h', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') diff --git a/completions/just.zsh b/completions/just.zsh index 0b79adb..715f5b5 100644 --- a/completions/just.zsh +++ b/completions/just.zsh @@ -67,6 +67,8 @@ _just() { '--man[Print man page]' \ '--summary[List names of available recipes]' \ '--variables[List names of variables]' \ +'(-f --justfile -d --working-directory)-g[Use global justfile]' \ +'(-f --justfile -d --working-directory)--global-justfile[Use global justfile]' \ '-h[Print help]' \ '--help[Print help]' \ '-V[Print version]' \ diff --git a/src/config.rs b/src/config.rs index 53b76aa..b657b18 100644 --- a/src/config.rs +++ b/src/config.rs @@ -94,6 +94,7 @@ mod arg { pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH"; pub(crate) const DRY_RUN: &str = "DRY-RUN"; pub(crate) const DUMP_FORMAT: &str = "DUMP-FORMAT"; + pub(crate) const GLOBAL_JUSTFILE: &str = "GLOBAL_JUSTFILE"; pub(crate) const HIGHLIGHT: &str = "HIGHLIGHT"; pub(crate) const JUSTFILE: &str = "JUSTFILE"; pub(crate) const LIST_HEADING: &str = "LIST-HEADING"; @@ -465,6 +466,15 @@ impl Config { .action(ArgAction::Append) .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"), ) + .arg( + Arg::new(arg::GLOBAL_JUSTFILE) + .action(ArgAction::SetTrue) + .long("global-justfile") + .short('g') + .conflicts_with(arg::JUSTFILE) + .conflicts_with(arg::WORKING_DIRECTORY) + .help("Use global justfile") + ) } fn color_from_matches(matches: &ArgMatches) -> ConfigResult { @@ -520,6 +530,39 @@ impl Config { } } + fn search_config(matches: &ArgMatches, positional: &Positional) -> ConfigResult { + if matches.get_flag(arg::GLOBAL_JUSTFILE) { + return Ok(SearchConfig::GlobalJustfile); + } + + let justfile = matches.get_one::(arg::JUSTFILE).map(Into::into); + + let working_directory = matches + .get_one::(arg::WORKING_DIRECTORY) + .map(Into::into); + + if let Some(search_directory) = positional.search_directory.as_ref().map(PathBuf::from) { + if justfile.is_some() || working_directory.is_some() { + return Err(ConfigError::SearchDirConflict); + } + Ok(SearchConfig::FromSearchDirectory { search_directory }) + } else { + match (justfile, working_directory) { + (None, None) => Ok(SearchConfig::FromInvocationDirectory), + (Some(justfile), None) => Ok(SearchConfig::WithJustfile { justfile }), + (Some(justfile), Some(working_directory)) => { + Ok(SearchConfig::WithJustfileAndWorkingDirectory { + justfile, + working_directory, + }) + } + (None, Some(_)) => Err(ConfigError::internal( + "--working-directory set without --justfile", + )), + } + } + } + pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult { let invocation_directory = env::current_dir().context(config_error::CurrentDirContext)?; @@ -545,39 +588,11 @@ impl Config { .map(|s| s.map(String::as_str)), ); - for (name, value) in positional.overrides { + for (name, value) in &positional.overrides { overrides.insert(name.clone(), value.clone()); } - let search_config = { - let justfile = matches.get_one::(arg::JUSTFILE).map(Into::into); - let working_directory = matches - .get_one::(arg::WORKING_DIRECTORY) - .map(Into::into); - - if let Some(search_directory) = positional.search_directory.map(PathBuf::from) { - if justfile.is_some() || working_directory.is_some() { - return Err(ConfigError::SearchDirConflict); - } - SearchConfig::FromSearchDirectory { search_directory } - } else { - match (justfile, working_directory) { - (None, None) => SearchConfig::FromInvocationDirectory, - (Some(justfile), None) => SearchConfig::WithJustfile { justfile }, - (Some(justfile), Some(working_directory)) => { - SearchConfig::WithJustfileAndWorkingDirectory { - justfile, - working_directory, - } - } - (None, Some(_)) => { - return Err(ConfigError::internal( - "--working-directory set without --justfile", - )) - } - } - } - }; + let search_config = Self::search_config(matches, &positional)?; for subcommand in cmd::ARGLESS { if matches.get_flag(subcommand) { diff --git a/src/search.rs b/src/search.rs index c14eb55..5085419 100644 --- a/src/search.rs +++ b/src/search.rs @@ -1,7 +1,7 @@ use {super::*, std::path::Component}; const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0]; -pub(crate) const JUSTFILE_NAMES: &[&str] = &["justfile", ".justfile"]; +pub(crate) const JUSTFILE_NAMES: [&str; 2] = ["justfile", ".justfile"]; const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"]; pub(crate) struct Search { @@ -10,6 +10,29 @@ pub(crate) struct Search { } impl Search { + fn global_justfile_paths() -> Vec { + let mut paths = Vec::new(); + + if let Some(config_dir) = dirs::config_dir() { + paths.push(config_dir.join("just").join(DEFAULT_JUSTFILE_NAME)); + } + + if let Some(home_dir) = dirs::home_dir() { + paths.push( + home_dir + .join(".config") + .join("just") + .join(DEFAULT_JUSTFILE_NAME), + ); + + for justfile_name in JUSTFILE_NAMES { + paths.push(home_dir.join(justfile_name)); + } + } + + paths + } + pub(crate) fn find( search_config: &SearchConfig, invocation_directory: &Path, @@ -18,21 +41,24 @@ impl Search { SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory), SearchConfig::FromSearchDirectory { search_directory } => { let search_directory = Self::clean(invocation_directory, search_directory); - let justfile = Self::justfile(&search_directory)?; - let working_directory = Self::working_directory_from_justfile(&justfile)?; - Ok(Self { justfile, working_directory, }) } + SearchConfig::GlobalJustfile => Ok(Self { + justfile: Self::global_justfile_paths() + .iter() + .find(|path| path.exists()) + .cloned() + .ok_or(SearchError::GlobalJustfileNotFound)?, + working_directory: Self::project_root(invocation_directory)?, + }), SearchConfig::WithJustfile { justfile } => { let justfile = Self::clean(invocation_directory, justfile); - let working_directory = Self::working_directory_from_justfile(&justfile)?; - Ok(Self { justfile, working_directory, @@ -50,9 +76,7 @@ impl Search { pub(crate) fn find_next(starting_dir: &Path) -> SearchResult { let justfile = Self::justfile(starting_dir)?; - let working_directory = Self::working_directory_from_justfile(&justfile)?; - Ok(Self { justfile, working_directory, @@ -66,39 +90,30 @@ impl Search { match search_config { SearchConfig::FromInvocationDirectory => { let working_directory = Self::project_root(invocation_directory)?; - let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); - Ok(Self { justfile, working_directory, }) } - SearchConfig::FromSearchDirectory { search_directory } => { let search_directory = Self::clean(invocation_directory, search_directory); - let working_directory = Self::project_root(&search_directory)?; - let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); - Ok(Self { justfile, working_directory, }) } - + SearchConfig::GlobalJustfile => Err(SearchError::GlobalJustfileInit), SearchConfig::WithJustfile { justfile } => { let justfile = Self::clean(invocation_directory, justfile); - let working_directory = Self::working_directory_from_justfile(&justfile)?; - Ok(Self { justfile, working_directory, }) } - SearchConfig::WithJustfileAndWorkingDirectory { justfile, working_directory, diff --git a/src/search_config.rs b/src/search_config.rs index 3af178a..fa447e4 100644 --- a/src/search_config.rs +++ b/src/search_config.rs @@ -9,6 +9,8 @@ pub(crate) enum SearchConfig { FromInvocationDirectory, /// As in `Invocation`, but start from `search_directory`. FromSearchDirectory { search_directory: PathBuf }, + /// Search for global justfile + GlobalJustfile, /// Use user-specified justfile, with the working directory set to the /// directory that contains it. WithJustfile { justfile: PathBuf }, diff --git a/src/search_error.rs b/src/search_error.rs index 3ee783e..988703d 100644 --- a/src/search_error.rs +++ b/src/search_error.rs @@ -3,6 +3,10 @@ use super::*; #[derive(Debug, Snafu)] #[snafu(visibility(pub(crate)))] pub(crate) enum SearchError { + #[snafu(display("Cannot initialize global justfile"))] + GlobalJustfileInit, + #[snafu(display("Global justfile not found"))] + GlobalJustfileNotFound, #[snafu(display( "I/O error reading directory `{}`: {}", directory.display(), diff --git a/src/source.rs b/src/source.rs index 8826b4d..1da7a5f 100644 --- a/src/source.rs +++ b/src/source.rs @@ -1,5 +1,6 @@ use super::*; +#[derive(Debug)] pub(crate) struct Source<'src> { pub(crate) file_depth: u32, pub(crate) namepath: Namepath<'src>, diff --git a/tests/global.rs b/tests/global.rs new file mode 100644 index 0000000..b6e7caf --- /dev/null +++ b/tests/global.rs @@ -0,0 +1,65 @@ +use super::*; + +#[test] +#[cfg(target_os = "macos")] +fn macos() { + let tempdir = tempdir(); + + let path = tempdir.path().to_owned(); + + Test::with_tempdir(tempdir) + .no_justfile() + .test_round_trip(false) + .write( + "Library/Application Support/just/justfile", + "@default:\n echo foo", + ) + .env("HOME", path.to_str().unwrap()) + .args(["--global-justfile"]) + .stdout("foo\n") + .run(); +} + +#[test] +#[cfg(all(unix, not(target_os = "macos")))] +fn not_macos() { + let tempdir = tempdir(); + + let path = tempdir.path().to_owned(); + + Test::with_tempdir(tempdir) + .no_justfile() + .test_round_trip(false) + .write("just/justfile", "@default:\n echo foo") + .env("XDG_CONFIG_HOME", path.to_str().unwrap()) + .args(["--global-justfile"]) + .stdout("foo\n") + .run(); +} + +#[test] +#[cfg(unix)] +fn unix() { + let tempdir = tempdir(); + + let path = tempdir.path().to_owned(); + + let tempdir = Test::with_tempdir(tempdir) + .no_justfile() + .test_round_trip(false) + .write("justfile", "@default:\n echo foo") + .env("HOME", path.to_str().unwrap()) + .args(["--global-justfile"]) + .stdout("foo\n") + .run() + .tempdir; + + Test::with_tempdir(tempdir) + .no_justfile() + .test_round_trip(false) + .write(".config/just/justfile", "@default:\n echo bar") + .env("HOME", path.to_str().unwrap()) + .args(["--global-justfile"]) + .stdout("bar\n") + .run(); +} diff --git a/tests/lib.rs b/tests/lib.rs index 1440117..806cdca 100644 --- a/tests/lib.rs +++ b/tests/lib.rs @@ -59,6 +59,8 @@ mod export; mod fallback; mod fmt; mod functions; +#[cfg(unix)] +mod global; mod ignore_comments; mod imports; mod init;