Merge branch 'master' into fish-shell-completion-fix

This commit is contained in:
Shaik Azhar Madar 2025-11-18 17:29:35 +05:30 committed by GitHub
commit ef98cedf4d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
38 changed files with 1548 additions and 538 deletions

View file

@ -20,7 +20,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
@ -45,11 +45,11 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: actions-rust-lang/setup-rust-toolchain@v1
with:
toolchain: 1.77
toolchain: 1.80.0
- uses: Swatinem/rust-cache@v2
@ -60,7 +60,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
@ -93,7 +93,7 @@ jobs:
runs-on: ${{matrix.os}}
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Remove Broken WSL bash executable
if: ${{ matrix.os == 'windows-latest' }}

View file

@ -34,17 +34,21 @@ jobs:
matrix:
target:
- aarch64-apple-darwin
- aarch64-pc-windows-msvc
- aarch64-unknown-linux-musl
- arm-unknown-linux-musleabihf
- armv7-unknown-linux-musleabihf
- loongarch64-unknown-linux-musl
- x86_64-apple-darwin
- x86_64-pc-windows-msvc
- aarch64-pc-windows-msvc
- x86_64-unknown-linux-musl
include:
- target: aarch64-apple-darwin
os: macos-latest
target_rustflags: ''
- target: aarch64-pc-windows-msvc
os: windows-latest
target_rustflags: ''
- target: aarch64-unknown-linux-musl
os: ubuntu-latest
target_rustflags: '--codegen linker=aarch64-linux-gnu-gcc'
@ -54,14 +58,14 @@ jobs:
- target: armv7-unknown-linux-musleabihf
os: ubuntu-latest
target_rustflags: '--codegen linker=arm-linux-gnueabihf-gcc'
- target: loongarch64-unknown-linux-musl
os: ubuntu-latest
target_rustflags: '--codegen linker=loongarch64-linux-gnu-gcc-14'
- target: x86_64-apple-darwin
os: macos-latest
target_rustflags: ''
- target: x86_64-pc-windows-msvc
os: windows-latest
- target: aarch64-pc-windows-msvc
os: windows-latest
target_rustflags: ''
- target: x86_64-unknown-linux-musl
os: ubuntu-latest
target_rustflags: ''
@ -72,33 +76,32 @@ jobs:
- prerelease
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- name: Install AArch64 Toolchain
if: ${{ matrix.target == 'aarch64-unknown-linux-musl' }}
- name: Install Target Dependencies
run: |
sudo apt-get update
sudo apt-get install gcc-aarch64-linux-gnu libc6-dev-i386
case ${{ matrix.target }} in
aarch64-pc-windows-msvc)
rustup target add aarch64-pc-windows-msvc
;;
aarch64-unknown-linux-musl)
sudo apt-get update
sudo apt-get install gcc-aarch64-linux-gnu libc6-dev-i386
;;
arm-unknown-linux-musleabihf|armv7-unknown-linux-musleabihf)
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf
;;
loongarch64-unknown-linux-musl)
sudo apt-get update
sudo apt-get install gcc-14-loongarch64-linux-gnu
;;
esac
- name: Install ARM Toolchain
if: ${{ matrix.target == 'arm-unknown-linux-musleabihf' || matrix.target == 'armv7-unknown-linux-musleabihf' }}
run: |
sudo apt-get update
sudo apt-get install gcc-arm-linux-gnueabihf
- name: Install AArch64 Toolchain (Windows)
if: ${{ matrix.target == 'aarch64-pc-windows-msvc' }}
run: |
rustup target add aarch64-pc-windows-msvc
- name: Generate Completion Scripts and Manpage
- name: Generate Manpage
run: |
set -euxo pipefail
cargo build
mkdir -p completions
for shell in bash elvish fish nu powershell zsh; do
./target/debug/just --completions $shell > completions/just.$shell
done
mkdir -p man
./target/debug/just --man > man/just.1
@ -113,7 +116,7 @@ jobs:
shell: bash
- name: Publish Archive
uses: softprops/action-gh-release@v2.3.2
uses: softprops/action-gh-release@v2.4.2
if: ${{ startsWith(github.ref, 'refs/tags/') }}
with:
draft: false
@ -123,7 +126,7 @@ jobs:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish Changelog
uses: softprops/action-gh-release@v2.3.2
uses: softprops/action-gh-release@v2.4.2
if: >-
${{
startsWith(github.ref, 'refs/tags/')
@ -160,7 +163,7 @@ jobs:
shasum -a 256 * > ../SHA256SUMS
- name: Publish Checksums
uses: softprops/action-gh-release@v2.3.2
uses: softprops/action-gh-release@v2.4.2
with:
draft: false
files: SHA256SUMS
@ -178,7 +181,7 @@ jobs:
contents: write
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v5
- uses: Swatinem/rust-cache@v2
@ -202,7 +205,7 @@ jobs:
- name: Deploy Pages
uses: peaceiris/actions-gh-pages@v4
if: ${{ needs.prerelease.outputs.value }}
if: ${{ !needs.prerelease.outputs.value }}
with:
github_token: ${{secrets.GITHUB_TOKEN}}
publish_branch: gh-pages

View file

@ -1,6 +1,42 @@
Changelog
=========
[1.43.1](https://github.com/casey/just/releases/tag/1.43.1) - 2025-11-12
------------------------------------------------------------------------
### Fixed
- Only initialize signal handler once ([#2953](https://github.com/casey/just/pull/2953) by [casey](https://github.com/casey))
- Preserve module docs when formatting ([#2931](https://github.com/casey/just/pull/2931) by [casey](https://github.com/casey))
- Preserve module groups when formatting ([#2930](https://github.com/casey/just/pull/2930) by [casey](https://github.com/casey))
- Don't suggest private recipes and aliases ([#2916](https://github.com/casey/just/pull/2916) by [casey](https://github.com/casey))
### Misc
- Update softprops/action-gh-release to 2.4.2 ([#2948](https://github.com/casey/just/pull/2948) by [app/dependabot](https://github.com/app/dependabot))
- Fix `env()` usage in readme ([#2936](https://github.com/casey/just/pull/2936) by [laniakea64](https://github.com/laniakea64))
- Use a case statement to install target dependencies ([#2929](https://github.com/casey/just/pull/2929) by [casey](https://github.com/casey))
- Build loongarch64 release binaries ([#2886](https://github.com/casey/just/pull/2886) by [SkyBird233](https://github.com/SkyBird233))
- Bump softprops/action-gh-release to 2.4.1 ([#2919](https://github.com/casey/just/pull/2919) by [app/dependabot](https://github.com/app/dependabot))
- Update softprops/action-gh-release to 2.3.4 ([#2910](https://github.com/casey/just/pull/2910) by [app/dependabot](https://github.com/app/dependabot))
[1.43.0](https://github.com/casey/just/releases/tag/1.43.0) - 2025-09-27
------------------------------------------------------------------------
### Added
- Add `[default]` attribute ([#2878](https://github.com/casey/just/pull/2878) by [casey](https://github.com/casey))
- Do not ascend above `--ceiling` when looking for justfile ([#2870](https://github.com/casey/just/pull/2870) by [casey](https://github.com/casey))
### Misc
- Don't generate completions at runtime ([#2896](https://github.com/casey/just/pull/2896) by [casey](https://github.com/casey))
- Update `softprops/action-gh-release` to 2.3.3 ([#2879](https://github.com/casey/just/pull/2879) by [app/dependabot](https://github.com/app/dependabot))
- Add submodule alias and dependency targets to grammar ([#2877](https://github.com/casey/just/pull/2877) by [casey](https://github.com/casey))
- Bump `actions/checkout` to v5 ([#2864](https://github.com/casey/just/pull/2864) by [app/dependabot](https://github.com/app/dependabot))
- Fix Windows `PATH_SEP` value in readme ([#2859](https://github.com/casey/just/pull/2859) by [casey](https://github.com/casey))
- Fix lints for Rust 1.89 ([#2860](https://github.com/casey/just/pull/2860) by [casey](https://github.com/casey))
- Note that Debian 13 has been released ([#2856](https://github.com/casey/just/pull/2856) by [sblondon](https://github.com/sblondon))
- Mention `just-mcp` in readme ([#2843](https://github.com/casey/just/pull/2843) by [casey](https://github.com/casey))
- Expand Windows instructions in readme ([#2842](https://github.com/casey/just/pull/2842) by [casey](https://github.com/casey))
- Note `[parallel]` attribute in parallelism section ([#2837](https://github.com/casey/just/pull/2837) by [casey](https://github.com/casey))
[1.42.4](https://github.com/casey/just/releases/tag/1.42.4) - 2025-07-24
------------------------------------------------------------------------

548
Cargo.lock generated

File diff suppressed because it is too large Load diff

View file

@ -1,6 +1,6 @@
[package]
name = "just"
version = "1.42.4"
version = "1.43.1"
authors = ["Casey Rodarmor <casey@rodarmor.com>"]
autotests = false
categories = ["command-line-utilities", "development-tools"]
@ -12,7 +12,7 @@ keywords = ["command-line", "task", "runner", "development", "utility"]
license = "CC0-1.0"
readme = "crates-io-readme.md"
repository = "https://github.com/casey/just"
rust-version = "1.77"
rust-version = "1.80.0"
[workspace]
members = [".", "crates/*"]
@ -23,9 +23,7 @@ blake3 = { version = "1.5.0", features = ["rayon", "mmap"] }
camino = "1.0.4"
chrono = "0.4.38"
clap = { version = "4.0.0", features = ["derive", "env", "wrap_help"] }
clap_complete = "=4.5.48"
clap_mangen = "0.2.20"
ctrlc = { version = "3.1.1", features = ["termination"] }
derive-where = "1.2.7"
dirs = "6.0.0"
dotenvy = "0.15"
@ -35,7 +33,6 @@ is_executable = "1.0.4"
lexiclean = "0.0.1"
libc = "0.2.0"
num_cpus = "1.15.0"
once_cell = "1.19.0"
percent-encoding = "2.3.1"
rand = "0.9.0"
regex = "1.10.4"
@ -55,12 +52,13 @@ unicode-width = "0.2.0"
uuid = { version = "1.0.0", features = ["v4"] }
[target.'cfg(unix)'.dependencies]
nix = { version = "0.30.1", features = ["user"] }
nix = { version = "0.30.1", features = ["signal", "user"] }
[target.'cfg(windows)'.dependencies]
ctrlc = { version = "3.1.1", features = ["termination"] }
[dev-dependencies]
clap_complete = "=4.5.48"
executable-path = "1.0.0"
pretty_assertions = "1.0.0"
temptree = "0.2.0"

View file

@ -55,7 +55,9 @@ item : alias
eol : NEWLINE
| COMMENT NEWLINE
alias : 'alias' NAME ':=' NAME eol
alias : 'alias' NAME ':=' target eol
target : NAME ('::' NAME)*
assignment : NAME ':=' expression eol
@ -138,8 +140,8 @@ variadic : '*' parameter
dependencies : dependency* ('&&' dependency+)?
dependency : NAME
| '(' NAME expression* ')'
dependency : target
| '(' target expression* ')'
body : INDENT line+ DEDENT

View file

@ -723,9 +723,12 @@ Features
### The Default Recipe
When `just` is invoked without a recipe, it runs the first recipe in the
`justfile`. This recipe might be the most frequently run command in the
project, like running the tests:
When `just` is invoked without a recipe, it runs the recipe with the
`[default]` attribute, or the first recipe in the `justfile` if no recipe has
the `[default]` attribute.
This recipe might be the most frequently run command in the project, like
running the tests:
```just
test:
@ -1718,7 +1721,7 @@ A default can be substituted for an empty environment variable value with the
```just
set unstable
foo := env('FOO') || 'DEFAULT_VALUE'
foo := env('FOO', '') || 'DEFAULT_VALUE'
```
#### Executables
@ -2107,6 +2110,7 @@ change their behavior.
|------|------|-------------|
| `[confirm]`<sup>1.17.0</sup> | recipe | Require confirmation prior to executing recipe. |
| `[confirm(PROMPT)]`<sup>1.23.0</sup> | recipe | Require confirmation prior to executing recipe with a custom prompt. |
| `[default]`<sup>1.43.0</sup> | recipe | Use recipe as module's default recipe. |
| `[doc(DOC)]`<sup>1.27.0</sup> | module, recipe | Set recipe or module's [documentation comment](#documentation-comments) to `DOC`. |
| `[extension(EXT)]`<sup>1.32.0</sup> | recipe | Set shebang recipe script's file extension to `EXT`. `EXT` should include a period if one is desired. |
| `[group(NAME)]`<sup>1.27.0</sup> | module, recipe | Put recipe or module in in [group](#groups) `NAME`. |

185
completions/just.bash Normal file
View file

@ -0,0 +1,185 @@
_just() {
local i cur prev words cword opts cmd
COMPREPLY=()
# Modules use "::" as the separator, which is considered a wordbreak character in bash.
# The _get_comp_words_by_ref function is a hack to allow for exceptions to this rule without
# modifying the global COMP_WORDBREAKS environment variable.
if type _get_comp_words_by_ref &>/dev/null; then
_get_comp_words_by_ref -n : cur prev words cword
else
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
words=$COMP_WORDS
cword=$COMP_CWORD
fi
cmd=""
opts=""
for i in ${words[@]}
do
case "${cmd},${i}" in
",$1")
cmd="just"
;;
*)
;;
esac
done
case "${cmd}" in
just)
opts="-E -n -g -f -q -u -v -d -c -e -l -s -h -V --alias-style --ceiling --check --chooser --clear-shell-args --color --command-color --cygpath --dotenv-filename --dotenv-path --dry-run --dump-format --explain --global-justfile --highlight --justfile --list-heading --list-prefix --list-submodules --no-aliases --no-deps --no-dotenv --no-highlight --one --quiet --allow-missing --set --shell --shell-arg --shell-command --tempdir --timestamp --timestamp-format --unsorted --unstable --verbose --working-directory --yes --changelog --choose --command --completions --dump --edit --evaluate --fmt --groups --init --list --man --request --show --summary --variables --help --version [ARGUMENTS]..."
if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
else
local recipes=$(just --summary 2> /dev/null)
if echo "${cur}" | \grep -qF '/'; then
local path_prefix=$(echo "${cur}" | sed 's/[/][^/]*$/\//')
local recipes=$(just --summary 2> /dev/null -- "${path_prefix}")
local recipes=$(printf "${path_prefix}%s\t" $recipes)
fi
if [[ $? -eq 0 ]]; then
COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
if type __ltrim_colon_completions &>/dev/null; then
__ltrim_colon_completions "$cur"
fi
return 0
fi
fi
case "${prev}" in
--alias-style)
COMPREPLY=($(compgen -W "left right separate" -- "${cur}"))
return 0
;;
--ceiling)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--chooser)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--color)
COMPREPLY=($(compgen -W "always auto never" -- "${cur}"))
return 0
;;
--command-color)
COMPREPLY=($(compgen -W "black blue cyan green purple red yellow" -- "${cur}"))
return 0
;;
--cygpath)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dotenv-filename)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dotenv-path)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-E)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--dump-format)
COMPREPLY=($(compgen -W "json just" -- "${cur}"))
return 0
;;
--justfile)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--list-heading)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--list-prefix)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--set)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell-arg)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--tempdir)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--timestamp-format)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--working-directory)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--command)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-c)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--completions)
COMPREPLY=($(compgen -W "bash elvish fish nushell powershell zsh" -- "${cur}"))
return 0
;;
--list)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-l)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--request)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--show)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-s)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _just -o nosort -o bashdefault -o default just
else
complete -F _just -o bashdefault -o default just
fi

93
completions/just.elvish Normal file
View file

@ -0,0 +1,93 @@
use builtin;
use str;
set edit:completion:arg-completer[just] = {|@words|
fn spaces {|n|
builtin:repeat $n ' ' | str:join ''
}
fn cand {|text desc|
edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc
}
var command = 'just'
for word $words[1..-1] {
if (str:has-prefix $word '-') {
break
}
set command = $command';'$word
}
var completions = [
&'just'= {
cand --alias-style 'Set list command alias display style'
cand --ceiling 'Do not ascend above <CEILING> directory when searching for a justfile.'
cand --chooser 'Override binary invoked by `--choose`'
cand --color 'Print colorful output'
cand --command-color 'Echo recipe lines in <COMMAND-COLOR>'
cand --cygpath 'Use binary at <CYGPATH> to convert between unix and Windows paths.'
cand --dotenv-filename 'Search for environment file named <DOTENV-FILENAME> instead of `.env`'
cand -E 'Load <DOTENV-PATH> as environment file instead of searching for one'
cand --dotenv-path 'Load <DOTENV-PATH> as environment file instead of searching for one'
cand --dump-format 'Dump justfile as <FORMAT>'
cand -f 'Use <JUSTFILE> as justfile'
cand --justfile 'Use <JUSTFILE> as justfile'
cand --list-heading 'Print <TEXT> before list'
cand --list-prefix 'Print <TEXT> before each list item'
cand --set 'Override <VARIABLE> with <VALUE>'
cand --shell 'Invoke <SHELL> to run recipes'
cand --shell-arg 'Invoke shell with <SHELL-ARG> as an argument'
cand --tempdir 'Save temporary files to <TEMPDIR>.'
cand --timestamp-format 'Timestamp format string'
cand -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
cand --working-directory 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
cand -c 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
cand --command 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set'
cand --completions 'Print shell completion script for <SHELL>'
cand -l 'List available recipes in <MODULE> or root if omitted'
cand --list 'List available recipes in <MODULE> or root if omitted'
cand --request 'Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.'
cand -s 'Show recipe at <PATH>'
cand --show 'Show recipe at <PATH>'
cand --check 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'
cand --clear-shell-args 'Clear shell arguments'
cand -n 'Print what just would do without doing it'
cand --dry-run 'Print what just would do without doing it'
cand --explain 'Print recipe doc comment before running it'
cand -g 'Use global justfile'
cand --global-justfile 'Use global justfile'
cand --highlight 'Highlight echoed recipe lines in bold'
cand --list-submodules 'List recipes in submodules'
cand --no-aliases 'Don''t show aliases in list'
cand --no-deps 'Don''t run recipe dependencies'
cand --no-dotenv 'Don''t load `.env` file'
cand --no-highlight 'Don''t highlight echoed recipe lines in bold'
cand --one 'Forbid multiple recipes from being invoked on the command line'
cand -q 'Suppress all output'
cand --quiet 'Suppress all output'
cand --allow-missing 'Ignore missing recipe and module errors'
cand --shell-command 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'
cand --timestamp 'Print recipe command timestamps'
cand -u 'Return list and summary entries in source order'
cand --unsorted 'Return list and summary entries in source order'
cand --unstable 'Enable unstable features'
cand -v 'Use verbose output'
cand --verbose 'Use verbose output'
cand --yes 'Automatically confirm all recipes.'
cand --changelog 'Print changelog'
cand --choose 'Select one or more recipes to run using a binary chooser. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`'
cand --dump 'Print justfile'
cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --evaluate 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.'
cand --fmt 'Format and overwrite justfile'
cand --groups 'List recipe groups'
cand --init 'Initialize new justfile in project root'
cand --man 'Print man page'
cand --summary 'List names of available recipes'
cand --variables 'List names of variables'
cand -h 'Print help'
cand --help 'Print help'
cand -V 'Print version'
cand --version 'Print version'
}
]
$completions[$command]
}

86
completions/just.fish Normal file
View file

@ -0,0 +1,86 @@
function __fish_just_complete_recipes
if string match -rq '(-f|--justfile)\s*=?(?<justfile>[^\s]+)' -- (string split -- ' -- ' (commandline -pc))[1]
set -fx JUST_JUSTFILE "$justfile"
end
printf "%s\n" (string split " " (just --summary))
end
# don't suggest files right off
complete -c just -n "__fish_is_first_arg" --no-files
# complete recipes
complete -c just -a '(__fish_just_complete_recipes)'
# autogenerated completions
complete -c just -l alias-style -d 'Set list command alias display style' -r -f -a "left\t''
right\t''
separate\t''"
complete -c just -l ceiling -d 'Do not ascend above <CEILING> directory when searching for a justfile.' -r -F
complete -c just -l chooser -d 'Override binary invoked by `--choose`' -r
complete -c just -l color -d 'Print colorful output' -r -f -a "always\t''
auto\t''
never\t''"
complete -c just -l command-color -d 'Echo recipe lines in <COMMAND-COLOR>' -r -f -a "black\t''
blue\t''
cyan\t''
green\t''
purple\t''
red\t''
yellow\t''"
complete -c just -l cygpath -d 'Use binary at <CYGPATH> to convert between unix and Windows paths.' -r -F
complete -c just -l dotenv-filename -d 'Search for environment file named <DOTENV-FILENAME> instead of `.env`' -r
complete -c just -s E -l dotenv-path -d 'Load <DOTENV-PATH> as environment file instead of searching for one' -r -F
complete -c just -l dump-format -d 'Dump justfile as <FORMAT>' -r -f -a "json\t''
just\t''"
complete -c just -s f -l justfile -d 'Use <JUSTFILE> as justfile' -r -F
complete -c just -l list-heading -d 'Print <TEXT> before list' -r
complete -c just -l list-prefix -d 'Print <TEXT> before each list item' -r
complete -c just -l set -d 'Override <VARIABLE> with <VALUE>' -r
complete -c just -l shell -d 'Invoke <SHELL> to run recipes' -r
complete -c just -l shell-arg -d 'Invoke shell with <SHELL-ARG> as an argument' -r
complete -c just -l tempdir -d 'Save temporary files to <TEMPDIR>.' -r -F
complete -c just -l timestamp-format -d 'Timestamp format string' -r
complete -c just -s d -l working-directory -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set' -r -F
complete -c just -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' -r
complete -c just -l completions -d 'Print shell completion script for <SHELL>' -r -f -a "bash\t''
elvish\t''
fish\t''
nushell\t''
powershell\t''
zsh\t''"
complete -c just -s l -l list -d 'List available recipes in <MODULE> or root if omitted' -r
complete -c just -l request -d 'Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.' -r
complete -c just -s s -l show -d 'Show recipe at <PATH>' -r
complete -c just -l check -d 'Run `--fmt` in \'check\' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.'
complete -c just -l clear-shell-args -d 'Clear shell arguments'
complete -c just -s n -l dry-run -d 'Print what just would do without doing it'
complete -c just -l explain -d 'Print recipe doc comment before running it'
complete -c just -s g -l global-justfile -d 'Use global justfile'
complete -c just -l highlight -d 'Highlight echoed recipe lines in bold'
complete -c just -l list-submodules -d 'List recipes in submodules'
complete -c just -l no-aliases -d 'Don\'t show aliases in list'
complete -c just -l no-deps -d 'Don\'t run recipe dependencies'
complete -c just -l no-dotenv -d 'Don\'t load `.env` file'
complete -c just -l no-highlight -d 'Don\'t highlight echoed recipe lines in bold'
complete -c just -l one -d 'Forbid multiple recipes from being invoked on the command line'
complete -c just -s q -l quiet -d 'Suppress all output'
complete -c just -l allow-missing -d 'Ignore missing recipe and module errors'
complete -c just -l shell-command -d 'Invoke <COMMAND> with the shell used to run recipe lines and backticks'
complete -c just -l timestamp -d 'Print recipe command timestamps'
complete -c just -s u -l unsorted -d 'Return list and summary entries in source order'
complete -c just -l unstable -d 'Enable unstable features'
complete -c just -s v -l verbose -d 'Use verbose output'
complete -c just -l yes -d 'Automatically confirm all recipes.'
complete -c just -l changelog -d 'Print changelog'
complete -c just -l choose -d 'Select one or more recipes to run using a binary chooser. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`'
complete -c just -l dump -d 'Print justfile'
complete -c just -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
complete -c just -l evaluate -d 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable\'s value.'
complete -c just -l fmt -d 'Format and overwrite justfile'
complete -c just -l groups -d 'List recipe groups'
complete -c just -l init -d 'Initialize new justfile in project root'
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 h -l help -d 'Print help'
complete -c just -s V -l version -d 'Print version'

8
completions/just.nu Normal file
View file

@ -0,0 +1,8 @@
def "nu-complete just" [] {
(^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description
}
# Just: A Command Runner
export extern "just" [
...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s)
]

119
completions/just.powershell Normal file
View file

@ -0,0 +1,119 @@
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'just'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-') -or
$element.Value -eq $wordToComplete) {
break
}
$element.Value
}) -join ';'
$completions = @(switch ($command) {
'just' {
[CompletionResult]::new('--alias-style', '--alias-style', [CompletionResultType]::ParameterName, 'Set list command alias display style')
[CompletionResult]::new('--ceiling', '--ceiling', [CompletionResultType]::ParameterName, 'Do not ascend above <CEILING> directory when searching for a justfile.')
[CompletionResult]::new('--chooser', '--chooser', [CompletionResultType]::ParameterName, 'Override binary invoked by `--choose`')
[CompletionResult]::new('--color', '--color', [CompletionResultType]::ParameterName, 'Print colorful output')
[CompletionResult]::new('--command-color', '--command-color', [CompletionResultType]::ParameterName, 'Echo recipe lines in <COMMAND-COLOR>')
[CompletionResult]::new('--cygpath', '--cygpath', [CompletionResultType]::ParameterName, 'Use binary at <CYGPATH> to convert between unix and Windows paths.')
[CompletionResult]::new('--dotenv-filename', '--dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named <DOTENV-FILENAME> instead of `.env`')
[CompletionResult]::new('-E', '-E ', [CompletionResultType]::ParameterName, 'Load <DOTENV-PATH> as environment file instead of searching for one')
[CompletionResult]::new('--dotenv-path', '--dotenv-path', [CompletionResultType]::ParameterName, 'Load <DOTENV-PATH> as environment file instead of searching for one')
[CompletionResult]::new('--dump-format', '--dump-format', [CompletionResultType]::ParameterName, 'Dump justfile as <FORMAT>')
[CompletionResult]::new('-f', '-f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile')
[CompletionResult]::new('--justfile', '--justfile', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile')
[CompletionResult]::new('--list-heading', '--list-heading', [CompletionResultType]::ParameterName, 'Print <TEXT> before list')
[CompletionResult]::new('--list-prefix', '--list-prefix', [CompletionResultType]::ParameterName, 'Print <TEXT> before each list item')
[CompletionResult]::new('--set', '--set', [CompletionResultType]::ParameterName, 'Override <VARIABLE> with <VALUE>')
[CompletionResult]::new('--shell', '--shell', [CompletionResultType]::ParameterName, 'Invoke <SHELL> to run recipes')
[CompletionResult]::new('--shell-arg', '--shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with <SHELL-ARG> as an argument')
[CompletionResult]::new('--tempdir', '--tempdir', [CompletionResultType]::ParameterName, 'Save temporary files to <TEMPDIR>.')
[CompletionResult]::new('--timestamp-format', '--timestamp-format', [CompletionResultType]::ParameterName, 'Timestamp format string')
[CompletionResult]::new('-d', '-d', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
[CompletionResult]::new('--working-directory', '--working-directory', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
[CompletionResult]::new('-c', '-c', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
[CompletionResult]::new('--command', '--command', [CompletionResultType]::ParameterName, 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set')
[CompletionResult]::new('--completions', '--completions', [CompletionResultType]::ParameterName, 'Print shell completion script for <SHELL>')
[CompletionResult]::new('-l', '-l', [CompletionResultType]::ParameterName, 'List available recipes in <MODULE> or root if omitted')
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'List available recipes in <MODULE> or root if omitted')
[CompletionResult]::new('--request', '--request', [CompletionResultType]::ParameterName, 'Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.')
[CompletionResult]::new('-s', '-s', [CompletionResultType]::ParameterName, 'Show recipe at <PATH>')
[CompletionResult]::new('--show', '--show', [CompletionResultType]::ParameterName, 'Show recipe at <PATH>')
[CompletionResult]::new('--check', '--check', [CompletionResultType]::ParameterName, 'Run `--fmt` in ''check'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.')
[CompletionResult]::new('--clear-shell-args', '--clear-shell-args', [CompletionResultType]::ParameterName, 'Clear shell arguments')
[CompletionResult]::new('-n', '-n', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--dry-run', '--dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--explain', '--explain', [CompletionResultType]::ParameterName, 'Print recipe doc comment before running it')
[CompletionResult]::new('-g', '-g', [CompletionResultType]::ParameterName, 'Use global justfile')
[CompletionResult]::new('--global-justfile', '--global-justfile', [CompletionResultType]::ParameterName, 'Use global justfile')
[CompletionResult]::new('--highlight', '--highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold')
[CompletionResult]::new('--list-submodules', '--list-submodules', [CompletionResultType]::ParameterName, 'List recipes in submodules')
[CompletionResult]::new('--no-aliases', '--no-aliases', [CompletionResultType]::ParameterName, 'Don''t show aliases in list')
[CompletionResult]::new('--no-deps', '--no-deps', [CompletionResultType]::ParameterName, 'Don''t run recipe dependencies')
[CompletionResult]::new('--no-dotenv', '--no-dotenv', [CompletionResultType]::ParameterName, 'Don''t load `.env` file')
[CompletionResult]::new('--no-highlight', '--no-highlight', [CompletionResultType]::ParameterName, 'Don''t highlight echoed recipe lines in bold')
[CompletionResult]::new('--one', '--one', [CompletionResultType]::ParameterName, 'Forbid multiple recipes from being invoked on the command line')
[CompletionResult]::new('-q', '-q', [CompletionResultType]::ParameterName, 'Suppress all output')
[CompletionResult]::new('--quiet', '--quiet', [CompletionResultType]::ParameterName, 'Suppress all output')
[CompletionResult]::new('--allow-missing', '--allow-missing', [CompletionResultType]::ParameterName, 'Ignore missing recipe and module errors')
[CompletionResult]::new('--shell-command', '--shell-command', [CompletionResultType]::ParameterName, 'Invoke <COMMAND> with the shell used to run recipe lines and backticks')
[CompletionResult]::new('--timestamp', '--timestamp', [CompletionResultType]::ParameterName, 'Print recipe command timestamps')
[CompletionResult]::new('-u', '-u', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
[CompletionResult]::new('--unsorted', '--unsorted', [CompletionResultType]::ParameterName, 'Return list and summary entries in source order')
[CompletionResult]::new('--unstable', '--unstable', [CompletionResultType]::ParameterName, 'Enable unstable features')
[CompletionResult]::new('-v', '-v', [CompletionResultType]::ParameterName, 'Use verbose output')
[CompletionResult]::new('--verbose', '--verbose', [CompletionResultType]::ParameterName, 'Use verbose output')
[CompletionResult]::new('--yes', '--yes', [CompletionResultType]::ParameterName, 'Automatically confirm all recipes.')
[CompletionResult]::new('--changelog', '--changelog', [CompletionResultType]::ParameterName, 'Print changelog')
[CompletionResult]::new('--choose', '--choose', [CompletionResultType]::ParameterName, 'Select one or more recipes to run using a binary chooser. If `--chooser` is not passed the chooser defaults to the value of $JUST_CHOOSER, falling back to `fzf`')
[CompletionResult]::new('--dump', '--dump', [CompletionResultType]::ParameterName, 'Print justfile')
[CompletionResult]::new('-e', '-e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--edit', '--edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--evaluate', '--evaluate', [CompletionResultType]::ParameterName, 'Evaluate and print all variables. If a variable name is given as an argument, only print that variable''s value.')
[CompletionResult]::new('--fmt', '--fmt', [CompletionResultType]::ParameterName, 'Format and overwrite justfile')
[CompletionResult]::new('--groups', '--groups', [CompletionResultType]::ParameterName, 'List recipe groups')
[CompletionResult]::new('--init', '--init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root')
[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('-h', '-h', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('--help', '--help', [CompletionResultType]::ParameterName, 'Print help')
[CompletionResult]::new('-V', '-V ', [CompletionResultType]::ParameterName, 'Print version')
[CompletionResult]::new('--version', '--version', [CompletionResultType]::ParameterName, 'Print version')
break
}
})
function Get-JustFileRecipes([string[]]$CommandElements) {
$justFileIndex = $commandElements.IndexOf("--justfile");
if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {
$justFileLocation = $commandElements[$justFileIndex + 1]
}
$justArgs = @("--summary")
if (Test-Path $justFileLocation) {
$justArgs += @("--justfile", $justFileLocation)
}
$recipes = $(just @justArgs) -split ' '
return $recipes | ForEach-Object { [CompletionResult]::new($_) }
}
$elementValues = $commandElements | Select-Object -ExpandProperty Value
$recipes = Get-JustFileRecipes -CommandElements $elementValues
$completions += $recipes
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}

180
completions/just.zsh Normal file
View file

@ -0,0 +1,180 @@
#compdef just
autoload -U is-at-least
_just() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
local common=(
'(--no-aliases)--alias-style=[Set list command alias display style]: :(left right separate)' \
'--ceiling=[Do not ascend above <CEILING> directory when searching for a justfile.]: :_files' \
'--chooser=[Override binary invoked by \`--choose\`]: :_default' \
'--color=[Print colorful output]: :(always auto never)' \
'--command-color=[Echo recipe lines in <COMMAND-COLOR>]: :(black blue cyan green purple red yellow)' \
'--cygpath=[Use binary at <CYGPATH> to convert between unix and Windows paths.]: :_files' \
'(-E --dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of \`.env\`]: :_default' \
'-E+[Load <DOTENV-PATH> as environment file instead of searching for one]: :_files' \
'--dotenv-path=[Load <DOTENV-PATH> as environment file instead of searching for one]: :_files' \
'--dump-format=[Dump justfile as <FORMAT>]:FORMAT:(json just)' \
'-f+[Use <JUSTFILE> as justfile]: :_files' \
'--justfile=[Use <JUSTFILE> as justfile]: :_files' \
'--list-heading=[Print <TEXT> before list]:TEXT:_default' \
'--list-prefix=[Print <TEXT> before each list item]:TEXT:_default' \
'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \
'--shell=[Invoke <SHELL> to run recipes]: :_default' \
'*--shell-arg=[Invoke shell with <SHELL-ARG> as an argument]: :_default' \
'--tempdir=[Save temporary files to <TEMPDIR>.]: :_files' \
'--timestamp-format=[Timestamp format string]: :_default' \
'-d+[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]: :_files' \
'--working-directory=[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]: :_files' \
'*-c+[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: :_default' \
'*--command=[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: :_default' \
'--completions=[Print shell completion script for <SHELL>]:SHELL:(bash elvish fish nushell powershell zsh)' \
'()-l+[List available recipes in <MODULE> or root if omitted]' \
'()--list=[List available recipes in <MODULE> or root if omitted]' \
'--request=[Execute <REQUEST>. For internal testing purposes only. May be changed or removed at any time.]: :_default' \
'-s+[Show recipe at <PATH>]: :(_just_commands)' \
'--show=[Show recipe at <PATH>]: :(_just_commands)' \
'--check[Run \`--fmt\` in '\''check'\'' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required.]' \
'--clear-shell-args[Clear shell arguments]' \
'(-q --quiet)-n[Print what just would do without doing it]' \
'(-q --quiet)--dry-run[Print what just would do without doing it]' \
'--explain[Print recipe doc comment before running it]' \
'(-f --justfile -d --working-directory)-g[Use global justfile]' \
'(-f --justfile -d --working-directory)--global-justfile[Use global justfile]' \
'--highlight[Highlight echoed recipe lines in bold]' \
'--list-submodules[List recipes in submodules]' \
'--no-aliases[Don'\''t show aliases in list]' \
'--no-deps[Don'\''t run recipe dependencies]' \
'--no-dotenv[Don'\''t load \`.env\` file]' \
'--no-highlight[Don'\''t highlight echoed recipe lines in bold]' \
'--one[Forbid multiple recipes from being invoked on the command line]' \
'(-n --dry-run)-q[Suppress all output]' \
'(-n --dry-run)--quiet[Suppress all output]' \
'--allow-missing[Ignore missing recipe and module errors]' \
'--shell-command[Invoke <COMMAND> with the shell used to run recipe lines and backticks]' \
'--timestamp[Print recipe command timestamps]' \
'-u[Return list and summary entries in source order]' \
'--unsorted[Return list and summary entries in source order]' \
'--unstable[Enable unstable features]' \
'*-v[Use verbose output]' \
'*--verbose[Use verbose output]' \
'--yes[Automatically confirm all recipes.]' \
'--changelog[Print changelog]' \
'--choose[Select one or more recipes to run using a binary chooser. If \`--chooser\` is not passed the chooser defaults to the value of \$JUST_CHOOSER, falling back to \`fzf\`]' \
'--dump[Print justfile]' \
'-e[Edit justfile with editor given by \$VISUAL or \$EDITOR, falling back to \`vim\`]' \
'--edit[Edit justfile with editor given by \$VISUAL or \$EDITOR, falling back to \`vim\`]' \
'--evaluate[Evaluate and print all variables. If a variable name is given as an argument, only print that variable'\''s value.]' \
'--fmt[Format and overwrite justfile]' \
'--groups[List recipe groups]' \
'--init[Initialize new justfile in project root]' \
'--man[Print man page]' \
'--summary[List names of available recipes]' \
'--variables[List names of variables]' \
'-h[Print help]' \
'--help[Print help]' \
'-V[Print version]' \
'--version[Print version]' \
)
_arguments "${_arguments_options[@]}" $common \
'1: :_just_commands' \
'*: :->args' \
&& ret=0
case $state in
args)
curcontext="${curcontext%:*}-${words[2]}:"
local lastarg=${words[${#words}]}
local recipe
local cmds; cmds=(
${(s: :)$(_call_program commands just --summary)}
)
# Find first recipe name
for ((i = 2; i < $#words; i++ )) do
if [[ ${cmds[(I)${words[i]}]} -gt 0 ]]; then
recipe=${words[i]}
break
fi
done
if [[ $lastarg = */* ]]; then
# Arguments contain slash would be recognised as a file
_arguments -s -S $common '*:: :_files'
elif [[ $lastarg = *=* ]]; then
# Arguments contain equal would be recognised as a variable
_message "value"
elif [[ $recipe ]]; then
# Show usage message
_message "`just --show $recipe`"
# Or complete with other commands
#_arguments -s -S $common '*:: :_just_commands'
else
_arguments -s -S $common '*:: :_just_commands'
fi
;;
esac
return ret
}
(( $+functions[_just_commands] )) ||
_just_commands() {
[[ $PREFIX = -* ]] && return 1
integer ret=1
local variables; variables=(
${(s: :)$(_call_program commands just --variables)}
)
local commands; commands=(
${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: }
)
if compset -P '*='; then
case "${${words[-1]%=*}#*=}" in
*) _message 'value' && ret=0 ;;
esac
else
_describe -t variables 'variables' variables -qS "=" && ret=0
_describe -t commands 'just commands' commands "$@"
fi
}
if [ "$funcstack[1]" = "_just" ]; then
(( $+functions[_just_variables] )) ||
_just_variables() {
[[ $PREFIX = -* ]] && return 1
integer ret=1
local variables; variables=(
${(s: :)$(_call_program commands just --variables)}
)
if compset -P '*='; then
case "${${words[-1]%=*}#*=}" in
*) _message 'value' && ret=0 ;;
esac
else
_describe -t variables 'variables' variables && ret=0
fi
return ret
}
_just "$@"
else
compdef _just just
fi

View file

@ -25,8 +25,9 @@ impl<'src> Alias<'src, Namepath<'src>> {
}
impl Alias<'_> {
pub(crate) fn is_private(&self) -> bool {
self.name.lexeme().starts_with('_') || self.attributes.contains(AttributeDiscriminant::Private)
pub(crate) fn is_public(&self) -> bool {
!self.name.lexeme().starts_with('_')
&& !self.attributes.contains(AttributeDiscriminant::Private)
}
}

View file

@ -15,7 +15,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
pub(crate) fn analyze(
asts: &'run HashMap<PathBuf, Ast<'src>>,
doc: Option<String>,
groups: &[String],
groups: &[StringLiteral<'src>],
loaded: &[PathBuf],
name: Option<Name<'src>>,
paths: &HashMap<PathBuf, PathBuf>,
@ -28,7 +28,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
mut self,
asts: &'run HashMap<PathBuf, Ast<'src>>,
doc: Option<String>,
groups: &[String],
groups: &[StringLiteral<'src>],
loaded: &[PathBuf],
name: Option<Name<'src>>,
paths: &HashMap<PathBuf, PathBuf>,
@ -94,7 +94,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}
Item::Unexport { name } => {
if !self.unexports.insert(name.lexeme().to_string()) {
return Err(name.token.error(DuplicateUnexport {
return Err(name.error(DuplicateUnexport {
variable: name.lexeme(),
}));
}
@ -112,7 +112,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
let variable = assignment.name.lexeme();
if !settings.allow_duplicate_variables && assignments.contains_key(variable) {
return Err(assignment.name.token.error(DuplicateVariable { variable }));
return Err(assignment.name.error(DuplicateVariable { variable }));
}
if assignments.get(variable).map_or(true, |original| {
@ -122,7 +122,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
}
if self.unexports.contains(variable) {
return Err(assignment.name.token.error(ExportUnexported { variable }));
return Err(assignment.name.error(ExportUnexported { variable }));
}
}
@ -172,10 +172,21 @@ impl<'run, 'src> Analyzer<'run, 'src> {
let source = root.to_owned();
let root = paths.get(root).unwrap();
Ok(Justfile {
aliases,
assignments,
default: recipes
let mut default = None;
for recipe in recipes.values() {
if recipe.attributes.contains(AttributeDiscriminant::Default) {
if default.is_some() {
return Err(recipe.name.error(CompileErrorKind::DuplicateDefault {
recipe: recipe.name.lexeme(),
}));
}
default = Some(Arc::clone(recipe));
}
}
let default = default.or_else(|| {
recipes
.values()
.filter(|recipe| recipe.name.path == root)
.fold(None, |accumulator, next| match accumulator {
@ -185,7 +196,13 @@ impl<'run, 'src> Analyzer<'run, 'src> {
} else {
Arc::clone(next)
}),
}),
})
});
Ok(Justfile {
aliases,
assignments,
default,
doc: doc.filter(|doc| !doc.is_empty()),
groups: groups.into(),
loaded: loaded.into(),
@ -236,7 +253,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
for parameter in &recipe.parameters {
if parameters.contains(parameter.name.lexeme()) {
return Err(parameter.name.token.error(DuplicateParameter {
return Err(parameter.name.error(DuplicateParameter {
recipe: recipe.name.lexeme(),
parameter: parameter.name.lexeme(),
}));
@ -304,7 +321,7 @@ impl<'run, 'src> Analyzer<'run, 'src> {
) -> CompileResult<'src, Alias<'src>> {
match Self::resolve_recipe(&alias.target, modules, recipes) {
Some(target) => Ok(alias.resolve(target)),
None => Err(alias.name.token.error(UnknownAliasTarget {
None => Err(alias.name.error(UnknownAliasTarget {
alias: alias.name.lexeme(),
target: alias.target,
})),

View file

@ -10,6 +10,7 @@ use super::*;
#[strum_discriminants(strum(serialize_all = "kebab-case"))]
pub(crate) enum Attribute<'src> {
Confirm(Option<StringLiteral<'src>>),
Default,
Doc(Option<StringLiteral<'src>>),
ExitMessage,
Extension(StringLiteral<'src>),
@ -34,7 +35,8 @@ impl AttributeDiscriminant {
fn argument_range(self) -> RangeInclusive<usize> {
match self {
Self::Confirm | Self::Doc => 0..=1,
Self::ExitMessage
Self::Default
| Self::ExitMessage
| Self::Linux
| Self::Macos
| Self::NoCd
@ -83,6 +85,7 @@ impl<'src> Attribute<'src> {
Ok(match discriminant {
AttributeDiscriminant::Confirm => Self::Confirm(arguments.into_iter().next()),
AttributeDiscriminant::Default => Self::Default,
AttributeDiscriminant::Doc => Self::Doc(arguments.into_iter().next()),
AttributeDiscriminant::ExitMessage => Self::ExitMessage,
AttributeDiscriminant::Extension => Self::Extension(arguments.into_iter().next().unwrap()),
@ -131,6 +134,7 @@ impl Display for Attribute<'_> {
match self {
Self::Confirm(None)
| Self::Default
| Self::Doc(None)
| Self::ExitMessage
| Self::Linux

View file

@ -103,6 +103,10 @@ impl Display for CompileError<'_> {
first.ordinal(),
self.token.line.ordinal(),
),
DuplicateDefault { recipe } => write!(
f,
"Recipe `{recipe}` has duplicate `[default]` attribute, which may only appear once per module",
),
DuplicateParameter { recipe, parameter } => {
write!(f, "Recipe `{recipe}` has duplicate parameter `{parameter}`")
}

View file

@ -27,6 +27,9 @@ pub(crate) enum CompileErrorKind<'src> {
attribute: &'src str,
first: usize,
},
DuplicateDefault {
recipe: &'src str,
},
DuplicateParameter {
recipe: &'src str,
parameter: &'src str,

View file

@ -12,88 +12,95 @@ pub(crate) enum Shell {
}
impl Shell {
pub(crate) fn script(self) -> RunResult<'static, String> {
pub(crate) fn script(self) -> &'static str {
match self {
Self::Bash => completions::clap(clap_complete::Shell::Bash),
Self::Elvish => completions::clap(clap_complete::Shell::Elvish),
Self::Fish => completions::clap(clap_complete::Shell::Fish),
Self::Nushell => Ok(completions::NUSHELL_COMPLETION_SCRIPT.into()),
Self::Powershell => completions::clap(clap_complete::Shell::PowerShell),
Self::Zsh => completions::clap(clap_complete::Shell::Zsh),
Self::Bash => include_str!("../completions/just.bash"),
Self::Elvish => include_str!("../completions/just.elvish"),
Self::Fish => include_str!("../completions/just.fish"),
Self::Nushell => include_str!("../completions/just.nu"),
Self::Powershell => include_str!("../completions/just.powershell"),
Self::Zsh => include_str!("../completions/just.zsh"),
}
}
}
fn clap(shell: clap_complete::Shell) -> RunResult<'static, String> {
fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> {
if let Some(index) = haystack.find(needle) {
haystack.replace_range(index..index + needle.len(), replacement);
Ok(())
} else {
Err(Error::internal(format!(
"Failed to find text:\n{needle}\n…in completion script:\n{haystack}"
)))
}
}
let mut script = {
let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?;
clap_complete::generate(
shell,
&mut crate::config::Config::app(),
env!("CARGO_PKG_NAME"),
&mut tempfile,
);
tempfile
.rewind()
.map_err(|io_error| Error::TempfileIo { io_error })?;
let mut buffer = String::new();
tempfile
.read_to_string(&mut buffer)
.map_err(|io_error| Error::TempfileIo { io_error })?;
buffer
#[cfg(test)]
mod tests {
use {
super::*,
pretty_assertions::assert_eq,
std::io::{Read, Seek},
tempfile::tempfile,
};
match shell {
clap_complete::Shell::Bash => {
for (needle, replacement) in completions::BASH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
clap_complete::Shell::Fish => {
script.insert_str(0, completions::FISH_RECIPE_COMPLETIONS);
}
clap_complete::Shell::PowerShell => {
for (needle, replacement) in completions::POWERSHELL_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
clap_complete::Shell::Zsh => {
for (needle, replacement) in completions::ZSH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement)?;
}
}
_ => {}
#[test]
fn scripts() {
assert_eq!(Shell::Bash.script(), clap(clap_complete::Shell::Bash));
assert_eq!(Shell::Elvish.script(), clap(clap_complete::Shell::Elvish));
assert_eq!(Shell::Fish.script(), clap(clap_complete::Shell::Fish));
assert_eq!(
Shell::Powershell.script(),
clap(clap_complete::Shell::PowerShell)
);
assert_eq!(Shell::Zsh.script(), clap(clap_complete::Shell::Zsh));
}
Ok(script.trim().into())
}
fn clap(shell: clap_complete::Shell) -> String {
fn replace(haystack: &mut String, needle: &str, replacement: &str) {
if let Some(index) = haystack.find(needle) {
haystack.replace_range(index..index + needle.len(), replacement);
} else {
panic!("Failed to find text:\n{needle}\n…in completion script:\n{haystack}")
}
}
const NUSHELL_COMPLETION_SCRIPT: &str = r#"def "nu-complete just" [] {
(^just --dump --unstable --dump-format json | from json).recipes | transpose recipe data | flatten | where {|row| $row.private == false } | select recipe doc parameters | rename value description
}
let mut script = {
let mut tempfile = tempfile().unwrap();
# Just: A Command Runner
export extern "just" [
...recipe: string@"nu-complete just", # Recipe(s) to run, may be with argument(s)
]"#;
clap_complete::generate(
shell,
&mut crate::config::Config::app(),
env!("CARGO_PKG_NAME"),
&mut tempfile,
);
const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
tempfile.rewind().unwrap();
let mut buffer = String::new();
tempfile.read_to_string(&mut buffer).unwrap();
buffer
};
match shell {
clap_complete::Shell::Bash => {
for (needle, replacement) in BASH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement);
}
}
clap_complete::Shell::Fish => {
script.insert_str(0, FISH_RECIPE_COMPLETIONS);
}
clap_complete::Shell::PowerShell => {
for (needle, replacement) in POWERSHELL_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement);
}
}
clap_complete::Shell::Zsh => {
for (needle, replacement) in ZSH_COMPLETION_REPLACEMENTS {
replace(&mut script, needle, replacement);
}
}
_ => {}
}
let mut script = script.trim().to_string();
script.push('\n');
script
}
const FISH_RECIPE_COMPLETIONS: &str = r#"function __fish_just_complete_recipes
if string match -rq '\s(-f|--justfile)\s*=?(?<justfile>[^\s]+)' -- (string split -- ' -- ' (commandline -pc))[1]
set -fx JUST_JUSTFILE "$justfile"
end
@ -109,26 +116,26 @@ complete -c just -a '(__fish_just_complete_recipes)'
# autogenerated completions
"#;
const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
(
r#" _arguments "${_arguments_options[@]}" : \"#,
r" local common=(",
),
(
r"'*--set=[Override <VARIABLE> with <VALUE>]:VARIABLE:_default:VARIABLE:_default' \",
r"'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \",
),
(
r"'()-s+[Show recipe at <PATH>]:PATH:_default' \
const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
(
r#" _arguments "${_arguments_options[@]}" : \"#,
r" local common=(",
),
(
r"'*--set=[Override <VARIABLE> with <VALUE>]:VARIABLE:_default:VARIABLE:_default' \",
r"'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \",
),
(
r"'()-s+[Show recipe at <PATH>]:PATH:_default' \
'()--show=[Show recipe at <PATH>]:PATH:_default' \",
r"'-s+[Show recipe at <PATH>]: :(_just_commands)' \
r"'-s+[Show recipe at <PATH>]: :(_just_commands)' \
'--show=[Show recipe at <PATH>]: :(_just_commands)' \",
),
(
"'*::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \
),
(
"'*::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \
justfile:_default' \\
&& ret=0",
r#")
r#")
_arguments "${_arguments_options[@]}" $common \
'1: :_just_commands' \
@ -173,10 +180,10 @@ const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
return ret
"#,
),
(
" local commands; commands=()",
r#" [[ $PREFIX = -* ]] && return 1
),
(
" local commands; commands=()",
r#" [[ $PREFIX = -* ]] && return 1
integer ret=1
local variables; variables=(
${(s: :)$(_call_program commands just --variables)}
@ -185,10 +192,10 @@ const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
${${${(M)"${(f)$(_call_program commands just --list)}":# *}/ ##/}/ ##/:Args: }
)
"#,
),
(
r#" _describe -t commands 'just commands' commands "$@""#,
r#" if compset -P '*='; then
),
(
r#" _describe -t commands 'just commands' commands "$@""#,
r#" if compset -P '*='; then
case "${${words[-1]%=*}#*=}" in
*) _message 'value' && ret=0 ;;
esac
@ -197,10 +204,10 @@ const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
_describe -t commands 'just commands' commands "$@"
fi
"#,
),
(
r#"_just "$@""#,
r#"(( $+functions[_just_variables] )) ||
),
(
r#"_just "$@""#,
r#"(( $+functions[_just_variables] )) ||
_just_variables() {
[[ $PREFIX = -* ]] && return 1
integer ret=1
@ -220,13 +227,13 @@ _just_variables() {
}
_just "$@""#,
),
];
),
];
const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
r#"$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText"#,
r#"function Get-JustFileRecipes([string[]]$CommandElements) {
r#"function Get-JustFileRecipes([string[]]$CommandElements) {
$justFileIndex = $commandElements.IndexOf("--justfile");
if ($justFileIndex -ne -1 -and $justFileIndex + 1 -le $commandElements.Length) {
@ -248,15 +255,15 @@ const POWERSHELL_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[(
$completions += $recipes
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText"#,
)];
)];
const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
(
r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
(
r#" if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi"#,
r#" if [[ ${cur} == -* ]] ; then
r#" if [[ ${cur} == -* ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
elif [[ ${COMP_CWORD} -eq 1 ]]; then
@ -273,15 +280,15 @@ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
return 0
fi
fi"#,
),
(
r"local i cur prev opts cmd",
r"local i cur prev words cword opts cmd",
),
(
r#" cur="${COMP_WORDS[COMP_CWORD]}"
),
(
r"local i cur prev opts cmd",
r"local i cur prev words cword opts cmd",
),
(
r#" cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}""#,
r#"
r#"
# Modules use "::" as the separator, which is considered a wordbreak character in bash.
# The _get_comp_words_by_ref function is a hack to allow for exceptions to this rule without
# modifying the global COMP_WORDBREAKS environment variable.
@ -294,14 +301,15 @@ const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[
cword=$COMP_CWORD
fi
"#,
),
(r"for i in ${COMP_WORDS[@]}", r"for i in ${words[@]}"),
(r"elif [[ ${COMP_CWORD} -eq 1 ]]; then", r"else"),
(
r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )"#,
r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
),
(r"for i in ${COMP_WORDS[@]}", r"for i in ${words[@]}"),
(r"elif [[ ${COMP_CWORD} -eq 1 ]]; then", r"else"),
(
r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )"#,
r#"COMPREPLY=( $(compgen -W "${recipes}" -- "${cur}") )
if type __ltrim_colon_completions &>/dev/null; then
__ltrim_colon_completions "$cur"
fi"#,
),
];
),
];
}

View file

@ -14,6 +14,7 @@ use {
pub(crate) struct Config {
pub(crate) alias_style: AliasStyle,
pub(crate) allow_missing: bool,
pub(crate) ceiling: Option<PathBuf>,
pub(crate) check: bool,
pub(crate) color: Color,
pub(crate) command_color: Option<ansi_term::Color>,
@ -92,6 +93,7 @@ mod arg {
pub(crate) const ALIAS_STYLE: &str = "ALIAS_STYLE";
pub(crate) const ALLOW_MISSING: &str = "ALLOW-MISSING";
pub(crate) const ARGUMENTS: &str = "ARGUMENTS";
pub(crate) const CEILING: &str = "CEILING";
pub(crate) const CHECK: &str = "CHECK";
pub(crate) const CHOOSER: &str = "CHOOSER";
pub(crate) const CLEAR_SHELL_ARGS: &str = "CLEAR-SHELL-ARGS";
@ -161,6 +163,14 @@ impl Config {
.help("Set list command alias display style")
.conflicts_with(arg::NO_ALIASES),
)
.arg(
Arg::new(arg::CEILING)
.long("ceiling")
.env("JUST_CEILING")
.action(ArgAction::Set)
.value_parser(value_parser!(PathBuf))
.help("Do not ascend above <CEILING> directory when searching for a justfile."),
)
.arg(
Arg::new(arg::CHECK)
.long("check")
@ -777,6 +787,7 @@ impl Config {
.unwrap()
.clone(),
allow_missing: matches.get_flag(arg::ALLOW_MISSING),
ceiling: matches.get_one::<PathBuf>(arg::CEILING).cloned(),
check: matches.get_flag(arg::CHECK),
color: (*matches.get_one::<UseColor>(arg::COLOR).unwrap()).into(),
command_color: matches

View file

@ -185,9 +185,6 @@ pub(crate) enum Error<'src> {
recipe: &'src str,
io_error: io::Error,
},
TempfileIo {
io_error: io::Error,
},
Unknown {
recipe: &'src str,
line_number: Option<usize>,
@ -501,9 +498,6 @@ impl ColorDisplay for Error<'_> {
write!(f, "Recipe `{recipe}` could not be run because of an IO error while trying to create a temporary \
directory or write a file to that directory: {io_error}")?;
}
TempfileIo { io_error } => {
write!(f, "Tempfile I/O error: {io_error}")?;
}
Unknown { recipe, line_number} => {
if let Some(n) = line_number {
write!(f, "Recipe `{recipe}` failed on line {n} for an unknown reason")?;

View file

@ -8,7 +8,7 @@ pub(crate) struct Interpreter<'src> {
impl Interpreter<'_> {
pub(crate) fn default_script_interpreter() -> &'static Interpreter<'static> {
static INSTANCE: Lazy<Interpreter<'static>> = Lazy::new(|| Interpreter {
static INSTANCE: LazyLock<Interpreter<'static>> = LazyLock::new(|| Interpreter {
arguments: vec![StringLiteral::from_raw("-eu")],
command: StringLiteral::from_raw("sh"),
});

View file

@ -15,7 +15,7 @@ pub(crate) enum Item<'src> {
Module {
absolute: Option<PathBuf>,
doc: Option<String>,
groups: Vec<String>,
groups: Vec<StringLiteral<'src>>,
name: Name<'src>,
optional: bool,
relative: Option<StringLiteral<'src>>,
@ -45,11 +45,21 @@ impl Display for Item<'_> {
write!(f, " {relative}")
}
Self::Module {
doc,
groups,
name,
relative,
optional,
relative,
..
} => {
if let Some(doc) = doc {
writeln!(f, "# {doc}")?;
}
for group in groups {
writeln!(f, "[group: {group}]")?;
}
write!(f, "mod")?;
if *optional {

View file

@ -13,7 +13,7 @@ pub(crate) struct Justfile<'src> {
#[serde(rename = "first", serialize_with = "keyed::serialize_option")]
pub(crate) default: Option<Arc<Recipe<'src>>>,
pub(crate) doc: Option<String>,
pub(crate) groups: Vec<String>,
pub(crate) groups: Vec<StringLiteral<'src>>,
#[serde(skip)]
pub(crate) loaded: Vec<PathBuf>,
#[serde(skip)]
@ -49,12 +49,22 @@ impl<'src> Justfile<'src> {
input,
self
.recipes
.keys()
.map(|name| Suggestion { name, target: None })
.chain(self.aliases.iter().map(|(name, alias)| Suggestion {
name,
target: Some(alias.target.name.lexeme()),
})),
.values()
.filter(|recipe| recipe.is_public())
.map(|recipe| Suggestion {
name: recipe.name(),
target: None,
})
.chain(
self
.aliases
.values()
.filter(|alias| alias.is_public())
.map(|alias| Suggestion {
name: alias.name.lexeme(),
target: Some(alias.target.name.lexeme()),
}),
),
)
}
@ -458,8 +468,12 @@ impl<'src> Justfile<'src> {
recipes
}
pub(crate) fn groups(&self) -> &[String] {
&self.groups
pub(crate) fn groups(&self) -> Vec<&str> {
self
.groups
.iter()
.map(|group| group.cooked.as_str())
.collect()
}
pub(crate) fn public_groups(&self, config: &Config) -> Vec<String> {

View file

@ -104,7 +104,6 @@ pub(crate) use {
edit_distance::edit_distance,
lexiclean::Lexiclean,
libc::EXIT_FAILURE,
once_cell::sync::Lazy,
rand::seq::IndexedRandom,
regex::Regex,
serde::{
@ -120,7 +119,7 @@ pub(crate) use {
ffi::OsString,
fmt::{self, Debug, Display, Formatter},
fs,
io::{self, Read, Seek, Write},
io::{self, Write},
iter::{self, FromIterator},
mem,
ops::Deref,
@ -128,11 +127,11 @@ pub(crate) use {
path::{self, Path, PathBuf},
process::{self, Command, ExitStatus, Stdio},
str::{self, Chars},
sync::{Arc, Mutex, MutexGuard, OnceLock},
sync::{Arc, LazyLock, Mutex, MutexGuard, OnceLock},
thread, vec,
},
strum::{Display, EnumDiscriminants, EnumString, IntoStaticStr},
tempfile::{tempfile, TempDir},
tempfile::TempDir,
typed_arena::Arena,
unicode_width::{UnicodeWidthChar, UnicodeWidthStr},
};

View file

@ -411,7 +411,7 @@ impl<'run, 'src> Parser<'run, 'src> {
let mut groups = Vec::new();
for attribute in attributes {
if let Attribute::Group(group) = attribute {
groups.push(group.cooked);
groups.push(group);
}
}

View file

@ -34,3 +34,17 @@ pub fn run(args: impl Iterator<Item = impl Into<OsString> + Clone>) -> Result<()
error.code().unwrap_or(EXIT_FAILURE)
})
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn run_can_be_called_more_than_once() {
let tmp = testing::tempdir();
fs::write(tmp.path().join("justfile"), "foo:").unwrap();
let search_directory = format!("{}/", tmp.path().to_str().unwrap());
run(["just", &search_directory].iter()).unwrap();
run(["just", &search_directory].iter()).unwrap();
}
}

View file

@ -31,14 +31,17 @@ impl Search {
/// Find justfile given search configuration and invocation directory
pub(crate) fn find(
search_config: &SearchConfig,
ceiling: Option<&Path>,
invocation_directory: &Path,
search_config: &SearchConfig,
) -> SearchResult<Self> {
match search_config {
SearchConfig::FromInvocationDirectory => Self::find_in_directory(invocation_directory),
SearchConfig::FromInvocationDirectory => {
Self::find_in_directory(ceiling, invocation_directory)
}
SearchConfig::FromSearchDirectory { search_directory } => {
let search_directory = Self::clean(invocation_directory, search_directory);
let justfile = Self::justfile(&search_directory)?;
let justfile = Self::justfile(ceiling, &search_directory)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self {
justfile,
@ -47,7 +50,7 @@ impl Search {
}
SearchConfig::GlobalJustfile => Ok(Self {
justfile: Self::find_global_justfile()?,
working_directory: Self::project_root(invocation_directory)?,
working_directory: Self::project_root(ceiling, invocation_directory)?,
}),
SearchConfig::WithJustfile { justfile } => {
let justfile = Self::clean(invocation_directory, justfile);
@ -88,7 +91,7 @@ impl Search {
}
/// Find justfile starting from parent directory of current justfile
pub(crate) fn search_parent_directory(&self) -> SearchResult<Self> {
pub(crate) fn search_parent_directory(&self, ceiling: Option<&Path>) -> SearchResult<Self> {
let parent = self
.justfile
.parent()
@ -96,12 +99,12 @@ impl Search {
.ok_or_else(|| SearchError::JustfileHadNoParent {
path: self.justfile.clone(),
})?;
Self::find_in_directory(parent)
Self::find_in_directory(ceiling, parent)
}
/// Find justfile starting in given directory searching upwards in directory tree
fn find_in_directory(starting_dir: &Path) -> SearchResult<Self> {
let justfile = Self::justfile(starting_dir)?;
fn find_in_directory(ceiling: Option<&Path>, starting_dir: &Path) -> SearchResult<Self> {
let justfile = Self::justfile(ceiling, starting_dir)?;
let working_directory = Self::working_directory_from_justfile(&justfile)?;
Ok(Self {
justfile,
@ -113,10 +116,11 @@ impl Search {
pub(crate) fn init(
search_config: &SearchConfig,
invocation_directory: &Path,
ceiling: Option<&Path>,
) -> SearchResult<Self> {
match search_config {
SearchConfig::FromInvocationDirectory => {
let working_directory = Self::project_root(invocation_directory)?;
let working_directory = Self::project_root(ceiling, invocation_directory)?;
let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
Ok(Self {
justfile,
@ -125,7 +129,7 @@ impl Search {
}
SearchConfig::FromSearchDirectory { 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(ceiling, &search_directory)?;
let justfile = working_directory.join(DEFAULT_JUSTFILE_NAME);
Ok(Self {
justfile,
@ -153,7 +157,7 @@ impl Search {
/// Search upwards from `directory` for a file whose name matches one of
/// `JUSTFILE_NAMES`
fn justfile(directory: &Path) -> SearchResult<PathBuf> {
fn justfile(ceiling: Option<&Path>, directory: &Path) -> SearchResult<PathBuf> {
for directory in directory.ancestors() {
let mut candidates = BTreeSet::new();
@ -181,6 +185,12 @@ impl Search {
1 => return Ok(candidates.into_iter().next().unwrap()),
_ => return Err(SearchError::MultipleCandidates { candidates }),
}
if let Some(ceiling) = ceiling {
if directory == ceiling {
break;
}
}
}
Err(SearchError::NotFound)
@ -207,7 +217,7 @@ impl Search {
/// Search upwards from `directory` for the root directory of a software
/// project, as determined by the presence of one of the version control
/// system directories given in `PROJECT_ROOT_CHILDREN`
fn project_root(directory: &Path) -> SearchResult<PathBuf> {
fn project_root(ceiling: Option<&Path>, directory: &Path) -> SearchResult<PathBuf> {
for directory in directory.ancestors() {
let entries = fs::read_dir(directory).map_err(|io_error| SearchError::Io {
io_error,
@ -225,6 +235,12 @@ impl Search {
}
}
}
if let Some(ceiling) = ceiling {
if directory == ceiling {
break;
}
}
}
Ok(directory.to_owned())
@ -250,7 +266,7 @@ mod tests {
#[test]
fn not_found() {
let tmp = testing::tempdir();
match Search::justfile(tmp.path()) {
match Search::justfile(None, tmp.path()) {
Err(SearchError::NotFound) => {}
_ => panic!("No justfile found error was expected"),
}
@ -270,7 +286,7 @@ mod tests {
}
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
match Search::justfile(path.as_path()) {
match Search::justfile(None, path.as_path()) {
Err(SearchError::MultipleCandidates { .. }) => {}
_ => panic!("Multiple candidates error was expected"),
}
@ -283,7 +299,7 @@ mod tests {
path.push(DEFAULT_JUSTFILE_NAME);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
if let Err(err) = Search::justfile(path.as_path()) {
if let Err(err) = Search::justfile(None, path.as_path()) {
panic!("No errors were expected: {err}");
}
}
@ -306,7 +322,7 @@ mod tests {
path.push(spongebob_case);
fs::write(&path, "default:\n\techo ok").unwrap();
path.pop();
if let Err(err) = Search::justfile(path.as_path()) {
if let Err(err) = Search::justfile(None, path.as_path()) {
panic!("No errors were expected: {err}");
}
}
@ -322,7 +338,7 @@ mod tests {
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
path.push("b");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
if let Err(err) = Search::justfile(path.as_path()) {
if let Err(err) = Search::justfile(None, path.as_path()) {
panic!("No errors were expected: {err}");
}
}
@ -341,7 +357,7 @@ mod tests {
path.pop();
path.push("b");
fs::create_dir(&path).expect("test justfile search: failed to create intermediary directory");
match Search::justfile(path.as_path()) {
match Search::justfile(None, path.as_path()) {
Ok(found_path) => {
path.pop();
path.push(DEFAULT_JUSTFILE_NAME);
@ -370,7 +386,7 @@ mod tests {
let search_config = SearchConfig::FromInvocationDirectory;
let search = Search::find(&search_config, &sub).unwrap();
let search = Search::find(None, &sub, &search_config).unwrap();
assert_eq!(search.justfile, justfile);
assert_eq!(search.working_directory, sub);

View file

@ -3,6 +3,7 @@ use super::*;
pub(crate) struct SignalHandler {
caught: Option<Signal>,
children: BTreeMap<i32, Command>,
initialized: bool,
verbosity: Verbosity,
}
@ -10,7 +11,11 @@ impl SignalHandler {
pub(crate) fn install(verbosity: Verbosity) -> RunResult<'static> {
let mut instance = Self::instance();
instance.verbosity = verbosity;
Platform::install_signal_handler(|signal| Self::instance().interrupt(signal))
if !instance.initialized {
Platform::install_signal_handler(|signal| Self::instance().handle(signal))?;
instance.initialized = true;
}
Ok(())
}
pub(crate) fn instance() -> MutexGuard<'static, Self> {
@ -35,11 +40,12 @@ impl SignalHandler {
Self {
caught: None,
children: BTreeMap::new(),
initialized: false,
verbosity: Verbosity::default(),
}
}
fn interrupt(&mut self, signal: Signal) {
fn handle(&mut self, signal: Signal) {
if signal.is_fatal() {
if self.children.is_empty() {
process::exit(signal.code());

View file

@ -6,6 +6,7 @@ use {
},
std::{
fs::File,
io::Read,
os::fd::{BorrowedFd, IntoRawFd},
sync::atomic::{self, AtomicI32},
},

View file

@ -63,14 +63,21 @@ impl Subcommand {
Self::changelog();
return Ok(());
}
Completions { shell } => return Self::completions(*shell),
Completions { shell } => {
Self::completions(*shell);
return Ok(());
}
Init => return Self::init(config),
Man => return Self::man(),
Request { request } => return Self::request(request),
_ => {}
}
let search = Search::find(&config.search_config, &config.invocation_directory)?;
let search = Search::find(
config.ceiling.as_deref(),
&config.invocation_directory,
&config.search_config,
)?;
if let Edit = self {
return Self::edit(&search);
@ -132,7 +139,9 @@ impl Subcommand {
if fallback {
if let Err(err @ (Error::UnknownRecipe { .. } | Error::UnknownSubmodule { .. })) = result {
search = search.search_parent_directory().map_err(|_| err)?;
search = search
.search_parent_directory(config.ceiling.as_deref())
.map_err(|_| err)?;
if config.verbosity.loquacious() {
eprintln!(
@ -277,9 +286,8 @@ impl Subcommand {
justfile.run(config, search, overrides, &recipes)
}
fn completions(shell: completions::Shell) -> RunResult<'static, ()> {
println!("{}", shell.script()?);
Ok(())
fn completions(shell: completions::Shell) {
print!("{}", shell.script());
}
fn dump(config: &Config, compilation: Compilation) -> RunResult<'static> {
@ -366,7 +374,11 @@ impl Subcommand {
}
fn init(config: &Config) -> RunResult<'static> {
let search = Search::init(&config.search_config, &config.invocation_directory)?;
let search = Search::init(
&config.search_config,
&config.invocation_directory,
config.ceiling.as_deref(),
)?;
if search.justfile.is_file() {
return Err(Error::InitExists {
@ -509,7 +521,7 @@ impl Subcommand {
BTreeMap::new()
} else {
let mut aliases = BTreeMap::<&str, Vec<&str>>::new();
for alias in module.aliases.values().filter(|alias| !alias.is_private()) {
for alias in module.aliases.values().filter(|alias| alias.is_public()) {
aliases
.entry(alias.target.name.lexeme())
.or_default()

84
tests/ceiling.rs Normal file
View file

@ -0,0 +1,84 @@
use super::*;
#[test]
fn justfile_run_search_stops_at_ceiling_dir() {
let tempdir = tempdir();
let ceiling = tempdir.path().join("foo");
fs::create_dir(&ceiling).unwrap();
#[cfg(not(windows))]
let ceiling = ceiling.canonicalize().unwrap();
Test::with_tempdir(tempdir)
.justfile(
"
foo:
echo bar
",
)
.create_dir("foo/bar")
.current_dir("foo/bar")
.args(["--ceiling", ceiling.to_str().unwrap()])
.stderr("error: No justfile found\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn ceiling_can_be_passed_as_environment_variable() {
let tempdir = tempdir();
let ceiling = tempdir.path().join("foo");
fs::create_dir(&ceiling).unwrap();
#[cfg(not(windows))]
let ceiling = ceiling.canonicalize().unwrap();
Test::with_tempdir(tempdir)
.justfile(
"
foo:
echo bar
",
)
.create_dir("foo/bar")
.current_dir("foo/bar")
.env("JUST_CEILING", ceiling.to_str().unwrap())
.stderr("error: No justfile found\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn justfile_init_search_stops_at_ceiling_dir() {
let tempdir = tempdir();
let ceiling = tempdir.path().join("foo");
fs::create_dir(&ceiling).unwrap();
#[cfg(not(windows))]
let ceiling = ceiling.canonicalize().unwrap();
let Output { tempdir, .. } = Test::with_tempdir(tempdir)
.no_justfile()
.test_round_trip(false)
.create_dir(".git")
.create_dir("foo/bar")
.current_dir("foo/bar")
.args(["--init", "--ceiling", ceiling.to_str().unwrap()])
.stderr_regex(if cfg!(windows) {
r"Wrote justfile to `.*\\foo\\bar\\justfile`\n"
} else {
"Wrote justfile to `.*/foo/bar/justfile`\n"
})
.run();
assert_eq!(
fs::read_to_string(tempdir.path().join("foo/bar/justfile")).unwrap(),
just::INIT_JUSTFILE
);
}

43
tests/default.rs Normal file
View file

@ -0,0 +1,43 @@
use super::*;
#[test]
fn default_attribute_overrides_first_recipe() {
Test::new()
.justfile(
"
foo:
@echo FOO
[default]
bar:
@echo BAR
",
)
.stdout("BAR\n")
.run();
}
#[test]
fn default_attribute_may_only_appear_once_per_justfile() {
Test::new()
.justfile(
"
[default]
foo:
[default]
bar:
",
)
.stderr(
"
error: Recipe `foo` has duplicate `[default]` attribute, which may only appear once per module
justfile:2:1
2 foo:
^^^
"
)
.status(EXIT_FAILURE)
.run();
}

View file

@ -1589,3 +1589,45 @@ fn private_variable() {
)
.run();
}
#[test]
fn module_groups_are_preserved() {
Test::new()
.justfile(
r#"
[group('bar')]
[group("baz")]
mod foo
"#,
)
.write("foo.just", "")
.arg("--dump")
.stdout(
r#"
[group: 'bar']
[group: "baz"]
mod foo
"#,
)
.run();
}
#[test]
fn module_docs_are_preserved() {
Test::new()
.justfile(
r"
# bar
mod foo
",
)
.write("foo.just", "")
.arg("--dump")
.stdout(
r"
# bar
mod foo
",
)
.run();
}

View file

@ -52,6 +52,7 @@ mod assignment;
mod attributes;
mod backticks;
mod byte_order_mark;
mod ceiling;
mod changelog;
mod choose;
mod command;
@ -60,6 +61,7 @@ mod conditional;
mod confirm;
mod constants;
mod datetime;
mod default;
mod delimiters;
mod dependencies;
mod directories;

View file

@ -1351,19 +1351,62 @@ b:
fn run_suggestion() {
Test::new()
.arg("hell")
.justfile(
r#"
hello a b='B ' c='C':
echo {{a}} {{b}} {{c}}
a Z="\t z":
"#,
)
.justfile("hello:")
.stderr("error: Justfile does not contain recipe `hell`\nDid you mean `hello`?\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn private_recipes_are_not_suggested() {
Test::new()
.arg("hell")
.justfile(
"
[private]
hello:
",
)
.stderr("error: Justfile does not contain recipe `hell`\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn alias_suggestion() {
Test::new()
.arg("hell")
.justfile(
"
alias hello := bar
bar:
",
)
.stderr(
"error: Justfile does not contain recipe `hell`\nDid you mean `hello`, an alias for `bar`?\n",
)
.status(EXIT_FAILURE)
.run();
}
#[test]
fn private_aliases_are_not_suggested() {
Test::new()
.arg("hell")
.justfile(
"
[private]
alias hello := bar
bar:
",
)
.stderr("error: Justfile does not contain recipe `hell`\n")
.status(EXIT_FAILURE)
.run();
}
#[test]
fn line_continuation_with_space() {
Test::new()

View file

@ -147,7 +147,6 @@ impl Test {
self
}
#[allow(unused)]
pub(crate) fn test_round_trip(mut self, test_round_trip: bool) -> Self {
self.test_round_trip = test_round_trip;
self

View file

@ -134,6 +134,7 @@ if [ -z "${target-}" ]; then
arm64-Darwin) target=aarch64-apple-darwin;;
armv6l-Linux) target=arm-unknown-linux-musleabihf;;
armv7l-Linux) target=armv7-unknown-linux-musleabihf;;
loongarch64-Linux) target=loongarch64-unknown-linux-musl;;
x86_64-Darwin) target=x86_64-apple-darwin;;
x86_64-Linux) target=x86_64-unknown-linux-musl;;
x86_64-MINGW64_NT) target=x86_64-pc-windows-msvc;;