Skip to content

Commit c765db0

Browse files
committed
contrib/completion/zsh: add CLI plugin completion support
Previously, the zsh completion script could list Docker CLI plugin commands (compose, buildx, etc.) via __docker_commands(), but had no handler in __docker_subcommand() for them. This meant that while "docker <TAB>" would show "compose" as an option, "docker compose <TAB>" produced no completions. Add support for CLI plugin completion by invoking the plugin binary's Cobra __completeNoDesc protocol, similar to how the bash completion already handles this. The implementation handles all six Cobra ShellCompDirective values: - Error (1): abort completion - NoSpace (2): suppress trailing space - NoFileComp (4): suppress file fallback - FilterFileExt (8): filter files by extension - FilterDirs (16): complete directories only - KeepOrder (32): preserve completion ordering Additionally: - Plugin paths are cached using zsh's _store_cache/_retrieve_cache with the same 1-hour TTL policy as __docker_commands - ActiveHelp markers are displayed as informational text - Literal colons in completions are escaped for _describe - --flag=<TAB> completions propagate the flag prefix correctly - Words are truncated to CURRENT for backward cursor movement Closes: #6231 Signed-off-by: 4RH1T3CT0R7 <iprintercanon@gmail.com>
1 parent d62b889 commit c765db0

File tree

1 file changed

+167
-0
lines changed

1 file changed

+167
-0
lines changed

