diff options
author | Ed Santiago <santiago@redhat.com> | 2019-03-01 15:39:51 -0700 |
---|---|---|
committer | Ed Santiago <santiago@redhat.com> | 2019-03-13 16:40:07 -0600 |
commit | 6aa8078cc1f54a5c6e6197d7e0273e36ed55060e (patch) | |
tree | 2b8b1aeb3c0c09bd53bc988da4a68cf33f233542 /completions/zsh | |
parent | 7426d4fbbeaf5ebd3d55576add89b99cd3f3f760 (diff) | |
download | podman-6aa8078cc1f54a5c6e6197d7e0273e36ed55060e.tar.gz podman-6aa8078cc1f54a5c6e6197d7e0273e36ed55060e.tar.bz2 podman-6aa8078cc1f54a5c6e6197d7e0273e36ed55060e.zip |
zsh completion
Weekend hack by someone who doesn't grok zsh completion
but who finds it deeply offensive that most completion
files have an unmaintainable duplication of options
and arguments. The idea behind this one is to discover
the command line using --help, with a few hardcoded
helpers for discovering containers, images, pods,
and figuring out which args take files/dirs as args.
Working remarkably well. I am using this in my daily
routine and wondering how I ever managed without it.
It's not perfect -- a future version can perhaps
show only stopped containers for podman rm, only
running ones for podman stop -- but ROI seems low
on that given my limited zsh completion skills.
Sadly, I can't figure out how to write a regression
test suite for this. It would be lovely to have a
list if partial command lines and expected completions,
because the history of this change is that (seemingly)
minor tweaks in one place cause breakage in another.
Does anyone know of such a framework?
Still... working well enough to ship, IMO.
Signed-off-by: Ed Santiago <santiago@redhat.com>
Diffstat (limited to 'completions/zsh')
-rw-r--r-- | completions/zsh/_podman | 385 |
1 files changed, 385 insertions, 0 deletions
diff --git a/completions/zsh/_podman b/completions/zsh/_podman new file mode 100644 index 000000000..530649c0c --- /dev/null +++ b/completions/zsh/_podman @@ -0,0 +1,385 @@ +#compdef podman + +# To get zsh to reread this file: unset -f _podman;rm -f ~/.zcompdump;compinit + +# On rereads, reset cache. (Not that the cacheing works, but some day it might) +unset -m '_podman_*' + +############################################################################### +# BEGIN 'podman help' parsers -- for options, subcommands, and usage + +# Run 'podman XX --help', set _podman_commands to a formatted list of cmds +_read_podman_commands() { + local line + + # Cache: the intention here is to run each 'podman help' only once. + # Unfortunately it doesn't seem to actually be working: even though + # I can see the var_ref in my shell, it's not visible here. + local _var_ref=_podman_commands_"${*// /_}" + typeset -ga _podman_commands + _podman_commands=(${(P)_var_ref}) + (( $#_podman_commands )) && return + + _call_program podman podman "$@" --help |\ + sed -n -e '0,/^Available Commands/d' -e '/^[A-Z]/q;p' |\ + sed -e 's/^ \+\([^ ]\+\) \+/\1:/' |\ + egrep . | while read line; do + _podman_commands+=($line) + done + + eval "typeset -ga $_var_ref" + eval "$_var_ref=(\$_podman_commands)" +} + +# Run 'podman XX --help', set _podman_flag_list to a formatted list +# of flag options for XX +_read_podman_flags() { + local line + + local _var_ref=_podman_flags_"${*// /_}" + eval "typeset -ga ${_var_ref}" + typeset -ga _podman_flag_list + _podman_flag_list=(${(P)_var_ref}) + (( $#_podman_flag_list )) && return + + # Extract the Flags; strip leading whitespace; pack '-f, --foo' + # as '-f,--foo' (no space); then add '=' to '--foo string'. + # The result will be, e.g. '-f,--foo=string Description of Option' + _call_program podman podman "$@" --help |\ + sed -n -e '0,/^Flags:/d' -e '/^$/q;p' |\ + sed -e 's/^ *//' -e 's/^\(-.,\) --/\1--/' |\ + sed -e 's/^\(-[^ ]\+\) \([^ ]\+\) /\1=\2 /' |\ + while read flags desc;do + # flags like --foo=string: split into --foo & string + local -a tmpa + local optval= + tmpa=(${(s.=.)flags}) + if [ -n "$tmpa[2]" ]; then + flags=$tmpa[1] + optval=$tmpa[2] + fi + + # 'podman attach --detach-keys' includes ']' in help msg + desc=${desc//\]/\\]} + + for flag in ${(s:,:)flags}; do + if [ -n "$optval" ]; then + _podman_flag_list+=("${flag}[$desc]:$(_podman_find_helper ${flags} ${optval} ${desc})") + else + _podman_flag_list+=("${flag}[$desc]") + fi + done + done + + eval "typeset -ga $_var_ref=(\$_podman_flag_list)" +} + +# Run 'podman XXX --help', set _podman_usage to the line after "Usage:" +_read_podman_usage() { + local _var_ref=_podman_usage_"${*// /_}" + typeset -ga _podman_usage + _podman_usage=${(P)_var_ref} + (( $#_podman_usage )) && return + + _podman_usage=$(_call_program podman podman "$@" --help |\ + grep -A1 'Usage:'|\ + tail -1 |\ + sed -e 's/^ *//') + + eval "typeset -ga $_var_ref" + eval "$_var_ref=\$_podman_usage" +} + +# END 'podman help' parsers +############################################################################### +# BEGIN custom helpers for individual option arguments + +# Find a zsh helper for a given flag or command-line option +_podman_find_helper() { + local flags=$1 + local optval=$2 + local desc=$3 + local helper= + + # Yes, this is a lot of hardcoding. IMHO it's still better than + # hardcoding every possible podman option. + # FIXME: there are many more options that could use helpers. + if expr "$desc" : ".*[Dd]irectory" >/dev/null; then + optval="directory" + helper="_files -/" + elif expr "$desc" : ".*[Pp]ath" >/dev/null; then + optval="path" + helper=_files + elif [ "$flags" = "--cgroup-manager" ]; then + optval="cgroup manager" + helper="(cgroupfs systemd)" + elif [ "$flags" = "--log-level" ]; then + optval="log level" + # 'Log messages above specified level: debug, ... (default "...")' + # Strip off the description and all 'default' strings + desc=${desc/Log*:/} # debug, info, ... (default "...") + desc=${(S)desc//\(*\)/} # debug, info, ... or panic + desc=${desc//,/} # debug info ... or panic + desc=${desc// or / } # debug info ... panic + desc=${desc// / } # collapse multiple spaces + # FIXME: how to present values _in order_, not sorted alphabetically? + helper="($desc)" + fi + echo "$optval:$helper" +} + +# END custom helpers for individual option arguments +############################################################################### +# BEGIN helpers for command-line args (containers, images) + +__podman_helper_generic() { + local expl line + local -a results + + local foo1=$1; shift + local name=$2; shift + + _call_program $foo1 podman "$@" |\ + while read line; do + results+=(${=line}) + done + + _wanted $foo1 expl $name compadd ${(u)results} +} + +_podman_helper_image() { + __podman_helper_generic podman-images 'images' \ + images --format '{{.ID}}\ {{.Repository}}:{{.Tag}}' +} + +# FIXME: at some point, distinguish between running & stopped containers +_podman_helper_container() { + __podman_helper_generic podman-containers 'containers' \ + ps -a --format '{{.Names}}\ {{.ID}}' +} + +_podman_helper_pod() { + __podman_helper_generic podman-pods 'pods' pod list --format '{{.Name}}' +} + +_podman_helper_volume() { + __podman_helper_generic podman-volumes 'volumes' volume ls --format '{{.Name}}' +} + +# Combinations. This one seen in diff & inspect +_podman_helper_container-or-image() { + _podman_helper_image + _podman_helper_container +} + +# Seen in generate-kube +_podman_helper_container-or-pod() { + _podman_helper_container + _podman_helper_pod +} + +# For top and pod-top +_podman_helper_format-descriptors() { + __podman_helper_generic top-format-descriptors 'format descriptors' \ + top --list-descriptors +} + +# for push, login/logout, and trust +# FIXME: some day, use this to define a helper for IMAGE-PATH (in 'pull') +_podman_helper_registry() { + local expl + local -a registries + + # Suggestions for improvement more than welcome. + python3 -c 'from configparser import ConfigParser;cp=ConfigParser();cp.read("/etc/containers/registries.conf");registries=eval(cp.get("registries.search","registries"));[print(r) for r in registries]' 2>/dev/null | while read line; do + registries+=($line) + done + + if (( $#registries )); then + _wanted podman-registry expl "registry" compadd ${(u)registries} + else + _hosts + fi +} + +# END helpers for command-line args +############################################################################### +# BEGIN figure out completion helpers for a given (sub)command + +# Read Usage string for this subcommand, set up helpers for its subargs +_set_up_podman_args() { + _read_podman_usage "$@" + + typeset -ga _podman_args=() + # E.g. 'podman exec [flags] CONTAINER [...' -> 'CONTAINER [....' + local usage_rhs=$(expr "$_podman_usage" : ".*\[flags\] \+\(.*\)") + + # e.g. podman pod ps which takes no further args + if [ -z "$usage_rhs" ]; then + return + fi + + # podman diff & inspect accept 'CONTAINER | IMAGE'; make into one keyword. + usage_rhs=${usage_rhs// | /-OR-} + + # Arg parsing. There are three possibilities in Usage messages: + # + # [IMAGE] - optional image arg (zero or one) + # IMAGE - exactly one image arg + # IMAGE [IMAGE...] - one or more image args + # and, theoretically: + # [IMAGE...] - zero or more? Haven't seen it in practice. Defer. + # + # For completion purposes, we only need to provide two options: + # one, or more than one? That is: continue offering completion + # suggestions after the first one? For that, we make two passes; + # in the first, mark an option as either '' (only one) or + + # Parse each command-line arg seen in usage message + local word + local -A _seen=() + for word in ${=usage_rhs}; do + local unbracketed=$(expr "$word" : "\[\(.*\)\]") + + if [ -n "$unbracketed" ]; then + # Remove all dots; assume(!?) that they'll all be at the end + unbracketed=${unbracketed//./} + + if (( $_seen[$unbracketed] )); then + # Is this the same word as the previous arg? + if expr "$_podman_args[-1]" : ":$unbracketed:" >/dev/null; then + # Yes. Make it '*:...' instead of ':...', indicating >1 + _podman_args[-1]="*$_podman_args[-1]" + fi + continue + fi + + word=$unbracketed + fi + + # As of 2019-03 all such instances are '[COMMAND [ARG...]]' and are, + # of course, at the end of the line. We can't offer completion for + # these, because the container will have different commands than + # the host system... but try anyway. + if [ "$word" = '[COMMAND' ]; then + # e.g. podman create, exec, run + _podman_args+=( + ":command: _command_names -e" + "*::arguments: _normal" + ) + return + fi + + # Look for an existing helper, e.g. IMAGE -> _podman_helper_image + local helper="_podman_helper_${(L)word}" + if (( $+functions[$helper] )); then + : + else + # No defined helper. Reset, but check for known expressions. + helper= + case "$word" in + KUBEFILE) helper='_files -g "*.y(|a)ml(-.)"' ;; + PATH) helper='_files' ;; + esac + fi + + # Another special case: 'top' actually takes multiple options + local multi= + if [ "$word" = "FORMAT-DESCRIPTORS" ]; then + multi='*' + fi + _podman_args+=("$multi:${(L)word}:$helper") + _seen[$word]=1 + done +} + +# For an endpoint command, i.e. not a subcommand. +_podman_terminus() { + typeset -A opt_args + typeset -ga _podman_flag_list + typeset -ga _podman_args + integer ret=1 + + # Find out what args it takes (e.g. image(s), container(s)) and see + # if we have helpers for them. + _set_up_podman_args "$@" + _arguments -C $_podman_flag_list $_podman_args && ret=0 + + return ret +} + +# END figure out completion helpers for a given (sub)command +################################################################################ +# BEGIN actual entry point + +# This is the main entry point; it's also where we (recursively) come in +# to handle nested subcommands such as 'podman container' or even 3-level +# ones like 'podman generate kube'. Nesting is complicated, so here's +# my best understanding as of 2019-03-12: +# +# Easy first: when you do "podman <TAB>" zsh calls us, we run 'podman --help', +# figure out the global options and subcommands, and run the magic _arguments +# command. That will offer those options/subcommands, and complete, etc. +# +# Where it gets harder is when you do "podman container mount <TAB>". +# zsh first calls us with words=(podman container mount) but we don't +# want all that full context yet! We want to go a piece at a time, +# handling 'container' first, then 'mount'; ending up with our +# final 'podman container mount --help' giving us suitable flags +# and no subcommands; from which we determine that it's a terminus +# and jump to a function that handles non-subcommand arguments. +# +# This is the closest I've yet come to understanding zsh completion; +# it is still incomplete and may in fact be incorrect. But it works +# better than anything I've played with so far. +_podman_subcommand() { + local curcontext="$curcontext" state line + typeset -A opt_args + integer ret=1 + + # Run 'podman --help' / 'podman system --help' for our context (initially + # empty, then possibly under subcommands); from those, get a list of + # flags appropriate for this context and, if applicable, subcommands. + _read_podman_flags "$@" + _read_podman_commands "$@" + + # Now, is this a sub-subcommand, or does it have args? + if (( $#_podman_commands )); then + # Subcommands required (podman, podman system, etc) + local cmd=${words// /_} + _arguments -C $_podman_flag_list \ + "(-): :->command" \ + "(-)*:: :->option-or-argument" \ + && ret=0 + + case $state in + (command) + _describe -t command "podman $* command" _podman_commands && ret=0 + ;; + (option-or-argument) + # I think this is when we have a _completed_ subcommand. + # Recurse back into here, offering options for that subcommand. + curcontext=${curcontext%:*:*}:podman-${words[1]}: + _podman_subcommand "$@" ${words[1]} && ret=0 + ;; + esac + else + # At a terminus, i.e. podman info, podman history; find out + # what args it takes. + _podman_terminus "$@" && ret=0 + fi + + return ret +} + +_podman() { + _podman_subcommand +} + +# Local Variables: +# mode: shell-script +# sh-indentation: 4 +# indent-tabs-mode: nil +# sh-basic-offset: 4 +# End: +# vim: ft=zsh sw=4 ts=4 et |