Generate shell completion scripts with --completions (#572)

Make just print clap-generated shell completion scripts with `--completions`
command. Currently, Bash, Zsh, Fish, PowerShell, and Elvish are supported.

Additionally, the generated completion scripts are checked in to the
`completions` folder.
This commit is contained in:
Casey Rodarmor 2020-01-15 01:20:38 -08:00 committed by GitHub
parent e14447030a
commit 85e8015702
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 372 additions and 4 deletions

View File

@ -878,6 +878,16 @@ Tools that pair nicely with `just` include:
For lightning-fast command running, put `alias j=just` in your shell's configuration file. For lightning-fast command running, put `alias j=just` in your shell's configuration file.
=== Shell Completion Scripts
Shell completion scripts for Bash, Zsh, Fish, PowerShell, and Elvish are available in the link:completions[] directory. Please refer to your shell's documentation for how to install them.
The `just` binary can also generate the same completion scripts at runtime, using the `--completions` command:
```sh
$ just --completions zsh > just.zsh
```
=== Syntax Highlighting === Syntax Highlighting
`justfile` syntax is close enough to `make` that you may want to tell your editor to use make syntax highlighting for just. `justfile` syntax is close enough to `make` that you may want to tell your editor to use make syntax highlighting for just.

85
completions/just.bash Normal file
View File

@ -0,0 +1,85 @@
_just() {
local i cur prev opts cmds
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD-1]}"
cmd=""
opts=""
for i in ${COMP_WORDS[@]}
do
case "${i}" in
just)
cmd="just"
;;
*)
;;
esac
done
case "${cmd}" in
just)
opts=" -q -v -e -l -h -V -f -d -s --dry-run --highlight --no-highlight --quiet --clear-shell-args --verbose --dump --edit --evaluate --init --list --summary --help --version --color --justfile --set --shell --shell-arg --working-directory --completions --show <ARGUMENTS>... "
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--color)
COMPREPLY=($(compgen -W "auto always never" -- "${cur}"))
return 0
;;
--justfile)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--set)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--shell-arg)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--working-directory)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-d)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
--completions)
COMPREPLY=($(compgen -W "zsh bash fish powershell elvish" -- "${cur}"))
return 0
;;
--show)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
-s)
COMPREPLY=($(compgen -f "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
}
complete -F _just -o bashdefault -o default just

52
completions/just.elvish Normal file
View File

@ -0,0 +1,52 @@
edit:completion:arg-completer[just] = [@words]{
fn spaces [n]{
repeat $n ' ' | joins ''
}
fn cand [text desc]{
edit:complex-candidate $text &display-suffix=' '(spaces (- 14 (wcswidth $text)))$desc
}
command = 'just'
for word $words[1:-1] {
if (has-prefix $word '-') {
break
}
command = $command';'$word
}
completions = [
&'just'= {
cand --color 'Print colorful output'
cand -f 'Use <JUSTFILE> as justfile.'
cand --justfile 'Use <JUSTFILE> as justfile.'
cand --set 'Override <VARIABLE> with <VALUE>'
cand --shell 'Invoke <SHELL> to run recipes'
cand --shell-arg 'Invoke shell with <SHELL-ARG> as an argument'
cand -d 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
cand --working-directory 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set'
cand --completions 'Print shell completion script for <SHELL>'
cand -s 'Show information about <RECIPE>'
cand --show 'Show information about <RECIPE>'
cand --dry-run 'Print what just would do without doing it'
cand --highlight 'Highlight echoed recipe lines in bold'
cand --no-highlight 'Don''t highlight echoed recipe lines in bold'
cand -q 'Suppress all output'
cand --quiet 'Suppress all output'
cand --clear-shell-args 'Clear shell arguments'
cand -v 'Use verbose output'
cand --verbose 'Use verbose output'
cand --dump 'Print entire justfile'
cand -e 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --edit 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`'
cand --evaluate 'Print evaluated variables'
cand --init 'Initialize new justfile in project root'
cand -l 'List available recipes and their arguments'
cand --list 'List available recipes and their arguments'
cand --summary 'List names of available recipes'
cand -h 'Print help information'
cand --help 'Print help information'
cand -V 'Print version information'
cand --version 'Print version information'
}
]
$completions[$command]
}

22
completions/just.fish Normal file
View File

@ -0,0 +1,22 @@
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" -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" -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 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-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 clear-shell-args -d 'Clear shell arguments'
complete -c just -n "__fish_use_subcommand" -s v -l verbose -d 'Use verbose output'
complete -c just -n "__fish_use_subcommand" -l dump -d 'Print entire 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 'Print evaluated variables'
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" -s h -l help -d 'Print help information'
complete -c just -n "__fish_use_subcommand" -s V -l version -d 'Print version information'

View File

@ -0,0 +1,60 @@
using namespace System.Management.Automation
using namespace System.Management.Automation.Language
Register-ArgumentCompleter -Native -CommandName 'just' -ScriptBlock {
param($wordToComplete, $commandAst, $cursorPosition)
$commandElements = $commandAst.CommandElements
$command = @(
'just'
for ($i = 1; $i -lt $commandElements.Count; $i++) {
$element = $commandElements[$i]
if ($element -isnot [StringConstantExpressionAst] -or
$element.StringConstantType -ne [StringConstantType]::BareWord -or
$element.Value.StartsWith('-')) {
break
}
$element.Value
}) -join ';'
$completions = @(switch ($command) {
'just' {
[CompletionResult]::new('--color', 'color', [CompletionResultType]::ParameterName, 'Print colorful output')
[CompletionResult]::new('-f', 'f', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile.')
[CompletionResult]::new('--justfile', 'justfile', [CompletionResultType]::ParameterName, 'Use <JUSTFILE> as justfile.')
[CompletionResult]::new('--set', 'set', [CompletionResultType]::ParameterName, 'Override <VARIABLE> with <VALUE>')
[CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Invoke <SHELL> to run recipes')
[CompletionResult]::new('--shell-arg', 'shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with <SHELL-ARG> as an argument')
[CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
[CompletionResult]::new('--working-directory', 'working-directory', [CompletionResultType]::ParameterName, 'Use <WORKING-DIRECTORY> as working directory. --justfile must also be set')
[CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for <SHELL>')
[CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
[CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about <RECIPE>')
[CompletionResult]::new('--dry-run', 'dry-run', [CompletionResultType]::ParameterName, 'Print what just would do without doing it')
[CompletionResult]::new('--highlight', 'highlight', [CompletionResultType]::ParameterName, 'Highlight echoed recipe lines in bold')
[CompletionResult]::new('--no-highlight', 'no-highlight', [CompletionResultType]::ParameterName, 'Don''t highlight echoed recipe lines in bold')
[CompletionResult]::new('-q', 'q', [CompletionResultType]::ParameterName, 'Suppress all output')
[CompletionResult]::new('--quiet', 'quiet', [CompletionResultType]::ParameterName, 'Suppress all output')
[CompletionResult]::new('--clear-shell-args', 'clear-shell-args', [CompletionResultType]::ParameterName, 'Clear shell arguments')
[CompletionResult]::new('-v', 'v', [CompletionResultType]::ParameterName, 'Use verbose output')
[CompletionResult]::new('--verbose', 'verbose', [CompletionResultType]::ParameterName, 'Use verbose output')
[CompletionResult]::new('--dump', 'dump', [CompletionResultType]::ParameterName, 'Print entire justfile')
[CompletionResult]::new('-e', 'e', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--edit', 'edit', [CompletionResultType]::ParameterName, 'Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`')
[CompletionResult]::new('--evaluate', 'evaluate', [CompletionResultType]::ParameterName, 'Print evaluated variables')
[CompletionResult]::new('--init', 'init', [CompletionResultType]::ParameterName, 'Initialize new justfile in project root')
[CompletionResult]::new('-l', 'l', [CompletionResultType]::ParameterName, 'List available recipes and their arguments')
[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('-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')
break
}
})
$completions.Where{ $_.CompletionText -like "$wordToComplete*" } |
Sort-Object -Property ListItemText
}

62
completions/just.zsh Normal file
View File

@ -0,0 +1,62 @@
#compdef just
autoload -U is-at-least
_just() {
typeset -A opt_args
typeset -a _arguments_options
local ret=1
if is-at-least 5.2; then
_arguments_options=(-s -S -C)
else
_arguments_options=(-s -C)
fi
local context curcontext="$curcontext" state line
_arguments "${_arguments_options[@]}" \
'--color=[Print colorful output]: :(auto always never)' \
'-f+[Use <JUSTFILE> as justfile.]' \
'--justfile=[Use <JUSTFILE> as justfile.]' \
'*--set=[Override <VARIABLE> with <VALUE>]' \
'--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]' \
'--completions=[Print shell completion script for <SHELL>]: :(zsh bash fish powershell elvish)' \
'-s+[Show information about <RECIPE>]' \
'--show=[Show information about <RECIPE>]' \
'(-q --quiet)--dry-run[Print what just would do without doing it]' \
'--highlight[Highlight echoed recipe lines in bold]' \
'--no-highlight[Don'\''t highlight echoed recipe lines in bold]' \
'(--dry-run)-q[Suppress all output]' \
'(--dry-run)--quiet[Suppress all output]' \
'--clear-shell-args[Clear shell arguments]' \
'*-v[Use verbose output]' \
'*--verbose[Use verbose output]' \
'--dump[Print entire justfile]' \
'-e[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
'--edit[Edit justfile with editor given by $VISUAL or $EDITOR, falling back to `vim`]' \
'--evaluate[Print evaluated variables]' \
'--init[Initialize new justfile in project root]' \
'-l[List available recipes and their arguments]' \
'--list[List available recipes and their arguments]' \
'--summary[List names of available recipes]' \
'-h[Print help information]' \
'--help[Print help information]' \
'-V[Print version information]' \
'--version[Print version information]' \
'::ARGUMENTS -- Overrides and recipe(s) to run, defaulting to the first recipe in the justfile:_files' \
&& ret=0
}
(( $+functions[_just_commands] )) ||
_just_commands() {
local commands; commands=(
)
_describe -t commands 'just commands' commands "$@"
}
_just "$@"

View File

@ -1,6 +1,6 @@
use crate::common::*; use crate::common::*;
use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches}; use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, ArgSettings};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
pub(crate) const DEFAULT_SHELL: &str = "sh"; pub(crate) const DEFAULT_SHELL: &str = "sh";
@ -30,9 +30,10 @@ mod cmd {
pub(crate) const LIST: &str = "LIST"; pub(crate) const LIST: &str = "LIST";
pub(crate) const SHOW: &str = "SHOW"; pub(crate) const SHOW: &str = "SHOW";
pub(crate) const SUMMARY: &str = "SUMMARY"; pub(crate) const SUMMARY: &str = "SUMMARY";
pub(crate) const COMPLETIONS: &str = "COMPLETIONS";
pub(crate) const ALL: &[&str] = &[DUMP, EDIT, INIT, EVALUATE, LIST, SHOW, SUMMARY]; pub(crate) const ALL: &[&str] = &[COMPLETIONS, DUMP, EDIT, INIT, EVALUATE, LIST, SHOW, SUMMARY];
pub(crate) const ARGLESS: &[&str] = &[DUMP, EDIT, INIT, LIST, SHOW, SUMMARY]; pub(crate) const ARGLESS: &[&str] = &[COMPLETIONS, DUMP, EDIT, INIT, LIST, SHOW, SUMMARY];
} }
mod arg { mod arg {
@ -156,6 +157,15 @@ impl Config {
.multiple(true) .multiple(true)
.help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"), .help("Overrides and recipe(s) to run, defaulting to the first recipe in the justfile"),
) )
.arg(
Arg::with_name(cmd::COMPLETIONS)
.long("completions")
.takes_value(true)
.value_name("SHELL")
.possible_values(&clap::Shell::variants())
.set(ArgSettings::CaseInsensitive)
.help("Print shell completion script for <SHELL>"),
)
.arg( .arg(
Arg::with_name(cmd::DUMP) Arg::with_name(cmd::DUMP)
.long("dump") .long("dump")
@ -311,7 +321,11 @@ impl Config {
} }
} }
let subcommand = if matches.is_present(cmd::EDIT) { let subcommand = if let Some(shell) = matches.value_of(cmd::COMPLETIONS) {
Subcommand::Completions {
shell: shell.to_owned(),
}
} else if matches.is_present(cmd::EDIT) {
Subcommand::Edit Subcommand::Edit
} else if matches.is_present(cmd::SUMMARY) { } else if matches.is_present(cmd::SUMMARY) {
Subcommand::Summary Subcommand::Summary
@ -402,6 +416,7 @@ impl Config {
match &self.subcommand { match &self.subcommand {
Dump => self.dump(justfile), Dump => self.dump(justfile),
Completions { shell } => Self::completions(&shell),
Evaluate { overrides } => self.run(justfile, &search, overrides, &Vec::new()), Evaluate { overrides } => self.run(justfile, &search, overrides, &Vec::new()),
Run { Run {
arguments, arguments,
@ -414,6 +429,16 @@ impl Config {
} }
} }
fn completions(shell: &str) -> Result<(), i32> {
let shell = shell
.parse::<clap::Shell>()
.expect("Invalid value for clap::Shell");
Self::app().gen_completions_to(env!("CARGO_PKG_NAME"), shell, &mut io::stdout());
Ok(())
}
fn dump(&self, justfile: Justfile) -> Result<(), i32> { fn dump(&self, justfile: Justfile) -> Result<(), i32> {
println!("{}", justfile); println!("{}", justfile);
Ok(()) Ok(())
@ -648,6 +673,9 @@ OPTIONS:
--color <COLOR> --color <COLOR>
Print colorful output [default: auto] [possible values: auto, always, never] Print colorful output [default: auto] [possible values: auto, always, never]
--completions <SHELL>
Print shell completion script for <SHELL> [possible values: zsh, bash, fish, powershell, elvish]
-f, --justfile <JUSTFILE> Use <JUSTFILE> as justfile. -f, --justfile <JUSTFILE> Use <JUSTFILE> as justfile.
--set <VARIABLE> <VALUE> Override <VARIABLE> with <VALUE> --set <VARIABLE> <VALUE> Override <VARIABLE> with <VALUE>
--shell <SHELL> Invoke <SHELL> to run recipes [default: sh] --shell <SHELL> Invoke <SHELL> to run recipes [default: sh]
@ -988,6 +1016,23 @@ ARGS:
}, },
} }
test! {
name: subcommand_completions,
args: ["--completions", "bash"],
subcommand: Subcommand::Completions{shell: "bash".to_owned()},
}
test! {
name: subcommand_completions_uppercase,
args: ["--completions", "BASH"],
subcommand: Subcommand::Completions{shell: "BASH".to_owned()},
}
error! {
name: subcommand_completions_invalid,
args: ["--completions", "monstersh"],
}
test! { test! {
name: subcommand_dump, name: subcommand_dump,
args: ["--dump"], args: ["--dump"],
@ -1236,6 +1281,16 @@ ARGS:
error: ConfigError::SearchDirConflict, error: ConfigError::SearchDirConflict,
} }
error! {
name: completions_arguments,
args: ["--completions", "zsh", "foo"],
error: ConfigError::SubcommandArguments { subcommand, arguments },
check: {
assert_eq!(subcommand, "--completions");
assert_eq!(arguments, &["foo"]);
},
}
error! { error! {
name: list_arguments, name: list_arguments,
args: ["--list", "bar"], args: ["--list", "bar"],

View File

@ -130,6 +130,7 @@ impl Search {
} }
} }
} }
if candidates.len() == 1 { if candidates.len() == 1 {
return Ok(candidates.pop().unwrap()); return Ok(candidates.pop().unwrap());
} else if candidates.len() > 1 { } else if candidates.len() > 1 {

View File

@ -2,6 +2,9 @@ use crate::common::*;
#[derive(PartialEq, Clone, Debug)] #[derive(PartialEq, Clone, Debug)]
pub(crate) enum Subcommand { pub(crate) enum Subcommand {
Completions {
shell: String,
},
Dump, Dump,
Edit, Edit,
Evaluate { Evaluate {

18
tests/completions.rs Normal file
View File

@ -0,0 +1,18 @@
use std::process::Command;
use executable_path::executable_path;
#[test]
fn output() {
let output = Command::new(executable_path("just"))
.arg("--completions")
.arg("bash")
.output()
.unwrap();
assert!(output.status.success());
let text = String::from_utf8_lossy(&output.stdout);
assert!(text.starts_with("_just() {"));
}