diff --git a/Cargo.lock b/Cargo.lock index b019ce4..4b90853 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -172,13 +172,49 @@ dependencies = [ "ansi_term", "atty", "bitflags 1.3.2", - "strsim", - "term_size", + "strsim 0.8.0", "textwrap", "unicode-width", "vec_map", ] +[[package]] +name = "clap" +version = "4.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" +dependencies = [ + "anstream", + "anstyle", + "clap_lex", + "strsim 0.11.1", + "terminal_size", +] + +[[package]] +name = "clap_complete" +version = "4.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" +dependencies = [ + "clap 4.5.4", +] + +[[package]] +name = "clap_lex" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + [[package]] name = "colorchoice" version = "1.0.1" @@ -474,7 +510,8 @@ dependencies = [ "atty", "blake3", "camino", - "clap", + "clap 4.5.4", + "clap_complete", "cradle", "ctrlc", "derivative", @@ -847,13 +884,19 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "structopt" version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" dependencies = [ - "clap", + "clap 2.34.0", "lazy_static", "structopt-derive", ] @@ -943,13 +986,13 @@ dependencies = [ ] [[package]] -name = "term_size" -version = "0.3.2" +name = "terminal_size" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e4129646ca0ed8f45d09b929036bafad5377103edd06e50bf574b353d2b08d9" +checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7" dependencies = [ - "libc", - "winapi", + "rustix", + "windows-sys 0.48.0", ] [[package]] @@ -958,7 +1001,6 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" dependencies = [ - "term_size", "unicode-width", ] diff --git a/Cargo.toml b/Cargo.toml index 6253f5b..cbbb1e1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,7 +22,8 @@ ansi_term = "0.12.0" atty = "0.2.0" blake3 = { version = "1.5.0", features = ["rayon", "mmap"] } camino = "1.0.4" -clap = { version = "2.33.0", features = ["wrap_help"] } +clap = { version = "4.0.0", features = ["env", "wrap_help"] } +clap_complete = "4.0.0" ctrlc = { version = "3.1.1", features = ["termination"] } derivative = "2.0.0" dirs = "5.0.1" diff --git a/completions/just.bash b/completions/just.bash index 57f0041..0cd0880 100644 --- a/completions/just.bash +++ b/completions/just.bash @@ -1,5 +1,5 @@ _just() { - local i cur prev words cword opts cmds + local i cur prev words cword opts cmd COMPREPLY=() # Modules use "::" as the separator, which is considered a wordbreak character in bash. @@ -19,19 +19,18 @@ _just() { for i in ${words[@]} do - case "${i}" in - "$1") + case "${cmd},${i}" in + ",$1") cmd="just" ;; - *) ;; esac done case "${cmd}" in - just) - opts=" -n -q -u -v -e -l -h -V -f -d -c -s -E --check --yes --dry-run --highlight --no-aliases --no-deps --no-dotenv --no-highlight --quiet --shell-command --clear-shell-args --unsorted --unstable --verbose --changelog --choose --dump --edit --evaluate --fmt --init --list --summary --variables --help --version --chooser --color --command-color --dump-format --list-heading --list-prefix --justfile --set --shell --shell-arg --working-directory --command --completions --show --dotenv-filename --dotenv-path ... " + "$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 --show --summary --variables --dotenv-filename --dotenv-path --help --version [ARGUMENTS]..." if [[ ${cur} == -* ]] ; then COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 @@ -53,7 +52,6 @@ _just() { fi fi case "${prev}" in - --chooser) COMPREPLY=($(compgen -f "${cur}")) return 0 @@ -82,7 +80,7 @@ _just() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -f) + -f) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -102,7 +100,7 @@ _just() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -d) + -d) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -110,19 +108,19 @@ _just() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -c) + -c) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; --completions) - COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}")) + COMPREPLY=($(compgen -W "bash elvish fish powershell zsh" -- "${cur}")) return 0 ;; --show) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -s) + -s) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -134,7 +132,7 @@ _just() { COMPREPLY=($(compgen -f "${cur}")) return 0 ;; - -E) + -E) COMPREPLY=($(compgen -f "${cur}")) return 0 ;; @@ -145,8 +143,11 @@ _just() { COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") ) return 0 ;; - esac } -complete -F _just -o bashdefault -o default just +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 diff --git a/completions/just.elvish b/completions/just.elvish index 6ec3874..86882d5 100644 --- a/completions/just.elvish +++ b/completions/just.elvish @@ -1,18 +1,21 @@ -edit:completion:arg-completer[just] = [@words]{ - fn spaces [n]{ - repeat $n ' ' | joins '' +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-suffix=' '(spaces (- 14 (wcswidth $text)))$desc + fn cand {|text desc| + edit:complex-candidate $text &display=$text' '(spaces (- 14 (wcswidth $text)))$desc } - command = 'just' - for word $words[1:-1] { - if (has-prefix $word '-') { + var command = 'just' + for word $words[1..-1] { + if (str:has-prefix $word '-') { break } - command = $command';'$word + set command = $command';'$word } - completions = [ + var completions = [ &'just'= { cand --chooser 'Override binary invoked by `--choose`' cand --color 'Print colorful output' @@ -65,10 +68,10 @@ edit:completion:arg-completer[just] = [@words]{ cand --list 'List available recipes and their arguments' cand --summary 'List names of available recipes' cand --variables 'List names of variables' - cand -h 'Print help information' - cand --help 'Print help information' - cand -V 'Print version information' - cand --version 'Print version information' + cand -h 'Print help' + cand --help 'Print help' + cand -V 'Print version' + cand --version 'Print version' } ] $completions[$command] diff --git a/completions/just.fish b/completions/just.fish index 6664bd6..23da104 100644 --- a/completions/just.fish +++ b/completions/just.fish @@ -35,45 +35,45 @@ complete -c just -n "__fish_is_first_arg" --no-files complete -c just -a '(__fish_just_complete_recipes)' # autogenerated completions -complete -c just -n "__fish_use_subcommand" -l chooser -d 'Override binary invoked by `--choose`' -complete -c just -n "__fish_use_subcommand" -l color -d 'Print colorful output' -r -f -a "auto always never" -complete -c just -n "__fish_use_subcommand" -l command-color -d 'Echo recipe lines in ' -r -f -a "black blue cyan green purple red yellow" -complete -c just -n "__fish_use_subcommand" -l dump-format -d 'Dump justfile as ' -r -f -a "just json" -complete -c just -n "__fish_use_subcommand" -l list-heading -d 'Print before list' -complete -c just -n "__fish_use_subcommand" -l list-prefix -d 'Print before each list item' -complete -c just -n "__fish_use_subcommand" -s f -l justfile -d 'Use as justfile' -complete -c just -n "__fish_use_subcommand" -l set -d 'Override with ' -complete -c just -n "__fish_use_subcommand" -l shell -d 'Invoke to run recipes' -complete -c just -n "__fish_use_subcommand" -l shell-arg -d 'Invoke shell with as an argument' -complete -c just -n "__fish_use_subcommand" -s d -l working-directory -d 'Use as working directory. --justfile must also be set' -complete -c just -n "__fish_use_subcommand" -s c -l command -d 'Run an arbitrary command with the working directory, `.env`, overrides, and exports set' -complete -c just -n "__fish_use_subcommand" -l completions -d 'Print shell completion script for ' -r -f -a "zsh bash fish powershell elvish" -complete -c just -n "__fish_use_subcommand" -s s -l show -d 'Show information about ' -complete -c just -n "__fish_use_subcommand" -l dotenv-filename -d 'Search for environment file named instead of `.env`' -complete -c just -n "__fish_use_subcommand" -s E -l dotenv-path -d 'Load as environment file instead of searching for one' -complete -c just -n "__fish_use_subcommand" -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 -n "__fish_use_subcommand" -l yes -d 'Automatically confirm all recipes.' -complete -c just -n "__fish_use_subcommand" -s n -l dry-run -d 'Print what just would do without doing it' -complete -c just -n "__fish_use_subcommand" -l highlight -d 'Highlight echoed recipe lines in bold' -complete -c just -n "__fish_use_subcommand" -l no-aliases -d 'Don\'t show aliases in list' -complete -c just -n "__fish_use_subcommand" -l no-deps -d 'Don\'t run recipe dependencies' -complete -c just -n "__fish_use_subcommand" -l no-dotenv -d 'Don\'t load `.env` file' -complete -c just -n "__fish_use_subcommand" -l no-highlight -d 'Don\'t highlight echoed recipe lines in bold' -complete -c just -n "__fish_use_subcommand" -s q -l quiet -d 'Suppress all output' -complete -c just -n "__fish_use_subcommand" -l shell-command -d 'Invoke with the shell used to run recipe lines and backticks' -complete -c just -n "__fish_use_subcommand" -l clear-shell-args -d 'Clear shell arguments' -complete -c just -n "__fish_use_subcommand" -s u -l unsorted -d 'Return list and summary entries in source order' -complete -c just -n "__fish_use_subcommand" -l unstable -d 'Enable unstable features' -complete -c just -n "__fish_use_subcommand" -s v -l verbose -d 'Use verbose output' -complete -c just -n "__fish_use_subcommand" -l changelog -d 'Print changelog' -complete -c just -n "__fish_use_subcommand" -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 -n "__fish_use_subcommand" -l dump -d 'Print justfile' -complete -c just -n "__fish_use_subcommand" -s e -l edit -d 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`' -complete -c just -n "__fish_use_subcommand" -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 -n "__fish_use_subcommand" -l fmt -d 'Format and overwrite justfile' -complete -c just -n "__fish_use_subcommand" -l init -d 'Initialize new justfile in project root' -complete -c just -n "__fish_use_subcommand" -s l -l list -d 'List available recipes and their arguments' -complete -c just -n "__fish_use_subcommand" -l summary -d 'List names of available recipes' -complete -c just -n "__fish_use_subcommand" -l variables -d 'List names of variables' -complete -c just -n "__fish_use_subcommand" -s h -l help -d 'Print help information' -complete -c just -n "__fish_use_subcommand" -s V -l version -d 'Print version information' +complete -c just -l chooser -d 'Override binary invoked by `--choose`' -r +complete -c just -l color -d 'Print colorful output' -r -f -a "{auto '',always '',never ''}" +complete -c just -l command-color -d 'Echo recipe lines in ' -r -f -a "{black '',blue '',cyan '',green '',purple '',red '',yellow ''}" +complete -c just -l dump-format -d 'Dump justfile as ' -r -f -a "{just '',json ''}" +complete -c just -l list-heading -d 'Print before list' -r +complete -c just -l list-prefix -d 'Print before each list item' -r +complete -c just -s f -l justfile -d 'Use as justfile' -r -F +complete -c just -l set -d 'Override with ' -r +complete -c just -l shell -d 'Invoke to run recipes' -r +complete -c just -l shell-arg -d 'Invoke shell with as an argument' -r +complete -c just -s d -l working-directory -d 'Use 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 ' -r -f -a "{bash '',elvish '',fish '',powershell '',zsh ''}" +complete -c just -s s -l show -d 'Show information about ' -r +complete -c just -l dotenv-filename -d 'Search for environment file named instead of `.env`' -r +complete -c just -s E -l dotenv-path -d 'Load as environment file instead of searching for one' -r -F +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 yes -d 'Automatically confirm all recipes.' +complete -c just -s n -l dry-run -d 'Print what just would do without doing it' +complete -c just -l highlight -d 'Highlight echoed recipe lines in bold' +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 -s q -l quiet -d 'Suppress all output' +complete -c just -l shell-command -d 'Invoke with the shell used to run recipe lines and backticks' +complete -c just -l clear-shell-args -d 'Clear shell arguments' +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 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 init -d 'Initialize new justfile in project root' +complete -c just -s l -l list -d 'List available recipes and their arguments' +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' diff --git a/completions/just.powershell b/completions/just.powershell index fbdc529..f46cea1 100644 --- a/completions/just.powershell +++ b/completions/just.powershell @@ -11,7 +11,8 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { $element = $commandElements[$i] if ($element -isnot [StringConstantExpressionAst] -or $element.StringConstantType -ne [StringConstantType]::BareWord -or - $element.Value.StartsWith('-')) { + $element.Value.StartsWith('-') -or + $element.Value -eq $wordToComplete) { break } $element.Value @@ -38,7 +39,7 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about ') [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about ') [CompletionResult]::new('--dotenv-filename', 'dotenv-filename', [CompletionResultType]::ParameterName, 'Search for environment file named instead of `.env`') - [CompletionResult]::new('-E', 'E', [CompletionResultType]::ParameterName, 'Load as environment file instead of searching for one') + [CompletionResult]::new('-E', 'E ', [CompletionResultType]::ParameterName, 'Load as environment file instead of searching for one') [CompletionResult]::new('--dotenv-path', 'dotenv-path', [CompletionResultType]::ParameterName, 'Load as environment file instead of searching for one') [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('--yes', 'yes', [CompletionResultType]::ParameterName, 'Automatically confirm all recipes.') @@ -70,10 +71,10 @@ Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock { [CompletionResult]::new('--list', 'list', [CompletionResultType]::ParameterName, 'List available recipes and their arguments') [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 information') - [CompletionResult]::new('--help', 'help', [CompletionResultType]::ParameterName, 'Print help information') - [CompletionResult]::new('-V', 'V', [CompletionResultType]::ParameterName, 'Print version information') - [CompletionResult]::new('--version', 'version', [CompletionResultType]::ParameterName, 'Print version information') + [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 } }) diff --git a/completions/just.zsh b/completions/just.zsh index 45f06a7..4550daa 100644 --- a/completions/just.zsh +++ b/completions/just.zsh @@ -15,35 +15,35 @@ _just() { local context curcontext="$curcontext" state line local common=( -'--chooser=[Override binary invoked by `--choose`]' \ +'--chooser=[Override binary invoked by \`--choose\`]: : ' \ '--color=[Print colorful output]: :(auto always never)' \ '--command-color=[Echo recipe lines in ]: :(black blue cyan green purple red yellow)' \ -'--dump-format=[Dump justfile as ]: :(just json)' \ -'--list-heading=[Print before list]' \ -'--list-prefix=[Print before each list item]' \ -'-f+[Use as justfile]' \ -'--justfile=[Use as justfile]' \ -'*--set[Override with ]: :_just_variables' \ -'--shell=[Invoke to run recipes]' \ -'*--shell-arg=[Invoke shell with as an argument]' \ -'-d+[Use as working directory. --justfile must also be set]' \ -'--working-directory=[Use as working directory. --justfile must also be set]' \ -'-c+[Run an arbitrary command with the working directory, `.env`, overrides, and exports set]' \ -'--command=[Run an arbitrary command with the working directory, `.env`, overrides, and exports set]' \ -'--completions=[Print shell completion script for ]: :(zsh bash fish powershell elvish)' \ -'-s+[Show information about ]: :_just_commands' \ -'--show=[Show information about ]: :_just_commands' \ -'(-E --dotenv-path)--dotenv-filename=[Search for environment file named instead of `.env`]' \ -'-E+[Load as environment file instead of searching for one]' \ -'--dotenv-path=[Load as environment file instead of searching for one]' \ -'--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.]' \ +'--dump-format=[Dump justfile as ]:FORMAT:(just json)' \ +'--list-heading=[Print before list]:TEXT: ' \ +'--list-prefix=[Print before each list item]:TEXT: ' \ +'-f+[Use as justfile]: :_files' \ +'--justfile=[Use as justfile]: :_files' \ +'*--set=[Override with ]: :(_just_variables)' \ +'--shell=[Invoke to run recipes]: : ' \ +'*--shell-arg=[Invoke shell with as an argument]: : ' \ +'-d+[Use as working directory. --justfile must also be set]: :_files' \ +'--working-directory=[Use as working directory. --justfile must also be set]: :_files' \ +'*-c+[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \ +'*--command=[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \ +'*--completions=[Print shell completion script for ]:SHELL:(bash elvish fish powershell zsh)' \ +'-s+[Show information about ]: :(_just_commands)' \ +'--show=[Show information about ]: :(_just_commands)' \ +'(-E --dotenv-path)--dotenv-filename=[Search for environment file named instead of \`.env\`]: : ' \ +'-E+[Load as environment file instead of searching for one]: :_files' \ +'--dotenv-path=[Load as environment file instead of searching for one]: :_files' \ +'--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.]' \ '--yes[Automatically confirm all recipes.]' \ '(-q --quiet)-n[Print what just would do without doing it]' \ '(-q --quiet)--dry-run[Print what just would do without doing it]' \ '--highlight[Highlight echoed recipe lines in bold]' \ '--no-aliases[Don'\''t show aliases in list]' \ '--no-deps[Don'\''t run recipe dependencies]' \ -'--no-dotenv[Don'\''t load `.env` file]' \ +'--no-dotenv[Don'\''t load \`.env\` file]' \ '--no-highlight[Don'\''t highlight echoed recipe lines in bold]' \ '(-n --dry-run)-q[Suppress all output]' \ '(-n --dry-run)--quiet[Suppress all output]' \ @@ -55,10 +55,10 @@ _just() { '*-v[Use verbose output]' \ '*--verbose[Use verbose output]' \ '--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`]' \ +'--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`]' \ +'-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]' \ '--init[Initialize new justfile in project root]' \ @@ -66,10 +66,10 @@ _just() { '--list[List available recipes and their arguments]' \ '--summary[List names of available recipes]' \ '--variables[List names of variables]' \ -'-h[Print help information]' \ -'--help[Print help information]' \ -'-V[Print version information]' \ -'--version[Print version information]' \ +'-h[Print help]' \ +'--help[Print help]' \ +'-V[Print version]' \ +'--version[Print version]' \ ) _arguments "${_arguments_options[@]}" $common \ @@ -114,6 +114,7 @@ _just() { esac return ret + } (( $+functions[_just_commands] )) || @@ -138,7 +139,8 @@ _just_commands() { } -(( $+functions[_just_variables] )) || +if [ "$funcstack[1]" = "_just" ]; then + (( $+functions[_just_variables] )) || _just_variables() { [[ $PREFIX = -* ]] && return 1 integer ret=1 @@ -158,3 +160,6 @@ _just_variables() { } _just "$@" +else + compdef _just just +fi diff --git a/src/completions.rs b/src/completions.rs index 2964d76..74d0eb5 100644 --- a/src/completions.rs +++ b/src/completions.rs @@ -43,21 +43,19 @@ pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ r" local common=(", ), ( - r"'*--set=[Override with ]' \", - r"'*--set[Override with ]: :_just_variables' \", + r"'*--set=[Override with ]:VARIABLE: :VARIABLE: ' \", + r"'*--set=[Override with ]: :(_just_variables)' \", ), ( - r"'-s+[Show information about ]' \ -'--show=[Show information about ]' \", - r"'-s+[Show information about ]: :_just_commands' \ -'--show=[Show information about ]: :_just_commands' \", + r"'()-s+[Show information about ]:RECIPE: ' \ +'()--show=[Show information about ]:RECIPE: ' \", + r"'-s+[Show information about ]: :(_just_commands)' \ +'--show=[Show information about ]: :(_just_commands)' \", ), ( - "'::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \ - justfile:_files' \\ -&& ret=0 -\x20\x20\x20\x20 -", + "'*::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the \ + justfile:' \\ +&& ret=0", r#") _arguments "${_arguments_options[@]}" $common \ @@ -105,9 +103,7 @@ pub(crate) const ZSH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ "#, ), ( - " local commands; commands=( -\x20\x20\x20\x20\x20\x20\x20\x20 - )", + " local commands; commands=()", r#" [[ $PREFIX = -* ]] && return 1 integer ret=1 local variables; variables=( @@ -206,10 +202,10 @@ pub(crate) const BASH_COMPLETION_REPLACEMENTS: &[(&str, &str)] = &[ fi fi"#, ), - (r" just)", r#" "$1")"#), + (r" just)", r#" "$1")"#), ( - r"local i cur prev opts cmds", - r"local i cur prev words cword opts cmds", + r"local i cur prev opts cmd", + r"local i cur prev words cword opts cmd", ), ( r#" cur="${COMP_WORDS[COMP_CWORD]}" diff --git a/src/config.rs b/src/config.rs index 4772cd9..10a15ca 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,13 +1,14 @@ use { super::*, - clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, ArgSettings}, + clap::{ + builder::{styling::AnsiColor, FalseyValueParser, PossibleValuesParser, Styles}, + value_parser, Arg, ArgAction, ArgGroup, ArgMatches, Command, + }, }; -pub(crate) const CHOOSER_ENVIRONMENT_KEY: &str = "JUST_CHOOSER"; - -pub(crate) const CHOOSE_HELP: &str = "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`"; +const CHOOSE_HELP: &str = "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`"; pub(crate) fn chooser_default(justfile: &Path) -> OsString { let mut chooser = OsString::new(); @@ -76,16 +77,7 @@ mod cmd { ]; pub(crate) const ARGLESS: &[&str] = &[ - CHANGELOG, - COMPLETIONS, - DUMP, - EDIT, - FORMAT, - INIT, - LIST, - SHOW, - SUMMARY, - VARIABLES, + CHANGELOG, DUMP, EDIT, FORMAT, INIT, LIST, SUMMARY, VARIABLES, ]; } @@ -147,270 +139,308 @@ mod arg { } impl Config { - pub(crate) fn app() -> App<'static, 'static> { - let app = App::new(env!("CARGO_PKG_NAME")) - .help_message("Print help information") - .version_message("Print version information") - .setting(AppSettings::ColoredHelp) - .setting(AppSettings::TrailingVarArg) + pub(crate) fn app() -> Command { + let app = Command::new(env!("CARGO_PKG_NAME")) + .bin_name(env!("CARGO_PKG_NAME")) + .trailing_var_arg(true) + .styles( + Styles::styled() + .header(AnsiColor::Yellow.on_default()) + .usage(AnsiColor::Yellow.on_default()) + .literal(AnsiColor::Green.on_default()) + .placeholder(AnsiColor::Green.on_default()) + ) .arg( - Arg::with_name(arg::CHECK) + Arg::new(arg::CHECK) .long("check") + .action(ArgAction::SetTrue) .requires(cmd::FORMAT) .help("Run `--fmt` in 'check' mode. Exits with 0 if justfile is formatted correctly. Exits with 1 and prints a diff if formatting is required."), ) .arg( - Arg::with_name(arg::CHOOSER) + Arg::new(arg::CHOOSER) .long("chooser") - .takes_value(true) + .env("JUST_CHOOSER") + .action(ArgAction::Set) .help("Override binary invoked by `--choose`"), ) .arg( - Arg::with_name(arg::COLOR) + Arg::new(arg::COLOR) .long("color") - .takes_value(true) - .possible_values(arg::COLOR_VALUES) + .action(ArgAction::Set) + .value_parser(PossibleValuesParser::new(arg::COLOR_VALUES)) .default_value(arg::COLOR_AUTO) .help("Print colorful output"), ) .arg( - Arg::with_name(arg::COMMAND_COLOR) + Arg::new(arg::COMMAND_COLOR) .long("command-color") - .takes_value(true) - .possible_values(arg::COMMAND_COLOR_VALUES) + .action(ArgAction::Set) + .value_parser(PossibleValuesParser::new(arg::COMMAND_COLOR_VALUES)) .help("Echo recipe lines in "), ) - .arg(Arg::with_name(arg::YES).long("yes").help("Automatically confirm all recipes.")) + .arg(Arg::new(arg::YES).long("yes").action(ArgAction::SetTrue).help("Automatically confirm all recipes.")) .arg( - Arg::with_name(arg::DRY_RUN) - .short("n") + Arg::new(arg::DRY_RUN) + .short('n') .long("dry-run") + .action(ArgAction::SetTrue) .help("Print what just would do without doing it") .conflicts_with(arg::QUIET), ) .arg( - Arg::with_name(arg::DUMP_FORMAT) + Arg::new(arg::DUMP_FORMAT) .long("dump-format") - .takes_value(true) - .possible_values(arg::DUMP_FORMAT_VALUES) + .action(ArgAction::Set) + .value_parser(PossibleValuesParser::new(arg::DUMP_FORMAT_VALUES)) .default_value(arg::DUMP_FORMAT_JUST) .value_name("FORMAT") .help("Dump justfile as "), ) .arg( - Arg::with_name(arg::HIGHLIGHT) + Arg::new(arg::HIGHLIGHT) .long("highlight") + .action(ArgAction::SetTrue) .help("Highlight echoed recipe lines in bold") .overrides_with(arg::NO_HIGHLIGHT), ) .arg( - Arg::with_name(arg::LIST_HEADING) + Arg::new(arg::LIST_HEADING) .long("list-heading") .help("Print before list") .value_name("TEXT") - .takes_value(true), + .action(ArgAction::Set), ) .arg( - Arg::with_name(arg::LIST_PREFIX) + Arg::new(arg::LIST_PREFIX) .long("list-prefix") .help("Print before each list item") .value_name("TEXT") - .takes_value(true), + .action(ArgAction::Set), ) .arg( - Arg::with_name(arg::NO_ALIASES) + Arg::new(arg::NO_ALIASES) .long("no-aliases") - .help("Don't show aliases in list") + .action(ArgAction::SetTrue) + .help("Don't show aliases in list"), ) .arg ( - Arg::with_name(arg::NO_DEPS) + Arg::new(arg::NO_DEPS) .long("no-deps") .alias("no-dependencies") + .action(ArgAction::SetTrue) .help("Don't run recipe dependencies") ) .arg( - Arg::with_name(arg::NO_DOTENV) + Arg::new(arg::NO_DOTENV) .long("no-dotenv") + .action(ArgAction::SetTrue) .help("Don't load `.env` file"), ) .arg( - Arg::with_name(arg::NO_HIGHLIGHT) + Arg::new(arg::NO_HIGHLIGHT) .long("no-highlight") + .action(ArgAction::SetTrue) .help("Don't highlight echoed recipe lines in bold") .overrides_with(arg::HIGHLIGHT), ) .arg( - Arg::with_name(arg::JUSTFILE) - .short("f") + Arg::new(arg::JUSTFILE) + .short('f') .long("justfile") - .takes_value(true) + .action(ArgAction::Set) + .value_parser(value_parser!(PathBuf)) .help("Use as justfile"), ) .arg( - Arg::with_name(arg::QUIET) - .short("q") + Arg::new(arg::QUIET) + .short('q') .long("quiet") + .action(ArgAction::SetTrue) .help("Suppress all output") .conflicts_with(arg::DRY_RUN), ) .arg( - Arg::with_name(arg::SET) + Arg::new(arg::SET) .long("set") - .takes_value(true) + .action(ArgAction::Append) .number_of_values(2) - .value_names(&["VARIABLE", "VALUE"]) - .multiple(true) + .value_names(["VARIABLE", "VALUE"]) .help("Override with "), ) .arg( - Arg::with_name(arg::SHELL) + Arg::new(arg::SHELL) .long("shell") - .takes_value(true) + .action(ArgAction::Set) .help("Invoke to run recipes"), ) .arg( - Arg::with_name(arg::SHELL_ARG) + Arg::new(arg::SHELL_ARG) .long("shell-arg") - .takes_value(true) - .multiple(true) - .number_of_values(1) + .action(ArgAction::Append) .allow_hyphen_values(true) .overrides_with(arg::CLEAR_SHELL_ARGS) .help("Invoke shell with as an argument"), ) .arg( - Arg::with_name(arg::SHELL_COMMAND) + Arg::new(arg::SHELL_COMMAND) .long("shell-command") .requires(cmd::COMMAND) + .action(ArgAction::SetTrue) .help("Invoke with the shell used to run recipe lines and backticks"), ) .arg( - Arg::with_name(arg::CLEAR_SHELL_ARGS) + Arg::new(arg::CLEAR_SHELL_ARGS) .long("clear-shell-args") + .action(ArgAction::SetTrue) .overrides_with(arg::SHELL_ARG) .help("Clear shell arguments"), ) .arg( - Arg::with_name(arg::UNSORTED) + Arg::new(arg::UNSORTED) .long("unsorted") - .short("u") + .short('u') + .action(ArgAction::SetTrue) .help("Return list and summary entries in source order"), ) .arg( - Arg::with_name(arg::UNSTABLE) + Arg::new(arg::UNSTABLE) .long("unstable") + .env("JUST_UNSTABLE") + .action(ArgAction::SetTrue) + .value_parser(FalseyValueParser::new()) .help("Enable unstable features"), ) .arg( - Arg::with_name(arg::VERBOSE) - .short("v") + Arg::new(arg::VERBOSE) + .short('v') .long("verbose") - .multiple(true) + .action(ArgAction::Count) .help("Use verbose output"), ) .arg( - Arg::with_name(arg::WORKING_DIRECTORY) - .short("d") + Arg::new(arg::WORKING_DIRECTORY) + .short('d') .long("working-directory") - .takes_value(true) + .action(ArgAction::Set) + .value_parser(value_parser!(PathBuf)) .help("Use as working directory. --justfile must also be set") .requires(arg::JUSTFILE), ) .arg( - Arg::with_name(cmd::CHANGELOG) + Arg::new(cmd::CHANGELOG) .long("changelog") + .action(ArgAction::SetTrue) .help("Print changelog"), ) - .arg(Arg::with_name(cmd::CHOOSE).long("choose").help(CHOOSE_HELP)) + .arg(Arg::new(cmd::CHOOSE).long("choose").action(ArgAction::SetTrue).help(CHOOSE_HELP)) .arg( - Arg::with_name(cmd::COMMAND) + Arg::new(cmd::COMMAND) .long("command") - .short("c") - .min_values(1) + .short('c') + .num_args(1..) .allow_hyphen_values(true) + .action(ArgAction::Append) + .value_parser(value_parser!(std::ffi::OsString)) .help( "Run an arbitrary command with the working directory, `.env`, overrides, and exports \ set", ), ) .arg( - Arg::with_name(cmd::COMPLETIONS) + Arg::new(cmd::COMPLETIONS) .long("completions") - .takes_value(true) + .action(ArgAction::Append) + .num_args(1..) .value_name("SHELL") - .possible_values(&clap::Shell::variants()) - .set(ArgSettings::CaseInsensitive) + .value_parser(value_parser!(clap_complete::Shell)) + .ignore_case(true) .help("Print shell completion script for "), ) .arg( - Arg::with_name(cmd::DUMP) + Arg::new(cmd::DUMP) .long("dump") + .action(ArgAction::SetTrue) .help("Print justfile"), ) .arg( - Arg::with_name(cmd::EDIT) - .short("e") + Arg::new(cmd::EDIT) + .short('e') .long("edit") + .action(ArgAction::SetTrue) .help("Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`"), ) - .arg(Arg::with_name(cmd::EVALUATE).long("evaluate").help( - "Evaluate and print all variables. If a variable name is given as an argument, only print \ - that variable's value.", - )) .arg( - Arg::with_name(cmd::FORMAT) + Arg::new(cmd::EVALUATE) + .long("evaluate") + .action(ArgAction::SetTrue) + .help( + "Evaluate and print all variables. If a variable name is given as an argument, only \ + print that variable's value.", + ), + ) + .arg( + Arg::new(cmd::FORMAT) .long("fmt") .alias("format") + .action(ArgAction::SetTrue) .help("Format and overwrite justfile"), ) .arg( - Arg::with_name(cmd::INIT) + Arg::new(cmd::INIT) .long("init") .alias("initialize") + .action(ArgAction::SetTrue) .help("Initialize new justfile in project root"), ) .arg( - Arg::with_name(cmd::LIST) - .short("l") + Arg::new(cmd::LIST) + .short('l') .long("list") + .action(ArgAction::SetTrue) .help("List available recipes and their arguments"), ) .arg( - Arg::with_name(cmd::SHOW) - .short("s") + Arg::new(cmd::SHOW) + .short('s') .long("show") - .takes_value(true) + .action(ArgAction::Set) .value_name("RECIPE") + .conflicts_with(arg::ARGUMENTS) .help("Show information about "), ) .arg( - Arg::with_name(cmd::SUMMARY) + Arg::new(cmd::SUMMARY) .long("summary") + .action(ArgAction::SetTrue) .help("List names of available recipes"), ) .arg( - Arg::with_name(cmd::VARIABLES) + Arg::new(cmd::VARIABLES) .long("variables") + .action(ArgAction::SetTrue) .help("List names of variables"), ) .arg( - Arg::with_name(arg::DOTENV_FILENAME) + Arg::new(arg::DOTENV_FILENAME) .long("dotenv-filename") - .takes_value(true) + .action(ArgAction::Set) .help("Search for environment file named instead of `.env`") .conflicts_with(arg::DOTENV_PATH), ) .arg( - Arg::with_name(arg::DOTENV_PATH) - .short("E") + Arg::new(arg::DOTENV_PATH) + .short('E') .long("dotenv-path") + .action(ArgAction::Set) + .value_parser(value_parser!(PathBuf)) .help("Load as environment file instead of searching for one") - .takes_value(true), ) - .group(ArgGroup::with_name("SUBCOMMAND").args(cmd::ALL)) + .group(ArgGroup::new("SUBCOMMAND").args(cmd::ALL)) .arg( - Arg::with_name(arg::ARGUMENTS) - .multiple(true) + Arg::new(arg::ARGUMENTS) + .num_args(1..) + .action(ArgAction::Append) .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"), ); @@ -434,12 +464,12 @@ impl Config { fn color_from_matches(matches: &ArgMatches) -> ConfigResult { let value = matches - .value_of(arg::COLOR) + .get_one::(arg::COLOR) .ok_or_else(|| ConfigError::Internal { message: "`--color` had no value".to_string(), })?; - match value { + match value.as_str() { arg::COLOR_AUTO => Ok(Color::auto()), arg::COLOR_ALWAYS => Ok(Color::always()), arg::COLOR_NEVER => Ok(Color::never()), @@ -450,8 +480,8 @@ impl Config { } fn command_color_from_matches(matches: &ArgMatches) -> ConfigResult> { - if let Some(value) = matches.value_of(arg::COMMAND_COLOR) { - match value { + if let Some(value) = matches.get_one::(arg::COMMAND_COLOR) { + match value.as_str() { arg::COMMAND_COLOR_BLACK => Ok(Some(ansi_term::Color::Black)), arg::COMMAND_COLOR_BLUE => Ok(Some(ansi_term::Color::Blue)), arg::COMMAND_COLOR_CYAN => Ok(Some(ansi_term::Color::Cyan)), @@ -469,13 +499,14 @@ impl Config { } fn dump_format_from_matches(matches: &ArgMatches) -> ConfigResult { - let value = matches - .value_of(arg::DUMP_FORMAT) - .ok_or_else(|| ConfigError::Internal { - message: "`--dump-format` had no value".to_string(), - })?; + let value = + matches + .get_one::(arg::DUMP_FORMAT) + .ok_or_else(|| ConfigError::Internal { + message: "`--dump-format` had no value".to_string(), + })?; - match value { + match value.as_str() { arg::DUMP_FORMAT_JSON => Ok(DumpFormat::Json), arg::DUMP_FORMAT_JUST => Ok(DumpFormat::Just), _ => Err(ConfigError::Internal { @@ -487,36 +518,37 @@ impl Config { pub(crate) fn from_matches(matches: &ArgMatches) -> ConfigResult { let invocation_directory = env::current_dir().context(config_error::CurrentDirContext)?; - let verbosity = if matches.is_present(arg::QUIET) { + let verbosity = if matches.get_flag(arg::QUIET) { Verbosity::Quiet } else { - Verbosity::from_flag_occurrences(matches.occurrences_of(arg::VERBOSE)) + Verbosity::from_flag_occurrences(matches.get_count(arg::VERBOSE)) }; let color = Self::color_from_matches(matches)?; let command_color = Self::command_color_from_matches(matches)?; - let set_count = matches.occurrences_of(arg::SET); let mut overrides = BTreeMap::new(); - if set_count > 0 { - let mut values = matches.values_of(arg::SET).unwrap(); - for _ in 0..set_count { - overrides.insert( - values.next().unwrap().to_owned(), - values.next().unwrap().to_owned(), - ); + if let Some(mut values) = matches.get_many::(arg::SET) { + while let (Some(k), Some(v)) = (values.next(), values.next()) { + overrides.insert(k.into(), v.into()); } } - let positional = Positional::from_values(matches.values_of(arg::ARGUMENTS)); + let positional = Positional::from_values( + matches + .get_many::(arg::ARGUMENTS) + .map(|s| s.map(String::as_str)), + ); for (name, value) in positional.overrides { overrides.insert(name.clone(), value.clone()); } let search_config = { - let justfile = matches.value_of(arg::JUSTFILE).map(PathBuf::from); - let working_directory = matches.value_of(arg::WORKING_DIRECTORY).map(PathBuf::from); + let justfile = matches.get_one::(arg::JUSTFILE).map(Into::into); + let working_directory = matches + .get_one::(arg::WORKING_DIRECTORY) + .map(Into::into); if let Some(search_directory) = positional.search_directory.map(PathBuf::from) { if justfile.is_some() || working_directory.is_some() { @@ -543,7 +575,7 @@ impl Config { }; for subcommand in cmd::ARGLESS { - if matches.is_present(subcommand) { + if matches.get_flag(subcommand) { match (!overrides.is_empty(), !positional.arguments.is_empty()) { (false, false) => {} (true, false) => { @@ -569,41 +601,37 @@ impl Config { } } - let subcommand = if matches.is_present(cmd::CHANGELOG) { + let subcommand = if matches.get_flag(cmd::CHANGELOG) { Subcommand::Changelog - } else if matches.is_present(cmd::CHOOSE) { + } else if matches.get_flag(cmd::CHOOSE) { Subcommand::Choose { - chooser: matches.value_of(arg::CHOOSER).map(str::to_owned), + chooser: matches.get_one::(arg::CHOOSER).map(Into::into), overrides, } - } else if let Some(values) = matches.values_of_os(cmd::COMMAND) { - let mut arguments = values.map(OsStr::to_owned).collect::>(); + } else if let Some(values) = matches.get_many::(cmd::COMMAND) { + let mut arguments = values.map(Into::into).collect::>(); Subcommand::Command { binary: arguments.remove(0), arguments, overrides, } - } else if let Some(shell) = matches.value_of(cmd::COMPLETIONS) { - Subcommand::Completions { - shell: shell.to_owned(), - } - } else if matches.is_present(cmd::EDIT) { + } else if let Some(&shell) = matches.get_one::(cmd::COMPLETIONS) { + Subcommand::Completions { shell } + } else if matches.get_flag(cmd::EDIT) { Subcommand::Edit - } else if matches.is_present(cmd::SUMMARY) { + } else if matches.get_flag(cmd::SUMMARY) { Subcommand::Summary - } else if matches.is_present(cmd::DUMP) { + } else if matches.get_flag(cmd::DUMP) { Subcommand::Dump - } else if matches.is_present(cmd::FORMAT) { + } else if matches.get_flag(cmd::FORMAT) { Subcommand::Format - } else if matches.is_present(cmd::INIT) { + } else if matches.get_flag(cmd::INIT) { Subcommand::Init - } else if matches.is_present(cmd::LIST) { + } else if matches.get_flag(cmd::LIST) { Subcommand::List - } else if let Some(name) = matches.value_of(cmd::SHOW) { - Subcommand::Show { - name: name.to_owned(), - } - } else if matches.is_present(cmd::EVALUATE) { + } else if let Some(name) = matches.get_one::(cmd::SHOW).map(Into::into) { + Subcommand::Show { name } + } else if matches.get_flag(cmd::EVALUATE) { if positional.arguments.len() > 1 { return Err(ConfigError::SubcommandArguments { subcommand: cmd::EVALUATE, @@ -619,7 +647,7 @@ impl Config { variable: positional.arguments.into_iter().next(), overrides, } - } else if matches.is_present(cmd::VARIABLES) { + } else if matches.get_flag(cmd::VARIABLES) { Subcommand::Variables } else { Subcommand::Run { @@ -628,55 +656,46 @@ impl Config { } }; - let shell_args = if matches.occurrences_of(arg::SHELL_ARG) > 0 - || matches.occurrences_of(arg::CLEAR_SHELL_ARGS) > 0 - { - Some( - matches - .values_of(arg::SHELL_ARG) - .map_or(Vec::new(), |shell_args| { - shell_args.map(str::to_owned).collect() - }), - ) + let shell_args = if matches.get_flag(arg::CLEAR_SHELL_ARGS) { + Some(Vec::new()) } else { - None + matches + .get_many::(arg::SHELL_ARG) + .map(|s| s.map(Into::into).collect()) }; - let unstable = matches.is_present(arg::UNSTABLE) - || env::var_os("JUST_UNSTABLE") - .map(|val| !(val == "false" || val == "0" || val.is_empty())) - .unwrap_or_default(); + let unstable = matches.get_flag(arg::UNSTABLE); Ok(Self { - check: matches.is_present(arg::CHECK), + check: matches.get_flag(arg::CHECK), color, command_color, - dotenv_filename: matches.value_of(arg::DOTENV_FILENAME).map(str::to_owned), - dotenv_path: matches.value_of(arg::DOTENV_PATH).map(PathBuf::from), - dry_run: matches.is_present(arg::DRY_RUN), + dotenv_filename: matches + .get_one::(arg::DOTENV_FILENAME) + .map(Into::into), + dotenv_path: matches.get_one::(arg::DOTENV_PATH).map(Into::into), + dry_run: matches.get_flag(arg::DRY_RUN), dump_format: Self::dump_format_from_matches(matches)?, - highlight: !matches.is_present(arg::NO_HIGHLIGHT), + highlight: !matches.get_flag(arg::NO_HIGHLIGHT), invocation_directory, list_heading: matches - .value_of(arg::LIST_HEADING) - .unwrap_or("Available recipes:\n") - .to_owned(), + .get_one::(arg::LIST_HEADING) + .map_or_else(|| "Available recipes:\n".into(), Into::into), list_prefix: matches - .value_of(arg::LIST_PREFIX) - .unwrap_or(" ") - .to_owned(), - load_dotenv: !matches.is_present(arg::NO_DOTENV), - no_aliases: matches.is_present(arg::NO_ALIASES), - no_dependencies: matches.is_present(arg::NO_DEPS), + .get_one::(arg::LIST_PREFIX) + .map_or_else(|| " ".into(), Into::into), + load_dotenv: !matches.get_flag(arg::NO_DOTENV), + no_aliases: matches.get_flag(arg::NO_ALIASES), + no_dependencies: matches.get_flag(arg::NO_DEPS), search_config, - shell: matches.value_of(arg::SHELL).map(str::to_owned), + shell: matches.get_one::(arg::SHELL).map(Into::into), shell_args, - shell_command: matches.is_present(arg::SHELL_COMMAND), + shell_command: matches.get_flag(arg::SHELL_COMMAND), subcommand, - unsorted: matches.is_present(arg::UNSORTED), + unsorted: matches.get_flag(arg::UNSORTED), unstable, verbosity, - yes: matches.is_present(arg::YES), + yes: matches.get_flag(arg::YES), }) } @@ -701,9 +720,11 @@ impl Config { #[cfg(test)] mod tests { - use super::*; - - use pretty_assertions::assert_eq; + use { + super::*, + clap::error::{ContextKind, ContextValue}, + pretty_assertions::assert_eq, + }; macro_rules! test { { @@ -748,10 +769,11 @@ mod tests { } } + #[track_caller] fn test(arguments: &[&str], want: Config) { let app = Config::app(); let matches = app - .get_matches_from_safe(arguments) + .try_get_matches_from(arguments) .expect("argument parsing failed"); let have = Config::from_matches(&matches).expect("config parsing failed"); assert_eq!(have, want); @@ -771,7 +793,7 @@ mod tests { let app = Config::app(); - app.get_matches_from_safe(arguments).expect_err("Expected clap error"); + app.try_get_matches_from(arguments).expect_err("Expected clap error"); } }; { @@ -789,7 +811,7 @@ mod tests { let app = Config::app(); - let matches = app.get_matches_from_safe(arguments).expect("Matching fails"); + let matches = app.try_get_matches_from(arguments).expect("Matching fails"); match Config::from_matches(&matches).expect_err("config parsing succeeded") { $error => { $($check)? } @@ -799,6 +821,30 @@ mod tests { } } + macro_rules! error_matches { + ( + name: $name:ident, + args: [$($arg:expr),*], + error: $error:pat, + $(check: $check:block,)? + ) => { + #[test] + fn $name() { + let arguments = &[ + "just", + $($arg,)* + ]; + + let app = Config::app(); + + match app.try_get_matches_from(arguments) { + Err($error) => { $($check)? } + other => panic!("Unexpected result from get matches: {other:?}") + } + } + }; + } + macro_rules! map { {} => { BTreeMap::new() @@ -1124,13 +1170,13 @@ mod tests { test! { name: subcommand_completions, args: ["--completions", "bash"], - subcommand: Subcommand::Completions{shell: "bash".to_owned()}, + subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, } test! { name: subcommand_completions_uppercase, args: ["--completions", "BASH"], - subcommand: Subcommand::Completions{shell: "BASH".to_owned()}, + subcommand: Subcommand::Completions{ shell: clap_complete::Shell::Bash }, } error! { @@ -1400,13 +1446,17 @@ mod tests { error: ConfigError::SearchDirConflict, } - error! { + error_matches! { name: completions_arguments, args: ["--completions", "zsh", "foo"], - error: ConfigError::SubcommandArguments { subcommand, arguments }, + error: error, check: { - assert_eq!(subcommand, cmd::COMPLETIONS); - assert_eq!(arguments, &["foo"]); + assert_eq!(error.kind(), clap::error::ErrorKind::InvalidValue); + assert_eq!(error.context().collect::>(), vec![ + (ContextKind::InvalidArg, &ContextValue::String("--completions ...".into())), + (ContextKind::InvalidValue, &ContextValue::String("foo".into())), + (ContextKind::ValidValue, &ContextValue::Strings(["bash".into(), "elvish".into(), "fish".into(), "powershell".into(), "zsh".into()].into())), + ]); }, } @@ -1490,13 +1540,17 @@ mod tests { }, } - error! { + error_matches! { name: show_arguments, args: ["--show", "foo", "bar"], - error: ConfigError::SubcommandArguments { subcommand, arguments }, + error: error, check: { - assert_eq!(subcommand, cmd::SHOW); - assert_eq!(arguments, &["bar"]); + assert_eq!(error.kind(), clap::error::ErrorKind::ArgumentConflict); + assert_eq!(error.context().collect::>(), vec![ + (ContextKind::InvalidArg, &ContextValue::String("--show ".into())), + (ContextKind::PriorArg, &ContextValue::String("[ARGUMENTS]...".into())), + (ContextKind::Usage, &ContextValue::StyledStr("\u{1b}[33mUsage:\u{1b}[0m \u{1b}[32mjust\u{1b}[0m \u{1b}[32m--show\u{1b}[0m\u{1b}[32m \u{1b}[0m\u{1b}[32m\u{1b}[0m \u{1b}[32m[ARGUMENTS]...\u{1b}[0m".into())), + ]); }, } diff --git a/src/error.rs b/src/error.rs index 0a59ca7..9bcf271 100644 --- a/src/error.rs +++ b/src/error.rs @@ -142,10 +142,13 @@ pub(crate) enum Error<'src> { line_number: Option, signal: i32, }, - TmpdirIo { + TempdirIo { recipe: &'src str, io_error: io::Error, }, + TempfileIo { + io_error: io::Error, + }, Unknown { recipe: &'src str, line_number: Option, @@ -403,10 +406,13 @@ impl<'src> ColorDisplay for Error<'src> { write!(f, "Recipe `{recipe}` was terminated by signal {signal}")?; } } - TmpdirIo { recipe, io_error } => { + TempdirIo { recipe, io_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")?; diff --git a/src/function.rs b/src/function.rs index fee4bfc..b7e03e5 100644 --- a/src/function.rs +++ b/src/function.rs @@ -504,7 +504,10 @@ mod tests { fn dir_not_unicode() { use std::os::unix::ffi::OsStrExt; assert_eq!( - dir("foo", || Some(OsStr::from_bytes(b"\xe0\x80\x80").into())).unwrap_err(), + dir("foo", || Some( + std::ffi::OsStr::from_bytes(b"\xe0\x80\x80").into() + )) + .unwrap_err(), "unable to convert foo directory path to string: ���", ); } diff --git a/src/lib.rs b/src/lib.rs index ab5bd4a..13d3af8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -42,10 +42,10 @@ pub(crate) use { cmp, collections::{BTreeMap, BTreeSet, HashMap}, env, - ffi::{OsStr, OsString}, + ffi::OsString, fmt::{self, Debug, Display, Formatter}, fs, - io::{self, Cursor, Write}, + io::{self, Write}, iter::{self, FromIterator}, mem, ops::Deref, diff --git a/src/recipe.rs b/src/recipe.rs index f5850a5..5834d5b 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -336,7 +336,7 @@ impl<'src, D> Recipe<'src, D> { Some(tempdir) => tempdir_builder.tempdir_in(context.search.working_directory.join(tempdir)), None => tempdir_builder.tempdir(), } - .map_err(|error| Error::TmpdirIo { + .map_err(|error| Error::TempdirIo { recipe: self.name(), io_error: error, })?; @@ -344,7 +344,7 @@ impl<'src, D> Recipe<'src, D> { path.push(shebang.script_filename(self.name())); { - let mut f = fs::File::create(&path).map_err(|error| Error::TmpdirIo { + let mut f = fs::File::create(&path).map_err(|error| Error::TempdirIo { recipe: self.name(), io_error: error, })?; @@ -372,14 +372,14 @@ impl<'src, D> Recipe<'src, D> { } f.write_all(text.as_bytes()) - .map_err(|error| Error::TmpdirIo { + .map_err(|error| Error::TempdirIo { recipe: self.name(), io_error: error, })?; } // make script executable - Platform::set_execute_permission(&path).map_err(|error| Error::TmpdirIo { + Platform::set_execute_permission(&path).map_err(|error| Error::TempdirIo { recipe: self.name(), io_error: error, })?; diff --git a/src/subcommand.rs b/src/subcommand.rs index 7cf101e..30417e0 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -1,4 +1,8 @@ -use super::*; +use { + super::*, + std::io::{Read, Seek}, + tempfile::tempfile, +}; const INIT_JUSTFILE: &str = "default:\n echo 'Hello, world!'\n"; @@ -15,7 +19,7 @@ pub(crate) enum Subcommand { overrides: BTreeMap, }, Completions { - shell: String, + shell: clap_complete::Shell, }, Dump, Edit, @@ -50,7 +54,7 @@ impl Subcommand { Self::changelog(); return Ok(()); } - Completions { shell } => return Self::completions(shell), + Completions { shell } => return Self::completions(*shell), Init => return Self::init(config), Run { arguments, @@ -213,10 +217,7 @@ impl Subcommand { return Err(Error::NoChoosableRecipes); } - let chooser = chooser - .map(OsString::from) - .or_else(|| env::var_os(config::CHOOSER_ENVIRONMENT_KEY)) - .unwrap_or_else(|| config::chooser_default(&search.justfile)); + let chooser = chooser.map_or_else(|| config::chooser_default(&search.justfile), From::from); let result = justfile .settings @@ -275,8 +276,8 @@ impl Subcommand { justfile.run(config, search, overrides, &recipes) } - fn completions(shell: &str) -> RunResult<'static, ()> { - use clap::Shell; + fn completions(shell: clap_complete::Shell) -> RunResult<'static, ()> { + use clap_complete::Shell; fn replace(haystack: &mut String, needle: &str, replacement: &str) -> RunResult<'static, ()> { if let Some(index) = haystack.find(needle) { @@ -289,15 +290,28 @@ impl Subcommand { } } - let shell = shell - .parse::() - .expect("Invalid value for clap::Shell"); + let mut script = { + let mut tempfile = tempfile().map_err(|io_error| Error::TempfileIo { io_error })?; - let buffer = Vec::new(); - let mut cursor = Cursor::new(buffer); - Config::app().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut cursor); - let buffer = cursor.into_inner(); - let mut script = String::from_utf8(buffer).expect("Clap completion not UTF-8"); + 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 + }; match shell { Shell::Bash => { @@ -319,7 +333,7 @@ impl Subcommand { replace(&mut script, needle, replacement)?; } } - Shell::Elvish => {} + _ => {} } println!("{}", script.trim()); diff --git a/src/testing.rs b/src/testing.rs index aa14a4a..bf3cbbf 100644 --- a/src/testing.rs +++ b/src/testing.rs @@ -10,7 +10,7 @@ pub(crate) fn config(args: &[&str]) -> Config { let app = Config::app(); - let matches = app.get_matches_from_safe(args).unwrap(); + let matches = app.try_get_matches_from(args).unwrap(); Config::from_matches(&matches).unwrap() } diff --git a/src/verbosity.rs b/src/verbosity.rs index 0461a50..4fc8c0d 100644 --- a/src/verbosity.rs +++ b/src/verbosity.rs @@ -9,7 +9,7 @@ pub(crate) enum Verbosity { } impl Verbosity { - pub(crate) fn from_flag_occurrences(flag_occurrences: u64) -> Self { + pub(crate) fn from_flag_occurrences(flag_occurrences: u8) -> Self { match flag_occurrences { 0 => Taciturn, 1 => Loquacious, diff --git a/tests/command.rs b/tests/command.rs index c1c9473..44bde24 100644 --- a/tests/command.rs +++ b/tests/command.rs @@ -39,17 +39,12 @@ test! { echo XYZ ", args: ("--command"), - stderr: &format!(" - error: The argument '--command ' requires a value but none was supplied + stderr: " + error: a value is required for '--command ...' but none was supplied - USAGE: - just{EXE_SUFFIX} --color --dump-format --shell \ - <--changelog|--choose|--command |--completions |--dump|--edit|\ - --evaluate|--fmt|--init|--list|--show |--summary|--variables> - - For more information try --help - "), - status: EXIT_FAILURE, + For more information, try '--help'. + ", + status: 2, } test! { diff --git a/tests/fmt.rs b/tests/fmt.rs index 3ad46f3..1d70a6f 100644 --- a/tests/fmt.rs +++ b/tests/fmt.rs @@ -15,10 +15,10 @@ test! { name: check_without_fmt, justfile: "", args: ("--check"), - stderr_regex: "error: The following required arguments were not provided: - --fmt + stderr_regex: "error: the following required arguments were not provided: + --fmt (.|\\n)+", - status: EXIT_FAILURE, + status: 2, } test! { diff --git a/tests/test.rs b/tests/test.rs index ccbba92..a200de7 100644 --- a/tests/test.rs +++ b/tests/test.rs @@ -189,6 +189,7 @@ impl Test { } impl Test { + #[track_caller] pub(crate) fn run(self) -> Output { if let Some(justfile) = &self.justfile { let justfile = unindent(justfile);