Add --global-justfile flag (#1846)

This commit is contained in:
Greg Shuflin 2024-05-19 02:29:13 -07:00 committed by GitHub
parent 104608d8cc
commit a343f5c80c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 182 additions and 54 deletions

View File

@ -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 `make`. You can find the old version in
[contrib/just.sh](https://github.com/casey/just/blob/master/contrib/just.sh). [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. 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 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 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 this. Notwithstanding my tardiness, I am very pleased to bring you this major
advance in `justfile` technology. advance in `justfile` technology.
#### Forwarding Alias ##### Forwarding Alias
If you'd rather not create aliases for every recipe, you can create a single 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 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, you'd prefer to have the recipes in your `justfile` run in your home directory,

View File

@ -30,7 +30,7 @@ _just() {
case "${cmd}" in case "${cmd}" in
"$1") "$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 if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0 return 0

View File

@ -69,6 +69,8 @@ set edit:completion:arg-completer[just] = {|@words|
cand --man 'Print man page' cand --man 'Print man page'
cand --summary 'List names of available recipes' cand --summary 'List names of available recipes'
cand --variables 'List names of variables' cand --variables 'List names of variables'
cand -g 'Use global justfile'
cand --global-justfile 'Use global justfile'
cand -h 'Print help' cand -h 'Print help'
cand --help 'Print help' cand --help 'Print help'
cand -V 'Print version' cand -V 'Print version'

View File

@ -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 man -d 'Print man page'
complete -c just -l summary -d 'List names of available recipes' complete -c just -l summary -d 'List names of available recipes'
complete -c just -l variables -d 'List names of variables' 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 h -l help -d 'Print help'
complete -c just -s V -l version -d 'Print version' complete -c just -s V -l version -d 'Print version'

View File

@ -72,6 +72,8 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
[CompletionResult]::new('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page') [CompletionResult]::new('--man', 'man', [CompletionResultType]::ParameterName, 'Print man page')
[CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes') [CompletionResult]::new('--summary', 'summary', [CompletionResultType]::ParameterName, 'List names of available recipes')
[CompletionResult]::new('--variables', 'variables', [CompletionResultType]::ParameterName, 'List names of variables') [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('-h', 'h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help') [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version') [CompletionResult]::new('-V', 'V ', [CompletionResultType]::ParameterName, 'Print version')

View File

@ -67,6 +67,8 @@ _just() {
'--man[Print man page]' \ '--man[Print man page]' \
'--summary[List names of available recipes]' \ '--summary[List names of available recipes]' \
'--variables[List names of variables]' \ '--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]' \ '-h[Print help]' \
'--help[Print help]' \ '--help[Print help]' \
'-V[Print version]' \ '-V[Print version]' \

View File

@ -94,6 +94,7 @@ mod arg {
pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH"; pub(crate) const DOTENV_PATH: &str = "DOTENV-PATH";
pub(crate) const DRY_RUN: &str = "DRY-RUN"; pub(crate) const DRY_RUN: &str = "DRY-RUN";
pub(crate) const DUMP_FORMAT: &str = "DUMP-FORMAT"; 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 HIGHLIGHT: &str = "HIGHLIGHT";
pub(crate) const JUSTFILE: &str = "JUSTFILE"; pub(crate) const JUSTFILE: &str = "JUSTFILE";
pub(crate) const LIST_HEADING: &str = "LIST-HEADING"; pub(crate) const LIST_HEADING: &str = "LIST-HEADING";
@ -465,6 +466,15 @@ impl Config {
.action(ArgAction::Append) .action(ArgAction::Append)
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"), .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<Color> { fn color_from_matches(matches: &ArgMatches) -> ConfigResult<Color> {
@ -520,6 +530,39 @@ impl Config {
} }
} }
fn search_config(matches: &ArgMatches, positional: &Positional) -> ConfigResult<SearchConfig> {
if matches.get_flag(arg::GLOBAL_JUSTFILE) {
return Ok(SearchConfig::GlobalJustfile);
}
let justfile = matches.get_one::<PathBuf>(arg::JUSTFILE).map(Into::into);
let working_directory = matches
.get_one::<PathBuf>(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<Self> { pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult<Self> {
let invocation_directory = env::current_dir().context(config_error::CurrentDirContext)?; let invocation_directory = env::current_dir().context(config_error::CurrentDirContext)?;
@ -545,39 +588,11 @@ impl Config {
.map(|s| s.map(String::as_str)), .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()); overrides.insert(name.clone(), value.clone());
} }
let search_config = { let search_config = Self::search_config(matches, &positional)?;
let justfile = matches.get_one::<PathBuf>(arg::JUSTFILE).map(Into::into);
let working_directory = matches
.get_one::<PathBuf>(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",
))
}
}
}
};
for subcommand in cmd::ARGLESS { for subcommand in cmd::ARGLESS {
if matches.get_flag(subcommand) { if matches.get_flag(subcommand) {

View File

@ -1,7 +1,7 @@
use {super::*, std::path::Component}; use {super::*, std::path::Component};
const DEFAULT_JUSTFILE_NAME: &str = JUSTFILE_NAMES[0]; 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"]; const PROJECT_ROOT_CHILDREN: &[&str] = &[".bzr", ".git", ".hg", ".svn", "_darcs"];
pub(crate) struct Search { pub(crate) struct Search {
@ -10,6 +10,29 @@ pub(crate) struct Search {
} }
impl Search { impl Search {
fn global_justfile_paths() -> Vec<PathBuf> {
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( pub(crate) fn find(
search_config: &SearchConfig, search_config: &SearchConfig,
invocation_directory: &Path, invocation_directory: &Path,
@ -18,21 +41,24 @@ impl Search {
SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory), SearchConfig::FromInvocationDirectory => Self::find_next(invocation_directory),
SearchConfig::FromSearchDirectory { search_directory } => { SearchConfig::FromSearchDirectory { search_directory } => {
let search_directory = Self::clean(invocation_directory, search_directory); let search_directory = Self::clean(invocation_directory, search_directory);
let justfile = Self::justfile(&search_directory)?; let justfile = Self::justfile(&search_directory)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?; let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self { Ok(Self {
justfile, justfile,
working_directory, 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 } => { SearchConfig::WithJustfile { justfile } => {
let justfile = Self::clean(invocation_directory, justfile); let justfile = Self::clean(invocation_directory, justfile);
let working_directory = Self::working_directory_from_justfile(&justfile)?; let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self { Ok(Self {
justfile, justfile,
working_directory, working_directory,
@ -50,9 +76,7 @@ impl Search {
pub(crate) fn find_next(starting_dir: &Path) -> SearchResult<Self> { pub(crate) fn find_next(starting_dir: &Path) -> SearchResult<Self> {
let justfile = Self::justfile(starting_dir)?; let justfile = Self::justfile(starting_dir)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?; let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self { Ok(Self {
justfile, justfile,
working_directory, working_directory,
@ -66,39 +90,30 @@ impl Search {
match search_config { match search_config {
SearchConfig::FromInvocationDirectory => { SearchConfig::FromInvocationDirectory => {
let working_directory = Self::project_root(invocation_directory)?; let working_directory = Self::project_root(invocation_directory)?;
let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
Ok(Self { Ok(Self {
justfile, justfile,
working_directory, working_directory,
}) })
} }
SearchConfig::FromSearchDirectory { search_directory } => { SearchConfig::FromSearchDirectory { search_directory } => {
let search_directory = Self::clean(invocation_directory, search_directory); let search_directory = Self::clean(invocation_directory, search_directory);
let working_directory = Self::project_root(&search_directory)?; let working_directory = Self::project_root(&search_directory)?;
let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME); let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
Ok(Self { Ok(Self {
justfile, justfile,
working_directory, working_directory,
}) })
} }
SearchConfig::GlobalJustfile => Err(SearchError::GlobalJustfileInit),
SearchConfig::WithJustfile { justfile } => { SearchConfig::WithJustfile { justfile } => {
let justfile = Self::clean(invocation_directory, justfile); let justfile = Self::clean(invocation_directory, justfile);
let working_directory = Self::working_directory_from_justfile(&justfile)?; let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self { Ok(Self {
justfile, justfile,
working_directory, working_directory,
}) })
} }
SearchConfig::WithJustfileAndWorkingDirectory { SearchConfig::WithJustfileAndWorkingDirectory {
justfile, justfile,
working_directory, working_directory,

View File

@ -9,6 +9,8 @@ pub(crate) enum SearchConfig {
FromInvocationDirectory, FromInvocationDirectory,
/// As in `Invocation`, but start from `search_directory`. /// As in `Invocation`, but start from `search_directory`.
FromSearchDirectory { search_directory: PathBuf }, FromSearchDirectory { search_directory: PathBuf },
/// Search for global justfile
GlobalJustfile,
/// Use user-specified justfile, with the working directory set to the /// Use user-specified justfile, with the working directory set to the
/// directory that contains it. /// directory that contains it.
WithJustfile { justfile: PathBuf }, WithJustfile { justfile: PathBuf },

View File

@ -3,6 +3,10 @@ use super::*;
#[derive(Debug, Snafu)] #[derive(Debug, Snafu)]
#[snafu(visibility(pub(crate)))] #[snafu(visibility(pub(crate)))]
pub(crate) enum SearchError { pub(crate) enum SearchError {
#[snafu(display("Cannot initialize global justfile"))]
GlobalJustfileInit,
#[snafu(display("Global justfile not found"))]
GlobalJustfileNotFound,
#[snafu(display( #[snafu(display(
"I/O error reading directory `{}`: {}", "I/O error reading directory `{}`: {}",
directory.display(), directory.display(),

View File

@ -1,5 +1,6 @@
use super::*; use super::*;
#[derive(Debug)]
pub(crate) struct Source<'src> { pub(crate) struct Source<'src> {
pub(crate) file_depth: u32, pub(crate) file_depth: u32,
pub(crate) namepath: Namepath<'src>, pub(crate) namepath: Namepath<'src>,

65
tests/global.rs Normal file
View File

@ -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();
}

View File

@ -59,6 +59,8 @@ mod export;
mod fallback; mod fallback;
mod fmt; mod fmt;
mod functions; mod functions;
#[cfg(unix)]
mod global;
mod ignore_comments; mod ignore_comments;
mod imports; mod imports;
mod init; mod init;