summaryrefslogtreecommitdiff
path: root/completions/zsh/_podman
blob: 067eebbbbfc0212d38070ebb83ad05bb7735e600 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
#compdef podman

# To get zsh to reread this file: unset -f _podman;rm -f ~/.zcompdump;compinit

# On rereads, reset cache. (Not that the caching 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' |\
        grep '^ \+-' |\
        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
    # For messages like 'restart policy ("always"|"no"|"on-failure")
    elif optlist=$(expr "$desc" : '.*(\(\"[^\\)]\+|[^\\)]\+\"\))' 2>/dev/null); then
        optval=${${flags##--}//-/ }       # "--log-level" => "log level"
        optlist=${optlist//\"/}           # "a"|"b"|"c"   => a|b|c
        optlist=${optlist//\|/ }          # a|b|c         => a b c
        # FIXME: how to present values _in order_, not sorted alphabetically?
        helper="($optlist)"
    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