--- /dev/null
+#!/bin/bash
+
+# -----------------------------------------------------------------
+# Programmable completion code for ipset (netfilter.org)
+#
+# https://github.com/AllKind/ipset-bash-completion
+# https://sourceforge.net/projects/ipset-bashcompl
+# -----------------------------------------------------------------
+
+# Copyright (C) 2013 AllKind (AllKind@fastest.cc)
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+# -----------------------------------------------------------------
+# Tested with ipset versions:
+# 6.16.1
+# -----------------------------------------------------------------
+#
+# Put it into ~/.bash_completion or /etc/bash_completion.d/
+#
+# -----------------------------------------------------------------
+#
+# Version 1.9
+#
+# -----------------------------------------------------------------
+
+
+_ipset_bash_default_compl() { # taken from examples - modified by me
+# call with the word to be completed as $1
+local t
+if [[ $1 == \$\(* ]]; then # command substitution
+ t=${1#??}
+ COMPREPLY=( $(compgen -c -P '$(' $t) )
+elif [[ $1 == \$\{* ]]; then # variables with a leading `${'
+ t=${1#??}
+ COMPREPLY=( $(compgen -v -P '${' -S '}' $t) )
+elif [[ $1 == \$* ]]; then # variables with a leading `$'
+ t=${1#?}
+ COMPREPLY=( $(compgen -v -P '$' $t ) )
+elif [[ "$1" == *@* ]]; then # hostname
+ t=${1#*@}
+ COMPREPLY=( $( compgen -A hostname $t ) )
+elif [[ $1 == *[*?[]* ]]; then # sh-style glob pattern
+ COMPREPLY=( $( compgen -G "$1" ) )
+# ksh-style extended glob pattern - must be complete
+elif shopt -q extglob && [[ $1 == *[?*+\!@]\(*\)* ]]; then
+ COMPREPLY=( $( compgen -G "$1" ) )
+fi
+}
+
+_ipset_complete() {
+shopt -s extglob
+local cur prev str_action ips_version
+local -i i=x=y=got_action=in_list=0
+local -i ignore_errors=use_file=names_only=headers_only=save_format=res_sort=0
+local arr_sets=() arr_types=() arr_members=()
+
+# ipset version check 6.x upwards (to v?) is supported
+ips_version="$(ipset --version)"
+ips_version="${ips_version#ipset v}"
+ips_version="${ips_version%%.*}"
+[[ $ips_version = +([[:digit:]]) ]] || return 1
+((ips_version < 6)) && return 1
+
+COMPREPLY=()
+COMP_WORDBREAKS=$' \t\n"\'><=;|&('
+
+# expecting _get_comp_words_by_ref() to exist from bash_completion
+_get_comp_words_by_ref cur || return
+_get_comp_words_by_ref prev || return
+
+#DEBUG=Y
+if [[ $DEBUG ]]; then
+ printf "\nCOMP_WORDBREAKS: <%s>\n" "$COMP_WORDBREAKS"
+ printf "COMP_LINE: <%s>\n" "$COMP_LINE"
+ printf "COMP_TYPE: <%s>\n" "$COMP_TYPE"
+ printf "COMP_POINT: <%s>\n" "$COMP_POINT"
+ printf "COMP_KEY: <%s>\n" "$COMP_KEY"
+ printf "COMP_CWORD: <%s>\n" "$COMP_CWORD"
+ printf "COMP_WORDS:\n"
+ printf "<%s>\n" "${COMP_WORDS[@]}"
+ printf "cur: <%s> prev: <%s>\n" "$cur" "$prev"
+fi
+
+for i in ${!COMP_WORDS[@]}; do # check if we already have an action registered
+ if [[ ${COMP_WORDS[i]} = @(create|add|del|test|destroy|list|save|restore|flush|rename|swap|help|version) ]]; then
+ if [[ ${COMP_WORDS[i]} != save ]]; then
+ got_action=1 str_action=${COMP_WORDS[i]}
+ break
+ else
+ if [[ ${COMP_WORDS[i-1]} != -o ]]; then
+ got_action=1 str_action=${COMP_WORDS[i]}
+ break
+ fi
+ fi
+ fi
+done
+
+# collect information about used options
+for ((i=1; i <= ${#COMP_WORDS[@]}; i++)); do
+ case "${COMP_WORDS[i]}" in
+ -\!) ignore_errors=1 ;;
+ -f) use_file=1 ;;
+ -n) names_only=1 ;;
+ -t) headers_only=1 ;;
+ -o) save_format=1
+ if [[ $prev = -o ]]; then
+ save_format=2 # expecting opt-arg
+ elif [[ ${COMP_WORDS[i+1]} = save ]]; then
+ save_format=3 # no -n/-t with -o save
+ fi
+ ;;
+ -r|-s) res_sort=1 ;;
+ esac
+done
+
+# invalid combination of options
+if ((names_only && headers_only)); then
+ COMPREPLY=()
+ return 0
+elif ((names_only || headers_only)); then
+ if ((res_sort || ignore_errors)) || ((save_format == 3)); then
+ COMPREPLY=()
+ return 0
+ fi
+elif ((ignore_errors)); then
+ if ((res_sort || save_format)); then
+ COMPREPLY=()
+ return 0
+ fi
+fi
+
+# for help or create, find supported set types and save them into an array
+if [[ $str_action = @(help|create) ]]; then i=0
+ while read -r; do
+ [[ $REPLY = "Supported set types:"* ]] && ((!i)) && i=1 && continue
+ ((i)) || continue
+ if [[ $REPLY = *:* ]]; then
+ set -- $REPLY
+ arr_types[${#arr_types[@]}]="$1"
+ fi
+ done < <(ipset help)
+ for i in ${!arr_types[@]}; do # remove dupe entries
+ for ((x=i+1; x <= ${#arr_types[@]}; x++)); do
+ if [[ ${arr_types[i]} = ${arr_types[x]} ]]; then
+ unset arr_types[x]
+ fi
+ done
+ done
+fi
+
+case "$cur" in
+ -*) # any option is requested
+ if ((save_format == 2)); then
+ COMPREPLY=()
+ return 0
+ fi
+ case "$prev" in
+ create|add|del|test|rename|swap) # -option not expected
+ COMPREPLY=()
+ return 0
+ ;;
+ \<|\>|-f) compopt -o nospace # expecting filenames as completion
+ COMPREPLY=( $( compgen -f -- $cur ) )
+ return 0
+ ;;
+ -) COMPREPLY=() # interactive mode
+ return 0
+ ;;
+ esac
+ if ((got_action)); then
+ case "$str_action" in
+ create|add|del)
+ COMPREPLY=( -\! -q )
+ for ((i=1; i <= ${#COMP_WORDS[@]}; i++)); do
+ if [[ $str_action = ${COMP_WORDS[i]} && ${COMP_WORDS[i+2]} = -* ]]; then
+ COMPREPLY=() # option not expected, want command value
+ break
+ fi
+ done
+ ;;
+ destroy|flush) COMPREPLY=( -q ) ;;
+ list)
+ if ((names_only || headers_only)); then
+ COMPREPLY=( -f -o -q )
+ elif ((res_sort)); then
+ COMPREPLY=( -f -o -q -r -s )
+ elif ((save_format == 1)); then
+ COMPREPLY=( -f -q -r -s -t )
+ elif ((save_format == 3)); then
+ COMPREPLY=( -f -q -r -s )
+ else
+ COMPREPLY=( -f -n -o -q -r -s -t )
+ fi
+ ;;
+ restore) COMPREPLY=( -\! -f -q ) ;;
+ save) COMPREPLY=( -f -q ) ;;
+ rename|swap|test) COMPREPLY=( -q )
+ for ((i=1; i <= ${#COMP_WORDS[@]}; i++)); do
+ if [[ $str_action = ${COMP_WORDS[i]} && ${COMP_WORDS[i+2]} = -* ]]; then
+ COMPREPLY=() # option not expected, want command value
+ break
+ fi
+ done
+ ;;
+ help|version) COMPREPLY=()
+ return 0
+ ;;
+ esac
+ else COMPREPLY=( - -\! -f -n -o -q -r -s -t )
+ if ((names_only || headers_only)) && ((save_format == 1)); then
+ COMPREPLY=( -f -q )
+ elif ((names_only || headers_only)); then
+ COMPREPLY=( -f -o -q )
+ elif ((res_sort)); then
+ COMPREPLY=( -f -o -q -r -s )
+ elif ((save_format == 1)); then
+ COMPREPLY=( -f -q -r -s -t )
+ elif ((save_format == 3)); then
+ COMPREPLY=( -f -q -r -s )
+ elif ((ignore_errors)); then
+ COMPREPLY=( -f -q )
+ elif ((use_file)); then
+ COMPREPLY=( -\! -n -o -q -r -s -t )
+ fi
+ fi
+ ;;
+ \<|\>) # redirection operator
+ compopt -o nospace
+ COMPREPLY=( $( compgen -f ) ) # no $cur, so completion starts without space after redirection
+ return 0
+ ;;
+ \$\{*) # variables with a leading `${'
+ COMPREPLY=( $(compgen -v -P '${' -S '}' ${cur#??}) )
+ return 0
+ ;;
+ \$*) # variables with a leading `$'
+ COMPREPLY=( $(compgen -v -P '$' ${cur#?} ) )
+ return 0
+ ;;
+ *) # not an option
+ if ((got_action)); then
+ arr_sets=( $(ipset list -n ) )
+ else
+ COMPREPLY=( $( compgen -W 'create add del test destroy list save restore flush rename swap help version' -- $cur ) )
+ fi
+ case "$prev" in # depend on previous option
+ restore) COMPREPLY=( \< )
+ for ((i=1; i <= ${#COMP_WORDS[@]}; i++)); do
+ if [[ ${COMP_WORDS[i]} = -f ]]; then
+ COMPREPLY=() # don't show redirector if we have option -f
+ break
+ fi
+ done
+ return 0
+ ;;
+ create|version) COMPREPLY=()
+ return 0
+ ;;
+ add|del|destroy|rename|swap|test)
+ COMPREPLY=( $( compgen -W '${arr_sets[@]}' -- $cur ) )
+ return 0
+ ;;
+ save)
+ if [[ $str_action = save ]]; then
+ COMPREPLY=( $( compgen -W '${arr_sets[@]}' -- $cur ) )
+ else
+ if ((save_format == 3)); then
+ COMPREPLY=( $( compgen -W 'list' -- $cur ) )
+ fi
+ fi
+ ;;
+ list) COMPREPLY=( $( compgen -W '${arr_sets[@]}' -- $cur ) )
+ return 0
+ ;;
+ help) COMPREPLY=( $( compgen -W '${arr_types[@]}' -- $cur ) )
+ return 0
+ ;;
+ -o)
+ if ((names_only || headers_only)); then
+ COMPREPLY=( $( compgen -W 'plain xml' -- $cur ) )
+ else
+ COMPREPLY=( $( compgen -W 'plain save xml' -- $cur ) )
+ fi
+ return 0
+ ;;
+ -f) compopt -o nospace
+ COMPREPLY=( $( compgen -f -- $cur ) )
+ return 0
+ ;;
+ \<|\>) compopt -o nospace
+ COMPREPLY=( $( compgen -f -- $cur ) )
+ return 0
+ ;;
+ -) COMPREPLY=() # interactive mode
+ return 0
+ ;;
+ *)
+ if ((got_action)); then
+ COMPREPLY=( $( compgen -W '${arr_sets[@]}' -- $cur ) )
+ for ((i=1; i <= ${#COMP_WORDS[@]}; i++)); do
+ case "${COMP_WORDS[i]}" in
+ add)
+ for ((x=${COMP_WORDS[i+1]}; x <= ${#COMP_WORDS[@]}; x++)); do
+ if [[ ${COMP_WORDS[x]} = $prev ]]; then
+ COMPREPLY=() # only list sets after the action command
+ break 2
+ fi
+ done
+ break
+ ;;
+ create) COMPREPLY=()
+ if [[ ${COMP_WORDS[i+1]} = $prev ]]; then
+ COMPREPLY=( $( compgen -W '${arr_types[@]}' -- $cur ) )
+ fi
+ break
+ ;;
+ del) # complete members
+ if [[ ${COMP_WORDS[i+1]} = $prev ]]; then
+ while read -r; do
+ [[ $REPLY = Members:* ]] && in_list=1 && continue
+ ((in_list)) || continue
+ arr_members[${#arr_members[@]}]="$REPLY"
+ done < <(ipset list "$prev" 2>/dev/null)
+ COMPREPLY=( $( compgen -W '${arr_members[@]}' -- $cur ) )
+ else
+ COMPREPLY=()
+ fi
+ break
+ ;;
+ help)
+ if [[ ${COMP_WORDS[i+1]} ]]; then
+ COMPREPLY=() # don't go further than showing the set types
+ return 0
+ fi
+ break
+ ;;
+ restore) COMPREPLY=() # not a redirecton
+ break
+ ;;
+ swap)
+ for x in ${!arr_sets[@]}; do
+ if [[ ${arr_sets[x]} = ${COMP_WORDS[i+2]} ]]; then
+ COMPREPLY=() # only list two sets
+ break 2
+ fi
+ done
+ break
+ ;;
+ *)
+ for ((y=1; y <= ${#COMP_WORDS[@]}; y++)); do
+ [[ ${COMP_WORDS[y]} ]] || continue
+ for x in ${!arr_sets[@]}; do
+ if [[ ${arr_sets[x]} = ${COMP_WORDS[y]} ]]; then
+ COMPREPLY=() # list only one set
+ break 2
+ fi
+ done
+ done
+ ;;
+ esac
+ done
+ else # we don't have the action yet, check options to display appropiate actions
+ if ((save_format || names_only || headers_only)); then
+ COMPREPLY=( $( compgen -W 'list' -- $cur ) )
+ return 0
+ elif ((res_sort)); then
+ COMPREPLY=( $( compgen -W 'list save' -- $cur ) )
+ return 0
+ elif ((ignore_errors && use_file)); then
+ COMPREPLY=( $( compgen -W 'restore' -- $cur ) )
+ return 0
+ elif ((ignore_errors)); then
+ COMPREPLY=( $( compgen -W 'create add del restore' -- $cur ) )
+ return 0
+ elif ((use_file)); then
+ COMPREPLY=( $( compgen -W 'list save restore' -- $cur ) )
+ return 0
+ fi
+ fi
+ ;;
+ esac
+ ;;
+esac
+if ((${#COMPREPLY[@]})); then # post process the reply
+ for ((i=1; i <= ${#COMP_WORDS[@]}; i++)); do # remove dupe options
+ [[ ${COMP_WORDS[i]} = @(""|-) ]] && continue
+ for x in ${!COMPREPLY[@]}; do
+ if [[ ${COMP_WORDS[i]} = ${COMPREPLY[x]} ]]; then
+ unset COMPREPLY[$x]
+ break
+ fi
+ done
+ done
+else
+ _ipset_bash_default_compl "$cur"
+fi
+if [[ $DEBUG ]]; then
+ printf "COMPREPLY:\n"
+ printf "<%s>\n" "${COMPREPLY[@]}"
+fi
+}
+complete -F _ipset_complete ipset