contrib/completion/zsh/_docker

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2617,6 +2617,163 @@ __docker_context_subcommand() {
26172617

26182618
# EO context
26192619

2620+
# BO cli-plugin completion
2621+
2622+
# Returns the path of a CLI plugin binary given the plugin command name.
2623+
# Looks up plugin paths from `docker info` output.
2624+
# Results are cached using the same policy as __docker_commands (1 hour TTL).
2625+
__docker_cli_plugin_path() {
2626+
local plugin_name=$1
2627+
local cache_policy
2628+
2629+
zstyle -s ":completion:${curcontext}:" cache-policy cache_policy
2630+
if [[ -z "$cache_policy" ]]; then
2631+
zstyle ":completion:${curcontext}:" cache-policy __docker_caching_policy
2632+
fi
2633+
2634+
if ( [[ ${+_docker_plugin_paths} -eq 0 ]] || _cache_invalid docker_plugin_paths ) \
2635+
&& ! _retrieve_cache docker_plugin_paths;
2636+
then
2637+
local -a raw_paths
2638+
raw_paths=(${(f)"$(_call_program commands docker $docker_options info --format '{{range .ClientInfo.Plugins}}{{.Path}}{{"\n"}}{{end}}')"})
2639+
declare -gA _docker_plugin_paths
2640+
_docker_plugin_paths=()
2641+
local p base
2642+
for p in $raw_paths; do
2643+
base=${p:t}
2644+
base=${base#docker-}
2645+
base=${base%%.*}
2646+
_docker_plugin_paths[$base]=$p
2647+
done
2648+
(( ${#_docker_plugin_paths} > 0 )) && _store_cache docker_plugin_paths _docker_plugin_paths
2649+
fi
2650+
2651+
if [[ -n "${_docker_plugin_paths[$plugin_name]}" ]]; then
2652+
echo "${_docker_plugin_paths[$plugin_name]}"
2653+
return 0
2654+
fi
2655+
return 1
2656+
}
2657+
2658+
# Completes arguments for a CLI plugin by invoking the plugin binary
2659+
# with the __completeNoDesc command (Cobra shell completion protocol).
2660+
# Reference: vendor/github.com/spf13/cobra/zsh_completions.go
2661+
__docker_complete_cli_plugin() {
2662+
local plugin_path=$1
2663+
shift
2664+
integer ret=1
2665+
2666+
# Truncate words to CURRENT to handle backward cursor movement,
2667+
# matching Cobra's own zsh template behavior.
2668+
local -a plugin_args
2669+
plugin_args=("${words[2,CURRENT]}")
2670+
2671+
# Cobra expects an empty trailing argument when the cursor is past
2672+
# the last typed word (user pressed space, starting a new argument).
2673+
local last_char=${words[CURRENT][-1]}
2674+
if [[ -z "$last_char" ]]; then
2675+
plugin_args+=('')
2676+
fi
2677+
2678+
# Detect --flag= prefix for completions (e.g., --output=<TAB>).
2679+
local flagPrefix=""
2680+
setopt local_options BASH_REMATCH
2681+
local lastParam=${plugin_args[-1]}
2682+
if [[ "$lastParam" =~ '-.*=' ]]; then
2683+
flagPrefix="-P ${BASH_REMATCH}"
2684+
fi
2685+
2686+
local raw_output
2687+
raw_output=$(_call_program commands "$plugin_path" __completeNoDesc "${plugin_args[@]}" 2>/dev/null)
2688+
[[ -z "$raw_output" ]] && return 1
2689+
2690+
local -a lines
2691+
lines=("${(@f)raw_output}")
2692+
2693+
# The last line is a Cobra shell completion directive (bitmask, e.g. :0, :2, :8).
2694+
# Validate it starts with ':' before parsing; default to 0 otherwise.
2695+
# Directive values: 1=Error, 2=NoSpace, 4=NoFileComp,
2696+
# 8=FilterFileExt, 16=FilterDirs, 32=KeepOrder
2697+
local directive=${lines[-1]}
2698+
local dir_num=0
2699+
if [[ "${directive[1]}" == ':' ]]; then
2700+
dir_num=${directive#:}
2701+
lines=("${lines[1,-2]}")
2702+
fi
2703+
2704+
# Bit 0 (value 1): ShellCompDirectiveError - abort completion
2705+
if (( dir_num & 1 )); then
2706+
return 1
2707+
fi
2708+
2709+
# Bit 3 (value 8): ShellCompDirectiveFilterFileExt
2710+
if (( dir_num & 8 )); then
2711+
local -a glob_args
2712+
local ext
2713+
for ext in $lines; do
2714+
if [[ ${ext[1]} != '*' ]]; then
2715+
ext="*.$ext"
2716+
fi
2717+
glob_args+=(-g "$ext")
2718+
done
2719+
_files "${glob_args[@]}" $flagPrefix && ret=0
2720+
return ret
2721+
fi
2722+
2723+
# Bit 4 (value 16): ShellCompDirectiveFilterDirs
2724+
if (( dir_num & 16 )); then
2725+
local subdir="${lines[1]}"
2726+
if [[ -n "$subdir" ]]; then
2727+
pushd "$subdir" >/dev/null 2>&1
2728+
_files -/ $flagPrefix && ret=0
2729+
popd >/dev/null 2>&1
2730+
else
2731+
_files -/ $flagPrefix && ret=0
2732+
fi
2733+
return ret
2734+
fi
2735+
2736+
# Filter ActiveHelp markers and build completions list.
2737+
# Escape literal colons since _describe uses colon as separator.
2738+
local -a completions
2739+
local line val desc activeHelpMarker="_activeHelp_ "
2740+
for line in $lines; do
2741+
if [[ "$line" == ${activeHelpMarker}* ]]; then
2742+
compadd -x "${line#$activeHelpMarker}"
2743+
continue
2744+
fi
2745+
if [[ $line == *$'\t'* ]]; then
2746+
val=${line%%$'\t'*}
2747+
desc=${line#*$'\t'}
2748+
val=${val//:/\\:}
2749+
desc=${desc//:/\\:}
2750+
completions+=("${val}:${desc}")
2751+
else
2752+
completions+=("${line//:/\\:}")
2753+
fi
2754+
done
2755+
2756+
if (( ${#completions} > 0 )); then
2757+
local -a suf order
2758+
# Bit 1 (value 2): ShellCompDirectiveNoSpace
2759+
if (( dir_num & 2 )); then
2760+
suf=(-S '')
2761+
fi
2762+
# Bit 5 (value 32): ShellCompDirectiveKeepOrder
2763+
if (( dir_num & 32 )); then
2764+
order=(-V)
2765+
fi
2766+
_describe $order -t "docker-${words[1]}-completions" "docker ${words[1]} command" completions $flagPrefix $suf && ret=0
2767+
elif ! (( dir_num & 4 )); then
2768+
# No completions and NoFileComp not set: fall back to file completion
2769+
_files $flagPrefix && ret=0
2770+
fi
2771+
2772+
return ret
2773+
}
2774+
2775+
# EO cli-plugin completion
2776+
26202777
__docker_caching_policy() {
26212778
oldp=( "$1"(Nmh+1) ) # 1 hour
26222779
(( $#oldp ))
@@ -3050,6 +3207,16 @@ __docker_subcommand() {
30503207
(help)
30513208
_arguments $(__docker_arguments) ":subcommand:__docker_commands" && ret=0
30523209
;;
3210+
(*)
3211+
# CLI plugin completion: delegate to the plugin binary if found.
3212+
# Plugins such as "compose" and "buildx" support Cobra's
3213+
# __completeNoDesc protocol for shell completion.
3214+
local plugin_path
3215+
plugin_path=$(__docker_cli_plugin_path "$words[1]")
3216+
if [[ -n "$plugin_path" ]]; then
3217+
__docker_complete_cli_plugin "$plugin_path" "${words[2,-1]}" && ret=0
3218+
fi
3219+
;;
30533220
esac
30543221

30553222
return ret

0 commit comments

Comments
 (0)