Repository: casey/just
Pull request #1924 (2024-02-23 00:29:48)

chore(deps): update clap
Steps:

- [x] Move from clap 2 to 3
  - [x] Make it compile
  - [x] Make all tests pass 
- [x] Resolve deprecation warnings
  - [x]  Make it compile
  - [x] Make all tests pass
- [x] Move from clap 3 to 4
  - [x] Make it compile
  - [x] Make all tests pass

I know there is #1865 but I think it does it too quickly to make it possible. I'm following the changelog instructions from clap: https://github.com/clap-rs/clap/blob/master/CHANGELOG.md#migrating, which makes it simpler

poliorcetics: Note: current commit has a deprecation warning already, the fix is planned for the next commit where I'll resolve such warnings
casey: Nice, this is a great approach, because it makes each commit easy to review independently.

The completion scripts are probably going to be a huge problem. The completion scripts are generated by clap, but then we patch them to add features. So if the completion scripts have changed a lot, those patchs might not apply cleaning, and would be hard to resolve.

If going from clap v3 to clap v4 winds up being hard, we can also merge just the move from v2 to v3.
poliorcetics: > The completion scripts are probably going to be a huge problem. The completion scripts are generated by clap, but then we patch them to add features. So if the completion scripts have changed a lot, those patchs might not apply cleaning, and would be hard to resolve.

