scog aims to generate shell completions for bash/zsh/fish, in a language-agnostic manner.
- Create a spec file:
Suppose that the YAML above defines the interface for the myapp binary (more details on how this should look like later):
name: myapp
about: A fictional CLI tool for managing projects
args:
- name: verbose
short: v
long: verbose
help: Enable verbose output
takes_value: false
[...]- Generate completions:
scog myapp.yamlYou should see some helpful output such as:
- Verify and install
Validate if the generated completions look good, then give them a try!
The YAML spec defines your CLI's interface. See the Example for a complete sample.
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Name of your binary |
about |
string | No | Short description of what it does |
version |
string | No | Version string |
args |
array | No | List of arguments/options |
subcommands |
array | No | List of subcommands |
| Field | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Argument identifier |
short |
char | no | Short flag (e.g., v for -v) |
long |
string | no | Long flag (e.g., verbose for --verbose) |
help |
string | no | Help text for the argument |
required |
bool | no | Whether the argument is required (default: false) |
takes_value |
bool | no | Whether argument takes a value. false makes it a boolean flag (default: true) |
value_hint |
string | no | Completion hint (see this for reference) |
possible_values |
array | no | List of allowed values for constrained options |
Subcommands follow the same structure as the top-level command:
subcommands:
- name: init
about: Initialize something
args:
- name: force
short: f
long: force
help: Force initialization
takes_value: falseSubcommands can be nested in other subcommands.
cargo install scogOr build from source:
git clone https://github.com/vrmiguel/scog
cd scog
cargo build --release# Build completions for all currently supported shells
scog spec.yaml
# Build completions only for bash
scog bash spec.yaml
# Build completions only for bash and zsh
scog bash zsh spec.yamlscog is merely some glue to use clap's own completion generation, but for utilities that are not built using Clap, or even Rust itself. Your spec is parsed and converted into a clap::Command structure at runtime, then clap's proven completion generators produce completions.
Given the following spec:
name: myapp
about: A fictional CLI tool for managing projects
version: 1.0.0
args:
- name: config
short: c
long: config
help: Path to configuration file
required: false
value_hint: FilePath
- name: verbose
short: v
long: verbose
help: Enable verbose output
required: false
takes_value: false # This is a flag, not an option
- name: output
short: o
long: output
help: Output directory
required: false
value_hint: DirPath
- name: format
short: f
long: format
help: Output format
required: false
possible_values:
- json
- yaml
- toml
subcommands:
- name: init
about: Initialize a new project
args:
- name: name
help: Project name
required: true
- name: template
long: template
help: Project template to use
possible_values:
- basic
- advanced
- minimal
- name: build
about: Build the project
args:
- name: release
long: release
help: Build in release mode
takes_value: false
- name: target
long: target
help: Build target
value_hint: OtherHere's what's generated for each shell:
Click to expand/collapse
# Print an optspec for argparse to handle cmd's options that are independent of any subcommand.
function __fish_myapp_global_optspecs
string join \n c/config= v/verbose o/output= f/format= h/help
end
function __fish_myapp_needs_command
# Figure out if the current invocation already has a command.
set -l cmd (commandline -opc)
set -e cmd[1]
argparse -s (__fish_myapp_global_optspecs) -- $cmd 2>/dev/null
or return
if set -q argv[1]
# Also print the command, so this can be used to figure out what it is.
echo $argv[1]
return 1
end
return 0
end
function __fish_myapp_using_subcommand
set -l cmd (__fish_myapp_needs_command)
test -z "$cmd"
and return 1
contains -- $cmd[1] $argv
end
complete -c myapp -n "__fish_myapp_needs_command" -s c -l config -d 'Path to configuration file' -r -F
complete -c myapp -n "__fish_myapp_needs_command" -s o -l output -d 'Output directory' -r -f -a "(__fish_complete_directories)"
complete -c myapp -n "__fish_myapp_needs_command" -s f -l format -d 'Output format' -r -f -a "json\t''
yaml\t''
toml\t''"
complete -c myapp -n "__fish_myapp_needs_command" -s v -l verbose -d 'Enable verbose output'
complete -c myapp -n "__fish_myapp_needs_command" -s h -l help -d 'Print help'
complete -c myapp -n "__fish_myapp_needs_command" -f -a "init" -d 'Initialize a new project'
complete -c myapp -n "__fish_myapp_needs_command" -f -a "build" -d 'Build the project'
complete -c myapp -n "__fish_myapp_needs_command" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'
complete -c myapp -n "__fish_myapp_using_subcommand init" -l template -d 'Project template to use' -r -f -a "basic\t''
advanced\t''
minimal\t''"
complete -c myapp -n "__fish_myapp_using_subcommand init" -s h -l help -d 'Print help'
complete -c myapp -n "__fish_myapp_using_subcommand build" -l target -d 'Build target' -r -f
complete -c myapp -n "__fish_myapp_using_subcommand build" -l release -d 'Build in release mode'
complete -c myapp -n "__fish_myapp_using_subcommand build" -s h -l help -d 'Print help'
complete -c myapp -n "__fish_myapp_using_subcommand help; and not __fish_seen_subcommand_from init build help" -f -a "init" -d 'Initialize a new project'
complete -c myapp -n "__fish_myapp_using_subcommand help; and not __fish_seen_subcommand_from init build help" -f -a "build" -d 'Build the project'
complete -c myapp -n "__fish_myapp_using_subcommand help; and not __fish_seen_subcommand_from init build help" -f -a "help" -d 'Print this message or the help of the given subcommand(s)'Click to expand/collapse
_myapp() {
local i cur prev opts cmd
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
cur="$2"
else
cur="${COMP_WORDS[COMP_CWORD]}"
fi
prev="$3"
cmd=""
opts=""
for i in "${COMP_WORDS[@]:0:COMP_CWORD}"
do
case "${cmd},${i}" in
",$1")
cmd="myapp"
;;
myapp,build)
cmd="myapp__build"
;;
myapp,help)
cmd="myapp__help"
;;
myapp,init)
cmd="myapp__init"
;;
myapp__help,build)
cmd="myapp__help__build"
;;
myapp__help,help)
cmd="myapp__help__help"
;;
myapp__help,init)
cmd="myapp__help__init"
;;
*)
;;
esac
done
case "${cmd}" in
myapp)
opts="-c -v -o -f -h --config --verbose --output --format --help init build help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 1 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--config)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
-c)
local oldifs
if [ -n "${IFS+x}" ]; then
oldifs="$IFS"
fi
IFS=$'\n'
COMPREPLY=($(compgen -f "${cur}"))
if [ -n "${oldifs+x}" ]; then
IFS="$oldifs"
fi
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o filenames
fi
return 0
;;
--output)
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o plusdirs
fi
return 0
;;
-o)
COMPREPLY=()
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o plusdirs
fi
return 0
;;
--format)
COMPREPLY=($(compgen -W "json yaml toml" -- "${cur}"))
return 0
;;
-f)
COMPREPLY=($(compgen -W "json yaml toml" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
myapp__build)
opts="-h --release --target --help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--target)
COMPREPLY=("${cur}")
if [[ "${BASH_VERSINFO[0]}" -ge 4 ]]; then
compopt -o nospace
fi
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
myapp__help)
opts="init build help"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
myapp__help__build)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
myapp__help__help)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
myapp__help__init)
opts=""
if [[ ${cur} == -* || ${COMP_CWORD} -eq 3 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
myapp__init)
opts="-h --template --help <name>"
if [[ ${cur} == -* || ${COMP_CWORD} -eq 2 ]] ; then
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
fi
case "${prev}" in
--template)
COMPREPLY=($(compgen -W "basic advanced minimal" -- "${cur}"))
return 0
;;
*)
COMPREPLY=()
;;
esac
COMPREPLY=( $(compgen -W "${opts}" -- "${cur}") )
return 0
;;
esac
}
if [[ "${BASH_VERSINFO[0]}" -eq 4 && "${BASH_VERSINFO[1]}" -ge 4 || "${BASH_VERSINFO[0]}" -gt 4 ]]; then
complete -F _myapp -o nosort -o bashdefault -o default myapp
else
complete -F _myapp -o bashdefault -o default myapp
fiClick to expand/collapse
#compdef myapp
autoload -U is-at-least
_myapp() {
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[@]}" : \
'-c+[Path to configuration file]: :_files' \
'--config=[Path to configuration file]: :_files' \
'-o+[Output directory]: :_files -/' \
'--output=[Output directory]: :_files -/' \
'-f+[Output format]: :(json yaml toml)' \
'--format=[Output format]: :(json yaml toml)' \
'-v[Enable verbose output]' \
'--verbose[Enable verbose output]' \
'-h[Print help]' \
'--help[Print help]' \
":: :_myapp_commands" \
"*::: :->myapp" \
&& ret=0
case $state in
(myapp)
words=($line[1] "${words[@]}")
(( CURRENT += 1 ))
curcontext="${curcontext%:*:*}:myapp-command-$line[1]:"
case $line[1] in
(init)
_arguments "${_arguments_options[@]}" : \
'--template=[Project template to use]: :(basic advanced minimal)' \
'-h[Print help]' \
'--help[Print help]' \
':name -- Project name:_default' \
&& ret=0
;;
(build)
_arguments "${_arguments_options[@]}" : \
'--target=[Build target]: :' \
'--release[Build in release mode]' \
'-h[Print help]' \
'--help[Print help]' \
&& ret=0
;;
(help)
_arguments "${_arguments_options[@]}" : \
":: :_myapp__help_commands" \
"*::: :->help" \
&& ret=0
case $state in
(help)
words=($line[1] "${words[@]}")
(( CURRENT += 1 ))
curcontext="${curcontext%:*:*}:myapp-help-command-$line[1]:"
case $line[1] in
(init)
_arguments "${_arguments_options[@]}" : \
&& ret=0
;;
(build)
_arguments "${_arguments_options[@]}" : \
&& ret=0
;;
(help)
_arguments "${_arguments_options[@]}" : \
&& ret=0
;;
esac
;;
esac
;;
esac
;;
esac
}
(( $+functions[_myapp_commands] )) ||
_myapp_commands() {
local commands; commands=(
'init:Initialize a new project' \
'build:Build the project' \
'help:Print this message or the help of the given subcommand(s)' \
)
_describe -t commands 'myapp commands' commands "$@"
}
(( $+functions[_myapp__build_commands] )) ||
_myapp__build_commands() {
local commands; commands=()
_describe -t commands 'myapp build commands' commands "$@"
}
(( $+functions[_myapp__help_commands] )) ||
_myapp__help_commands() {
local commands; commands=(
'init:Initialize a new project' \
'build:Build the project' \
'help:Print this message or the help of the given subcommand(s)' \
)
_describe -t commands 'myapp help commands' commands "$@"
}
(( $+functions[_myapp__help__build_commands] )) ||
_myapp__help__build_commands() {
local commands; commands=()
_describe -t commands 'myapp help build commands' commands "$@"
}
(( $+functions[_myapp__help__help_commands] )) ||
_myapp__help__help_commands() {
local commands; commands=()
_describe -t commands 'myapp help help commands' commands "$@"
}
(( $+functions[_myapp__help__init_commands] )) ||
_myapp__help__init_commands() {
local commands; commands=()
_describe -t commands 'myapp help init commands' commands "$@"
}
(( $+functions[_myapp__init_commands] )) ||
_myapp__init_commands() {
local commands; commands=()
_describe -t commands 'myapp init commands' commands "$@"
}
if [ "$funcstack[1]" = "_myapp" ]; then
_myapp "$@"
else
compdef _myapp myapp
fiContributions are welcome! Please feel free to submit issues or pull requests.
Built with clap and clap_complete.