1# Copyright (c) 2010-2016, Aneurin Price <aneurin.price@gmail.com>
2
3# Permission is hereby granted, free of charge, to any person
4# obtaining a copy of this software and associated documentation
5# files (the "Software"), to deal in the Software without
6# restriction, including without limitation the rights to use,
7# copy, modify, merge, publish, distribute, sublicense, and/or sell
8# copies of the Software, and to permit persons to whom the
9# Software is furnished to do so, subject to the following
10# conditions:
11
12# The above copyright notice and this permission notice shall be
13# included in all copies or substantial portions of the Software.
14
15# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17# OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19# HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22# OTHER DEALINGS IN THE SOFTWARE.
23
24__ZFS_CMD="@sbindir@/zfs"
25__ZPOOL_CMD="@sbindir@/zpool"
26
27# Disable bash's built-in hostname completion, as this makes it impossible to
28# provide completions containing an @-sign, which is necessary for completing
29# snapshot names. If bash_completion is in use, this will already be disabled
30# and replaced with better completions anyway.
31shopt -u hostcomplete
32
33__zfs_get_commands()
34{
35    $__ZFS_CMD 2>&1 | awk '/^\t[a-z]/ {print $1}' | cut -f1 -d '|' | uniq
36}
37
38__zfs_get_properties()
39{
40    $__ZFS_CMD get 2>&1 | awk '$2 == "YES" || $2 == "NO" {print $1}'; echo all name space
41}
42
43__zfs_get_editable_properties()
44{
45    $__ZFS_CMD get 2>&1 | awk '$2 == "YES" {print $1"="}'
46}
47
48__zfs_get_inheritable_properties()
49{
50    $__ZFS_CMD get 2>&1 | awk '$3 == "YES" {print $1}'
51}
52
53__zfs_list_datasets()
54{
55    $__ZFS_CMD list -H -o name -s name -t filesystem,volume "$@"
56}
57
58__zfs_list_filesystems()
59{
60    $__ZFS_CMD list -H -o name -s name -t filesystem
61}
62
63__zfs_match_snapshot()
64{
65    local base_dataset="${cur%@*}"
66    if [[ "$base_dataset" != "$cur" ]]
67    then
68        $__ZFS_CMD list -H -o name -s name -t snapshot -d 1 "$base_dataset"
69    else
70        if [[ "$cur" != "" ]] && __zfs_list_datasets "$cur" &> /dev/null
71        then
72            $__ZFS_CMD list -H -o name -s name -t filesystem,volume -r "$cur" | tail -n +2
73            # We output the base dataset name even though we might be
74            # completing a command that can only take a snapshot, because it
75            # prevents bash from considering the completion finished when it
76            # ends in the bare @.
77            echo "$cur"
78            echo "$cur@"
79        else
80            local datasets
81            datasets="$(__zfs_list_datasets)"
82            # As above
83            echo "$datasets"
84            if [[ "$cur" == */ ]]
85            then
86                # If the current command ends with a slash, then the only way
87                # it can be completed with a single tab press (ie. in this pass)
88                # is if it has exactly one child, so that's the only time we
89                # need to offer a suggestion with an @ appended.
90                local num_children
91                # This is actually off by one as zfs list includes the named
92                # dataset in addition to its children
93                num_children=$(__zfs_list_datasets -d 1 "${cur%/}" 2> /dev/null | wc -l)
94                if [[ $num_children != 2 ]]
95                then
96                    return 0
97                fi
98            fi
99            echo "$datasets" | awk '{print $1 "@"}'
100        fi
101    fi
102}
103
104__zfs_match_snapshot_or_bookmark()
105{
106    local base_dataset="${cur%[#@]*}"
107    if [[ "$base_dataset" != "$cur" ]]
108    then
109        if [[ $cur == *@* ]]
110        then
111            $__ZFS_CMD list -H -o name -s name -t snapshot -d 1 "$base_dataset"
112        else
113            $__ZFS_CMD list -H -o name -s name -t bookmark -d 1 "$base_dataset"
114        fi
115    else
116        $__ZFS_CMD list -H -o name -s name -t filesystem,volume
117        if [[ -e "$cur" ]] && $__ZFS_CMD list -H -o name -s name -t filesystem,volume "$cur" &> /dev/null
118        then
119            echo "$cur@"
120            echo "$cur#"
121        fi
122    fi
123}
124
125__zfs_match_multiple_snapshots()
126{
127    local existing_opts
128    existing_opts="$(expr "$cur" : '\(.*\)[%,]')"
129    if [[ -e "$existing_opts" ]]
130    then
131        local base_dataset="${cur%@*}"
132        if [[ "$base_dataset" != "$cur" ]]
133        then
134            local cur="${cur##*,}"
135            if [[ $cur =~ ^%|%.*% ]]
136            then
137                # correct range syntax is start%end
138                return 1
139            fi
140            local range_start
141            range_start="$(expr "$cur" : '\(.*%\)')"
142            # shellcheck disable=SC2016
143            $__ZFS_CMD list -H -o name -s name -t snapshot -d 1 "$base_dataset" | sed 's$.*@$'"$range_start"'$g'
144        fi
145    else
146        __zfs_match_snapshot_or_bookmark
147    fi
148}
149
150__zfs_list_volumes()
151{
152    $__ZFS_CMD list -H -o name -s name -t volume
153}
154
155__zfs_argument_chosen()
156{
157    local word property
158    for word in $(seq $((COMP_CWORD-1)) -1 2)
159    do
160        local prev="${COMP_WORDS[$word]}"
161        if [[ ${COMP_WORDS[$word-1]} != -[tos] ]]
162        then
163            if [[ "$prev" == [^,]*,* ]] || [[ "$prev" == *[@:\#]* ]]
164            then
165                return 0
166            fi
167            for property in "$@"
168            do
169                if [[ $prev == "$property"* ]]
170                then
171                    return 0
172                fi
173            done
174        fi
175    done
176    return 1
177}
178
179__zfs_complete_ordered_arguments()
180{
181    local list1=$1
182    local list2=$2
183    local cur=$3
184    local extra=$4
185    # shellcheck disable=SC2086
186    if __zfs_argument_chosen $list1
187    then
188        mapfile -t COMPREPLY < <(compgen -W "$list2 $extra" -- "$cur")
189    else
190        mapfile -t COMPREPLY < <(compgen -W "$list1 $extra" -- "$cur")
191    fi
192}
193
194__zfs_complete_multiple_options()
195{
196    local options=$1
197    local cur=$2
198    local existing_opts
199
200    mapfile -t COMPREPLY < <(compgen -W "$options" -- "${cur##*,}")
201    existing_opts=$(expr "$cur" : '\(.*,\)')
202    if [[ -n "$existing_opts" ]]
203    then
204        COMPREPLY=( "${COMPREPLY[@]/#/${existing_opts}}" )
205    fi
206}
207
208__zfs_complete_switch()
209{
210    local options=$1
211    if [[ ${cur:0:1} == - ]]
212    then
213        mapfile -t COMPREPLY < <(compgen -W "-{$options}" -- "$cur")
214        return 0
215    else
216        return 1
217    fi
218}
219
220__zfs_complete_nospace()
221{
222    # Google indicates that there may still be bash versions out there that
223    # don't have compopt.
224    if type compopt &> /dev/null
225    then
226        compopt -o nospace
227    fi
228}
229
230__zfs_complete()
231{
232    local cur prev cmd cmds
233    COMPREPLY=()
234    if type _get_comp_words_by_ref &> /dev/null
235    then
236        # Don't split on colon
237        _get_comp_words_by_ref -n : -c cur -p prev -w COMP_WORDS -i COMP_CWORD
238    else
239        cur="${COMP_WORDS[COMP_CWORD]}"
240        prev="${COMP_WORDS[COMP_CWORD-1]}"
241    fi
242    cmd="${COMP_WORDS[1]}"
243
244    if [[ ${prev##*/} == zfs ]]
245    then
246        cmds=$(__zfs_get_commands)
247        mapfile -t COMPREPLY < <(compgen -W "$cmds -?" -- "$cur")
248        return 0
249    fi
250
251    case "${cmd}" in
252        bookmark)
253            if __zfs_argument_chosen
254            then
255                mapfile -t COMPREPLY < <(compgen -W "${prev%@*}# ${prev/@/#}" -- "$cur")
256            else
257                mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
258            fi
259            ;;
260        clone)
261            case "${prev}" in
262                -o)
263                    mapfile -t COMPREPLY < <(compgen -W "$(__zfs_get_editable_properties)" -- "$cur")
264                    __zfs_complete_nospace
265                    ;;
266                *)
267                    if ! __zfs_complete_switch "o,p"
268                    then
269                        if __zfs_argument_chosen
270                        then
271                            mapfile -t COMPREPLY < <(compgen -W "$(__zfs_list_datasets)" -- "$cur")
272                        else
273                            mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
274                        fi
275                    fi
276                    ;;
277            esac
278            ;;
279        get)
280            case "${prev}" in
281                -d)
282                    mapfile -t COMPREPLY < <(compgen -W "" -- "$cur")
283                    ;;
284                -t)
285                    __zfs_complete_multiple_options "filesystem volume snapshot bookmark all" "$cur"
286                    ;;
287                -s)
288                    __zfs_complete_multiple_options "local default inherited temporary received none" "$cur"
289                    ;;
290                -o)
291                    __zfs_complete_multiple_options "name property value source received all" "$cur"
292                    ;;
293                *)
294                    if ! __zfs_complete_switch "H,r,p,d,o,t,s"
295                    then
296                        # shellcheck disable=SC2046
297                        if __zfs_argument_chosen $(__zfs_get_properties)
298                        then
299                            mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
300                        else
301                            __zfs_complete_multiple_options "$(__zfs_get_properties)" "$cur"
302                        fi
303                    fi
304                    ;;
305            esac
306            ;;
307        inherit)
308            if ! __zfs_complete_switch "r"
309            then
310                __zfs_complete_ordered_arguments "$(__zfs_get_inheritable_properties)" "$(__zfs_match_snapshot)" "$cur"
311            fi
312            ;;
313        list)
314            case "${prev}" in
315                -d)
316                    mapfile -t COMPREPLY < <(compgen -W "" -- "$cur")
317                    ;;
318                -t)
319                    __zfs_complete_multiple_options "filesystem volume snapshot bookmark all" "$cur"
320                    ;;
321                -o)
322                    __zfs_complete_multiple_options "$(__zfs_get_properties)" "$cur"
323                    ;;
324                -s|-S)
325                    mapfile -t COMPREPLY < <(compgen -W "$(__zfs_get_properties)" -- "$cur")
326                    ;;
327                *)
328                    if ! __zfs_complete_switch "H,r,d,o,t,s,S"
329                    then
330                        mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
331                    fi
332                    ;;
333            esac
334            ;;
335        promote)
336            mapfile -t COMPREPLY < <(compgen -W "$(__zfs_list_filesystems)" -- "$cur")
337            ;;
338        rollback)
339            if ! __zfs_complete_switch "r,R,f"
340            then
341                mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
342            fi
343            ;;
344        send)
345            if ! __zfs_complete_switch "D,n,P,p,R,v,e,L,i,I"
346            then
347                if __zfs_argument_chosen
348                then
349                    mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
350                else
351                    if [[ $prev == -*i* ]]
352                    then
353                        mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot_or_bookmark)" -- "$cur")
354                    else
355                        mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
356                    fi
357                fi
358            fi
359            ;;
360        snapshot)
361            case "${prev}" in
362                -o)
363                    mapfile -t COMPREPLY < <(compgen -W "$(__zfs_get_editable_properties)" -- "$cur")
364                    __zfs_complete_nospace
365                    ;;
366                *)
367                    if ! __zfs_complete_switch "o,r"
368                    then
369                        mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
370                        __zfs_complete_nospace
371                    fi
372                    ;;
373            esac
374            ;;
375        set)
376            __zfs_complete_ordered_arguments "$(__zfs_get_editable_properties)" "$(__zfs_match_snapshot)" "$cur"
377            __zfs_complete_nospace
378            ;;
379        upgrade)
380            case "${prev}" in
381                -a|-V|-v)
382                    mapfile -t COMPREPLY < <(compgen -W "" -- "$cur")
383                    ;;
384                *)
385                    if ! __zfs_complete_switch "a,V,v,r"
386                    then
387                        mapfile -t COMPREPLY < <(compgen -W "$(__zfs_list_filesystems)" -- "$cur")
388                    fi
389                    ;;
390            esac
391            ;;
392        destroy)
393            if ! __zfs_complete_switch "d,f,n,p,R,r,v"
394            then
395                __zfs_complete_multiple_options "$(__zfs_match_multiple_snapshots)" "$cur"
396                __zfs_complete_nospace
397            fi
398            ;;
399        *)
400            mapfile -t COMPREPLY < <(compgen -W "$(__zfs_match_snapshot)" -- "$cur")
401            ;;
402    esac
403    if type __ltrim_colon_completions &> /dev/null
404    then
405        __ltrim_colon_completions "$cur"
406    fi
407    return 0
408}
409
410__zpool_get_commands()
411{
412    $__ZPOOL_CMD 2>&1 | awk '/^\t[a-z]/ {print $1}' | uniq
413}
414
415__zpool_get_properties()
416{
417    $__ZPOOL_CMD get 2>&1 | awk '$2 == "YES" || $2 == "NO" {print $1}'; echo all
418}
419
420__zpool_get_editable_properties()
421{
422    $__ZPOOL_CMD get 2>&1 | awk '$2 == "YES" {print $1"="}'
423}
424
425__zpool_list_pools()
426{
427    $__ZPOOL_CMD list -H -o name
428}
429
430__zpool_complete()
431{
432    local cur prev cmd cmds pools
433    COMPREPLY=()
434    cur="${COMP_WORDS[COMP_CWORD]}"
435    prev="${COMP_WORDS[COMP_CWORD-1]}"
436    cmd="${COMP_WORDS[1]}"
437
438    if [[ ${prev##*/} == zpool ]]
439    then
440        cmds=$(__zpool_get_commands)
441        mapfile -t COMPREPLY < <(compgen -W "$cmds" -- "$cur")
442        return 0
443    fi
444
445    case "${cmd}" in
446        get)
447            __zfs_complete_ordered_arguments "$(__zpool_get_properties)" "$(__zpool_list_pools)" "$cur"
448            return 0
449            ;;
450        import)
451            if [[ $prev == -d ]]
452            then
453                _filedir -d
454            else
455                mapfile -t COMPREPLY < <(compgen -W "$(__zpool_list_pools) -d" -- "$cur")
456            fi
457            return 0
458            ;;
459        set)
460            __zfs_complete_ordered_arguments "$(__zpool_get_editable_properties)" "$(__zpool_list_pools)" "$cur"
461            __zfs_complete_nospace
462            return 0
463            ;;
464        add|attach|clear|create|detach|offline|online|remove|replace)
465            pools="$(__zpool_list_pools)"
466            # shellcheck disable=SC2086
467            if __zfs_argument_chosen $pools
468            then
469                _filedir
470            else
471                mapfile -t COMPREPLY < <(compgen -W "$pools" -- "$cur")
472            fi
473            return 0
474            ;;
475        *)
476            mapfile -t COMPREPLY < <(compgen -W "$(__zpool_list_pools)" -- "$cur")
477            return 0
478            ;;
479    esac
480
481}
482
483complete -F __zfs_complete zfs
484complete -F __zpool_complete zpool
485