The issue is not the cleaning, I fixed that easily enough, I just can't get `clap_complete` to output to anything but `stdout`, which is obviously annoying for us. It happens with clap 3 too, so even that upgrade is blocked
casey: Which function generates the completion script, and which version of clap_complete? Looking at the latest clap_complete, it looks like the generate function takes a writer to write to. [clap_complete::generator::generate](https://docs.rs/clap_complete/latest/clap_complete/generator/fn.generate.html).
poliorcetics: Found the issue, I'll fix it and continue this pr
poliorcetics: Okay I think I did everything I was supposed to for a clean clap upgrade (congrats to the clap changelog, it's very well made) so this is ready for review :D

Patch:
diff --git a/Cargo.lock b/Cargo.lock
index b019ce47..4b90853a 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 6253f5bd..cbbb1e1e 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 57f00419..0cd0880c 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  <ARGUMENTS>... "
+        "$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 6ec3874e..86882d50 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 6664bd65..23da1045 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 <COMMAND-COLOR>' -r -f -a "black blue cyan green purple red yellow"
-complete -c just -n "__fish_use_subcommand" -l dump-format -d 'Dump justfile as <FORMAT>' -r -f -a "just json"
-complete -c just -n "__fish_use_subcommand" -l list-heading -d 'Print <TEXT> before list'
-complete -c just -n "__fish_use_subcommand" -l list-prefix -d 'Print <TEXT> before each list item'
-complete -c just -n "__fish_use_subcommand" -s f -l justfile -d 'Use <JUSTFILE> as justfile'
-complete -c just -n "__fish_use_subcommand" -l set -d 'Override <VARIABLE> with <VALUE>'
-complete -c just -n "__fish_use_subcommand" -l shell -d 'Invoke <SHELL> to run recipes'
-complete -c just -n "__fish_use_subcommand" -l shell-arg -d 'Invoke shell with <SHELL-ARG> as an argument'
-complete -c just -n "__fish_use_subcommand" -s d -l working-directory -d 'Use <WORKING-DIRECTORY> 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 <SHELL>' -r -f -a "zsh bash fish powershell elvish"
-complete -c just -n "__fish_use_subcommand" -s s -l show -d 'Show information about <RECIPE>'
-complete -c just -n "__fish_use_subcommand" -l dotenv-filename -d 'Search for environment file named <DOTENV-FILENAME> instead of `.env`'
-complete -c just -n "__fish_use_subcommand" -s E -l dotenv-path -d 'Load <DOTENV-PATH> 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 <COMMAND> 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 <COMMAND-COLOR>' -r -f -a "{black	'',blue	'',cyan	'',green	'',purple	'',red	'',yellow	''}"
+complete -c just -l dump-format -d 'Dump justfile as <FORMAT>' -r -f -a "{just	'',json	''}"
+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 -s f -l justfile -d 'Use <JUSTFILE> as justfile' -r -F
+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 -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	'',elvish	'',fish	'',powershell	'',zsh	''}"
+complete -c just -s s -l show -d 'Show information about <RECIPE>' -r
+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 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 <COMMAND> 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 fbdc529b..f46cea1a 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 <RECIPE>')
             [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
             [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('-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('--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 45f06a78..4550daa7 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 <COMMAND-COLOR>]: :(black blue cyan green purple red yellow)' \
-'--dump-format=[Dump justfile as <FORMAT>]: :(just json)' \
-'--list-heading=[Print <TEXT> before list]' \
-'--list-prefix=[Print <TEXT> before each list item]' \
-'-f+[Use <JUSTFILE> as justfile]' \
-'--justfile=[Use <JUSTFILE> as justfile]' \
-'*--set[Override <VARIABLE> with <VALUE>]: :_just_variables' \
-'--shell=[Invoke <SHELL> to run recipes]' \
-'*--shell-arg=[Invoke shell with <SHELL-ARG> as an argument]' \
-'-d+[Use <WORKING-DIRECTORY> as working directory. --justfile must also be set]' \
-'--working-directory=[Use <WORKING-DIRECTORY> 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 <SHELL>]: :(zsh bash fish powershell elvish)' \
-'-s+[Show information about <RECIPE>]: :_just_commands' \
-'--show=[Show information about <RECIPE>]: :_just_commands' \
-'(-E --dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of `.env`]' \
-'-E+[Load <DOTENV-PATH> as environment file instead of searching for one]' \
-'--dotenv-path=[Load <DOTENV-PATH> 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>]:FORMAT:(just json)' \
+'--list-heading=[Print <TEXT> before list]:TEXT: ' \
+'--list-prefix=[Print <TEXT> before each list item]:TEXT: ' \
+'-f+[Use <JUSTFILE> as justfile]: :_files' \
+'--justfile=[Use <JUSTFILE> as justfile]: :_files' \
+'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \
+'--shell=[Invoke <SHELL> to run recipes]: : ' \
+'*--shell-arg=[Invoke shell with <SHELL-ARG> as an argument]: : ' \
+'-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]: : ' \
+'*--command=[Run an arbitrary command with the working directory, \`.env\`, overrides, and exports set]: : ' \
+'*--completions=[Print shell completion script for <SHELL>]:SHELL:(bash elvish fish powershell zsh)' \
+'-s+[Show information about <RECIPE>]: :(_just_commands)' \
+'--show=[Show information about <RECIPE>]: :(_just_commands)' \
+'(-E --dotenv-path)--dotenv-filename=[Search for environment file named <DOTENV-FILENAME> instead of \`.env\`]: : ' \
+'-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' \
+'--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 2964d76c..74d0eb5d 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 <VARIABLE> with <VALUE>]' \",
-    r"'*--set[Override <VARIABLE> with <VALUE>]: :_just_variables' \",
+    r"'*--set=[Override <VARIABLE> with <VALUE>]:VARIABLE: :VARIABLE: ' \",
+    r"'*--set=[Override <VARIABLE> with <VALUE>]: :(_just_variables)' \",
   ),
   (
-    r"'-s+[Show information about <RECIPE>]' \
-'--show=[Show information about <RECIPE>]' \",
-    r"'-s+[Show information about <RECIPE>]: :_just_commands' \
-'--show=[Show information about <RECIPE>]: :_just_commands' \",
+    r"'()-s+[Show information about <RECIPE>]:RECIPE: ' \
+'()--show=[Show information about <RECIPE>]:RECIPE: ' \",
+    r"'-s+[Show information about <RECIPE>]: :(_just_commands)' \
+'--show=[Show information about <RECIPE>]: :(_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 4772cd93..10a15caa 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 <COMMAND-COLOR>"),
       )
-      .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 <FORMAT>"),
       )
       .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 <TEXT> 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 <TEXT> 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 <JUSTFILE> 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 <VARIABLE> with <VALUE>"),
       )
       .arg(
-        Arg::with_name(arg::SHELL)
+        Arg::new(arg::SHELL)
           .long("shell")
-          .takes_value(true)
+          .action(ArgAction::Set)
           .help("Invoke <SHELL> 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 <SHELL-ARG> 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 <COMMAND> 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 <WORKING-DIRECTORY> 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 <SHELL>"),
       )
       .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 <RECIPE>"),
       )
       .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 <DOTENV-FILENAME> 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 <DOTENV-PATH> 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<Color> {
     let value = matches
-      .value_of(arg::COLOR)
+      .get_one::<String>(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<Option<ansi_term::Color>> {
-    if let Some(value) = matches.value_of(arg::COMMAND_COLOR) {
-      match value {
+    if let Some(value) = matches.get_one::<String>(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<DumpFormat> {
-    let value = matches
-      .value_of(arg::DUMP_FORMAT)
-      .ok_or_else(|| ConfigError::Internal {
-        message: "`--dump-format` had no value".to_string(),
-      })?;
-
-    match value {
+    let value =
+      matches
+        .get_one::<String>(arg::DUMP_FORMAT)
+        .ok_or_else(|| ConfigError::Internal {
+          message: "`--dump-format` had no value".to_string(),
+        })?;
+
+    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<Self> {
     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::<String>(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::<String>(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::<PathBuf>(arg::JUSTFILE).map(Into::into);
+      let working_directory = matches
+        .get_one::<PathBuf>(arg::WORKING_DIRECTORY)
+        .map(Into::into);
 
       if let Some(search_directory) = positional.search_directory.map(PathBuf::from) {
         if justfile.is_some() || working_directory.is_some() {
@@ -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::<String>(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::<Vec<OsString>>();
+    } else if let Some(values) = matches.get_many::<OsString>(cmd::COMMAND) {
+      let mut arguments = values.map(Into::into).collect::<Vec<OsString>>();
       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::<clap_complete::Shell>(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::<String>(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::<String>(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::<String>(arg::DOTENV_FILENAME)
+        .map(Into::into),
+      dotenv_path: matches.get_one::<PathBuf>(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::<String>(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::<String>(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::<String>(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<_>>(), vec![
+        (ContextKind::InvalidArg, &ContextValue::String("--completions <SHELL>...".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<_>>(), vec![
+        (ContextKind::InvalidArg, &ContextValue::String("--show <RECIPE>".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<RECIPE>\u{1b}[0m \u{1b}[32m[ARGUMENTS]...\u{1b}[0m".into())),
+      ]);
     },
   }
 
diff --git a/src/error.rs b/src/error.rs
index 0a59ca7d..9bcf2719 100644
--- a/src/error.rs
+++ b/src/error.rs
@@ -142,10 +142,13 @@ pub(crate) enum Error<'src> {
     line_number: Option<usize>,
     signal: i32,
   },
-  TmpdirIo {
+  TempdirIo {
     recipe: &'src str,
     io_error: io::Error,
   },
+  TempfileIo {
+    io_error: io::Error,
+  },
   Unknown {
     recipe: &'src str,
     line_number: Option<usize>,
@@ -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 fee4bfc5..b7e03e55 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 ab5bd4ad..13d3af8e 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 f5850a50..5834d5b3 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 7cf101e5..30417e02 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<String, String>,
   },
   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::<Shell>()
-      .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/verbosity.rs b/src/verbosity.rs
index 0461a505..4fc8c0df 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,


Test patch:
diff --git a/src/testing.rs b/src/testing.rs
index aa14a4ac..bf3cbbf5 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/tests/command.rs b/tests/command.rs
index c1c9473e..44bde24f 100644
--- a/tests/command.rs
+++ b/tests/command.rs
@@ -39,17 +39,12 @@ test! {
       echo XYZ
   ",
   args: ("--command"),
-  stderr: &format!("
-    error: The argument '--command <COMMAND>' requires a value but none was supplied
+  stderr: "
+    error: a value is required for '--command <COMMAND>...' but none was supplied
 
-    USAGE:
-        just{EXE_SUFFIX} --color <COLOR> --dump-format <FORMAT> --shell <SHELL> \
-        <--changelog|--choose|--command <COMMAND>|--completions <SHELL>|--dump|--edit|\
-        --evaluate|--fmt|--init|--list|--show <RECIPE>|--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 3ad46f38..1d70a6f9 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 ccbba928..a200de7c 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);