#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 " 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 ". # 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