From 85e80157028f6220f46f75a62a87590301710b08 Mon Sep 17 00:00:00 2001 From: Casey Rodarmor Date: Wed, 15 Jan 2020 01:20:38 -0800 Subject: [PATCH] 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. --- README.adoc | 10 +++++ completions/just.bash | 85 +++++++++++++++++++++++++++++++++++++ completions/just.elvish | 52 +++++++++++++++++++++++ completions/just.fish | 22 ++++++++++ completions/just.powershell | 60 ++++++++++++++++++++++++++ completions/just.zsh | 62 +++++++++++++++++++++++++++ src/config.rs | 63 +++++++++++++++++++++++++-- src/search.rs | 1 + src/subcommand.rs | 3 ++ tests/completions.rs | 18 ++++++++ 10 files changed, 372 insertions(+), 4 deletions(-) create mode 100644 completions/just.bash create mode 100644 completions/just.elvish create mode 100644 completions/just.fish create mode 100644 completions/just.powershell create mode 100644 completions/just.zsh create mode 100644 tests/completions.rs diff --git a/README.adoc b/README.adoc index 1ccb63e..11cce04 100644 --- a/README.adoc +++ b/README.adoc @@ -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. +=== 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 `justfile` syntax is close enough to `make` that you may want to tell your editor to use make syntax highlighting for just. diff --git a/completions/just.bash b/completions/just.bash new file mode 100644 index 0000000..578eb8b --- /dev/null +++ b/completions/just.bash @@ -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 ... " + 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 diff --git a/completions/just.elvish b/completions/just.elvish new file mode 100644 index 0000000..68089cf --- /dev/null +++ b/completions/just.elvish @@ -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 as justfile.' + cand --justfile 'Use as justfile.' + cand --set 'Override with ' + cand --shell 'Invoke to run recipes' + cand --shell-arg 'Invoke shell with as an argument' + cand -d 'Use as working directory. --justfile must also be set' + cand --working-directory 'Use as working directory. --justfile must also be set' + cand --completions 'Print shell completion script for ' + cand -s 'Show information about ' + cand --show 'Show information about ' + 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] +} diff --git a/completions/just.fish b/completions/just.fish new file mode 100644 index 0000000..67548ce --- /dev/null +++ b/completions/just.fish @@ -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 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" -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 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' diff --git a/completions/just.powershell b/completions/just.powershell new file mode 100644 index 0000000..92f9f8c --- /dev/null +++ b/completions/just.powershell @@ -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 as justfile.') + [CompletionResult]::new('--justfile', 'justfile', [CompletionResultType]::ParameterName, 'Use as justfile.') + [CompletionResult]::new('--set', 'set', [CompletionResultType]::ParameterName, 'Override with ') + [CompletionResult]::new('--shell', 'shell', [CompletionResultType]::ParameterName, 'Invoke to run recipes') + [CompletionResult]::new('--shell-arg', 'shell-arg', [CompletionResultType]::ParameterName, 'Invoke shell with as an argument') + [CompletionResult]::new('-d', 'd', [CompletionResultType]::ParameterName, 'Use as working directory. --justfile must also be set') + [CompletionResult]::new('--working-directory', 'working-directory', [CompletionResultType]::ParameterName, 'Use as working directory. --justfile must also be set') + [CompletionResult]::new('--completions', 'completions', [CompletionResultType]::ParameterName, 'Print shell completion script for ') + [CompletionResult]::new('-s', 's', [CompletionResultType]::ParameterName, 'Show information about ') + [CompletionResult]::new('--show', 'show', [CompletionResultType]::ParameterName, 'Show information about ') + [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 +} diff --git a/completions/just.zsh b/completions/just.zsh new file mode 100644 index 0000000..7ed88b9 --- /dev/null +++ b/completions/just.zsh @@ -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 as justfile.]' \ +'--justfile=[Use as justfile.]' \ +'*--set=[Override with ]' \ +'--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]' \ +'--completions=[Print shell completion script for ]: :(zsh bash fish powershell elvish)' \ +'-s+[Show information about ]' \ +'--show=[Show information about ]' \ +'(-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 "$@" \ No newline at end of file diff --git a/src/config.rs b/src/config.rs index 3510e4a..0803bea 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1,6 +1,6 @@ use crate::common::*; -use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches}; +use clap::{App, AppSettings, Arg, ArgGroup, ArgMatches, ArgSettings}; use unicode_width::UnicodeWidthStr; pub(crate) const DEFAULT_SHELL: &str = "sh"; @@ -30,9 +30,10 @@ mod cmd { pub(crate) const LIST: &str = "LIST"; pub(crate) const SHOW: &str = "SHOW"; 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 ARGLESS: &[&str] = &[DUMP, EDIT, INIT, LIST, SHOW, SUMMARY]; + pub(crate) const ALL: &[&str] = &[COMPLETIONS, DUMP, EDIT, INIT, EVALUATE, LIST, SHOW, SUMMARY]; + pub(crate) const ARGLESS: &[&str] = &[COMPLETIONS, DUMP, EDIT, INIT, LIST, SHOW, SUMMARY]; } mod arg { @@ -156,6 +157,15 @@ impl Config { .multiple(true) .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 "), + ) .arg( Arg::with_name(cmd::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 } else if matches.is_present(cmd::SUMMARY) { Subcommand::Summary @@ -402,6 +416,7 @@ impl Config { match &self.subcommand { Dump => self.dump(justfile), + Completions { shell } => Self::completions(&shell), Evaluate { overrides } => self.run(justfile, &search, overrides, &Vec::new()), Run { arguments, @@ -414,6 +429,16 @@ impl Config { } } + fn completions(shell: &str) -> Result<(), i32> { + let shell = shell + .parse::() + .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> { println!("{}", justfile); Ok(()) @@ -648,6 +673,9 @@ OPTIONS: --color Print colorful output [default: auto] [possible values: auto, always, never] + --completions + Print shell completion script for [possible values: zsh, bash, fish, powershell, elvish] + -f, --justfile Use as justfile. --set Override with --shell Invoke 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! { name: subcommand_dump, args: ["--dump"], @@ -1236,6 +1281,16 @@ ARGS: 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! { name: list_arguments, args: ["--list", "bar"], diff --git a/src/search.rs b/src/search.rs index 1625cab..70c7e6b 100644 --- a/src/search.rs +++ b/src/search.rs @@ -130,6 +130,7 @@ impl Search { } } } + if candidates.len() == 1 { return Ok(candidates.pop().unwrap()); } else if candidates.len() > 1 { diff --git a/src/subcommand.rs b/src/subcommand.rs index 9147cce..664842e 100644 --- a/src/subcommand.rs +++ b/src/subcommand.rs @@ -2,6 +2,9 @@ use crate::common::*; #[derive(PartialEq, Clone, Debug)] pub(crate) enum Subcommand { + Completions { + shell: String, + }, Dump, Edit, Evaluate { diff --git a/tests/completions.rs b/tests/completions.rs new file mode 100644 index 0000000..0121e1d --- /dev/null +++ b/tests/completions.rs @@ -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() {")); +}