# shellcheck shell=bash
# udevadm(8) completion                                   -*- shell-script -*-
# SPDX-License-Identifier: LGPL-2.1-or-later
#
# This file is part of eudev, adapted from the one of systemd.
#
# Copyright © 2010 Ran Benita
#
# eudev is free software; you can redistribute it and/or modify it
# under the terms of the GNU Lesser General Public License as published by
# the Free Software Foundation; either version 2.1 of the License, or
# (at your option) any later version.
#
# eudev 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 Lesser General Public License
# along with eudev; If not, see <https://www.gnu.org/licenses/>.

__contains_word () {
    local w word=$1; shift
    for w in "$@"; do
        [[ $w = "$word" ]] && return
    done
}

__get_all_sysdevs() {
    local -a devs=(/sys/bus/*/devices/*/ /sys/class/*/*/)
    printf '%s\n' "${devs[@]%/}"
}

__get_all_sysnames() {
    local -a devs=(/sys/bus/*/devices/*/ /sys/class/*/*/)
    devs=("${devs[@]%/}")
    printf '%s\n' "${devs[@]##*/}" | sort -u
}

__get_all_device_nodes() {
    find /dev -xtype b -o -xtype c
}

__get_all_devices() {
    __get_all_sysdevs
    __get_all_device_nodes
}

__get_root() {
    local i

    for ((i=0; i < COMP_CWORD; i++)); do
        if [[ "${COMP_WORDS[i]}" = --root=* ]]; then
            echo "${COMP_WORDS[i]#--root=}"
            break
        fi
        if (( i > 0 )) && [[ "${COMP_WORDS[i-1]}" == "--root" ]]; then
            # "--root=PATH" is split into "--root", "=" and "PATH" if COMP_WORDBREAKS contains "="
            if [[ "${COMP_WORDS[i]}" == = ]]; then
                (( i + 1 < COMP_CWORD )) && echo "${COMP_WORDS[i+1]}"
            else
                echo "${COMP_WORDS[i]}"
            fi
            break
        fi
    done
}

__get_udev_rules_files() {
    local root=$( __get_root )

    ls "$root"/lib/udev/rules.d/*.rules \
       "$root"/usr/lib/udev/rules.d/*.rules \
       "$root"/usr/local/lib/udev/rules.d/*.rules \
       "$root"/run/udev/rules.d/*.rules \
       "$root"/etc/udev/rules.d/*.rules 2>/dev/null
}

__get_udev_rules_names() {
    local -a rules=( $( __get_udev_rules_files ) )
    printf '%s\n' "${rules[@]##*/}" | sort -u
}

_udevadm() {
    local i verb verb_i comps builtin command optval
    local cur=${COMP_WORDS[COMP_CWORD]} prev=${COMP_WORDS[COMP_CWORD-1]}

    # COMP_WORDBREAKS contains "=" by default, which splits "--opt=value" into "--opt", "=" and "value":
    # complete the value like the argument after "--opt", also when the cursor is right after "=";
    # "-o=value" is split the same way, see below
    if [[ $cur == = && $prev == -?* ]]; then
        cur=''
        optval=1
    elif [[ $prev == = ]] && (( COMP_CWORD >= 2 )) && [[ ${COMP_WORDS[COMP_CWORD-2]} == -?* ]]; then
        prev=${COMP_WORDS[COMP_CWORD-2]}
        optval=1
    fi
    local -A OPTS=(
        [COMMON]='-h --help -V --version'
        [DEBUG]='-d --debug'
        [INFO_STANDALONE]='-r --root -a --attribute-walk -t --tree -x --export -e --export-db -c --cleanup-db
                           -w --wait-for-initialization --value --no-pager --initialized-match --initialized-nomatch'
        [INFO_ARG]='-q --query -p --path -n --name -P --export-prefix -d --device-id-of-file --property
                    --json --subsystem-match --subsystem-nomatch --attr-match --attr-nomatch --property-match
                    --tag-match --sysname-match --name-match --parent-match'
        [TRIGGER_STANDALONE]='-v --verbose -n --dry-run -q --quiet -w --settle --wait-daemon --uuid
                              --initialized-match --initialized-nomatch --include-parents'
        [TRIGGER_ARG]='-t --type -c --action -s --subsystem-match -S --subsystem-nomatch
                       -a --attr-match -A --attr-nomatch -p --property-match
                       -g --tag-match -y --sysname-match --name-match -b --parent-match
                       --prioritized-subsystem'
        [SETTLE]='-t --timeout -E --exit-if-exists'
        [CONTROL_STANDALONE]='-e --exit -s --stop-exec-queue -S --start-exec-queue -R --reload --ping --revert'
        [CONTROL_ARG]='-l --log-level --log-priority -p --property -m --children-max -t --timeout --trace'
        [MONITOR_STANDALONE]='-k --kernel -u --udev -p --property'
        [MONITOR_ARG]='-s --subsystem-match -t --tag-match'
        [HWDB_STANDALONE]='-u --update -s --strict --usr'
        [HWDB_ARG]='-o --output -t --test -r --root'
        [TEST_STANDALONE]='-v --verbose'
        [TEST_ARG]='-a --action -N --resolve-names -D --extra-rules-dir --json'
        [TEST_BUILTIN]='-a --action'
        [VERIFY_STANDALONE]='--no-summary --no-style'
        [VERIFY_ARG]='-N --resolve-names --root'
        [CAT_STANDALONE]='--tldr --config'
        [CAT_ARG]='--root'
        [WAIT_STANDALONE]='--removed --settle'
        [WAIT_ARG]='-t --timeout --initialized'
        [LOCK_STANDALONE]='-p --print'
        [LOCK_ARG]='-t --timeout -d --device -b --backing'
    )

    local verbs=(help info trigger settle control monitor hwdb test-builtin test verify cat wait lock version)
    # the options of each command that take an argument
    local -A ARG_OPTS=(
        [info]=INFO_ARG [trigger]=TRIGGER_ARG [settle]=SETTLE [control]=CONTROL_ARG [monitor]=MONITOR_ARG
        [hwdb]=HWDB_ARG [test]=TEST_ARG [test-builtin]=TEST_BUILTIN [verify]=VERIFY_ARG [cat]=CAT_ARG
        [wait]=WAIT_ARG [lock]=LOCK_ARG
    )
    local builtins=(blkid btrfs factory_reset hwdb input_id keyboard kmod net_driver net_id path_id tpm2_id usb_id)

    for ((i=0; i < COMP_CWORD; i++)); do
        if __contains_word "${COMP_WORDS[i]}" "${verbs[@]}"; then
            verb=${COMP_WORDS[i]}
            verb_i=$i
            break
        fi
    done

    # no value for "--opt=" if --opt takes none, or only an optional one, e.g. --wait-for-initialization=SECS,
    # and none for "-o=", which is -o with the argument "=...", or with no argument followed by the option -=,
    # e.g. "udevadm -d=", not a command after -d
    if [[ -n $optval ]] && { [[ $prev != --?* || -z ${verb-} || -z ${ARG_OPTS[$verb]-} ]] ||
                             ! __contains_word "$prev" ${OPTS[${ARG_OPTS[$verb]}]}; }; then
        COMPREPLY=()
        return 0
    fi

    if [[ -z ${verb-} ]]; then
        if [[ "$cur" = -* ]]; then
            COMPREPLY=( $(compgen -W '${OPTS[COMMON]} ${OPTS[DEBUG]}' -- "$cur") )
        else
            COMPREPLY=( $(compgen -W '${verbs[*]}' -- "$cur") )
        fi
        return 0
    fi

    case $verb in
        'info')
            if __contains_word "$prev" ${OPTS[INFO_ARG]}; then
                case $prev in
                    -q|--query)
                        comps='name symlink path property all'
                        ;;
                    -p|--path)
                        comps=$( __get_all_sysdevs )
                        local IFS=$'\n'
                        ;;
                    -n|--name)
                        comps=$( __get_all_device_nodes )
                        ;;
                    -d|--device-id-of-file)
                        comps=$( compgen -A file -- "$cur" )
                        ;;
                    --json)
                        comps=$( udevadm info --json=help 2>/dev/null )
                        ;;
                    --parent-match)
                        comps=$( __get_all_sysdevs )
                        local IFS=$'\n'
                        ;;
                    --name-match)
                        comps=$( __get_all_device_nodes )
                        ;;
                    --sysname-match)
                        comps=$( __get_all_sysnames )
                        local IFS=$'\n'
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            if [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[INFO_STANDALONE]} ${OPTS[INFO_ARG]}"
            else
                comps=$( __get_all_devices )
                local IFS=$'\n'
            fi
            ;;

        'trigger')
            if __contains_word "$prev" ${OPTS[TRIGGER_ARG]}; then
                case $prev in
                    -t|--type)
                        comps='all devices subsystems'
                        ;;
                    -c|--action)
                        comps='add remove change move online offline bind unbind'
                        ;;
                    -y|--sysname-match)
                        comps=$( __get_all_sysnames )
                        local IFS=$'\n'
                        ;;
                    -b|--parent-match)
                        comps=$( __get_all_sysdevs )
                        local IFS=$'\n'
                        ;;
                    --name-match)
                        comps=$( __get_all_device_nodes )
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            if [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[TRIGGER_STANDALONE]} ${OPTS[TRIGGER_ARG]}"
            else
                comps=$( __get_all_devices )
                local IFS=$'\n'
            fi
            ;;

        'settle')
            if __contains_word "$prev" ${OPTS[SETTLE]}; then
                case $prev in
                    -E|--exit-if-exists)
                        comps=$( compgen -A file -- "$cur" )
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            comps="${OPTS[COMMON]} ${OPTS[SETTLE]}"
            ;;

        'control')
            if __contains_word "$prev" ${OPTS[CONTROL_ARG]}; then
                case $prev in
                    -l|--log-level|--log-priority)
                        comps='alert crit debug emerg err info notice warning'
                        ;;
                    --trace)
                        comps='yes no'
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            comps="${OPTS[COMMON]} ${OPTS[CONTROL_STANDALONE]} ${OPTS[CONTROL_ARG]}"
            ;;

        'monitor')
            if __contains_word "$prev" ${OPTS[MONITOR_ARG]}; then
                case $prev in
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            comps="${OPTS[COMMON]} ${OPTS[MONITOR_STANDALONE]} ${OPTS[MONITOR_ARG]}"
            ;;

        'hwdb')
            if __contains_word "$prev" ${OPTS[HWDB_ARG]}; then
                case $prev in
                    -o|--output)
                        comps=$( compgen -A file -- "$cur" )
                        ;;
                    -r|--root)
                        comps=''
                        compopt -o dirnames
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            comps="${OPTS[COMMON]} ${OPTS[HWDB_STANDALONE]} ${OPTS[HWDB_ARG]}"
            ;;

        'test')
            if __contains_word "$prev" ${OPTS[TEST_ARG]}; then
                case $prev in
                    -a|--action)
                        comps='add remove change move online offline bind unbind'
                        ;;
                    -N|--resolve-names)
                        comps='early late never'
                        ;;
                    -D|--extra-rules-dir)
                        comps=''
                        compopt -o dirnames
                        ;;
                    --json)
                        comps=$( udevadm test --json help 2>/dev/null )
                        ;;
                esac
            elif [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[TEST_ARG]} ${OPTS[TEST_STANDALONE]}"
            else
                comps=$( __get_all_devices )
                local IFS=$'\n'
            fi
            ;;

        'test-builtin')
            if __contains_word "$prev" ${OPTS[TEST_BUILTIN]}; then
                case $prev in
                    -a|--action)
                        comps='add remove change move online offline bind unbind'
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            for ((i=0; i < COMP_CWORD; i++)); do
                if __contains_word "${COMP_WORDS[i]}" "${builtins[@]}"; then
                    builtin=${COMP_WORDS[i]}
                    break
                fi
            done

            if [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[TEST_BUILTIN]}"
            elif [[ -z $builtin ]]; then
                comps="${builtins[@]}"
            else
                comps=$( __get_all_devices )
                local IFS=$'\n'
            fi
            ;;

        'verify')
            if __contains_word "$prev" ${OPTS[VERIFY_ARG]}; then
                case $prev in
                    -N|--resolve-names)
                        comps=$( udevadm verify --resolve-names help 2>/dev/null )
                        ;;
                    --root)
                        comps=''
                        compopt -o dirnames
                        ;;
                    *)
                        comps=''
                        ;;
                esac
            elif [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[VERIFY_ARG]} ${OPTS[VERIFY_STANDALONE]}"
            elif [[ $cur = */* ]]; then
                comps=$( __get_udev_rules_files )
                compopt -o dirnames
            else
                comps=$( __get_udev_rules_names )
                compopt -o default
            fi
            ;;

        'cat')
            if __contains_word "$prev" ${OPTS[CAT_ARG]}; then
                case $prev in
                    --root)
                        comps=''
                        compopt -o dirnames
                        ;;
                    *)
                        comps=''
                        ;;
                esac
            elif [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[CAT_ARG]} ${OPTS[CAT_STANDALONE]}"
            elif __contains_word "--config" ${COMP_WORDS[*]}; then
                comps="${OPTS[COMMON]} ${OPTS[CAT_ARG]} ${OPTS[CAT_STANDALONE]}"
            elif [[ $cur = */* ]]; then
                comps=$( __get_udev_rules_files )
                compopt -o dirnames
            else
                comps=$( __get_udev_rules_names )
                compopt -o default
            fi
            ;;

        'wait')
            if __contains_word "$prev" ${OPTS[WAIT_ARG]}; then
                case $prev in
                    --initialized)
                        comps='yes no'
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            fi

            if [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[WAIT_STANDALONE]} ${OPTS[WAIT_ARG]}"
            else
                comps=$( __get_all_devices )
                local IFS=$'\n'
            fi
            ;;

        'lock')
            # the first word that is neither an option nor its argument is the command to run, and
            # the rest are its arguments
            for ((i=verb_i+1; i < COMP_CWORD; i++)); do
                # "=" and the value after it, of an "--opt=value" that COMP_WORDBREAKS split
                [[ ${COMP_WORDS[i]} == = || ${COMP_WORDS[i-1]} == = ]] && continue
                if [[ ${COMP_WORDS[i]} != -* ]] && ! __contains_word "${COMP_WORDS[i-1]}" ${OPTS[LOCK_ARG]}; then
                    command=${COMP_WORDS[i]}
                    break
                fi
            done

            if [[ -n $command ]]; then
                comps=''
                compopt -o default
            elif __contains_word "$prev" ${OPTS[LOCK_ARG]}; then
                case $prev in
                    -d|--device)
                        comps=$( find /dev -xtype b )
                        ;;
                    -b|--backing)
                        comps=$( compgen -A file -- "$cur" )
                        ;;
                    *)
                        comps=''
                        ;;
                esac
                COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
                return 0
            elif [[ $cur = -* ]]; then
                comps="${OPTS[COMMON]} ${OPTS[LOCK_STANDALONE]} ${OPTS[LOCK_ARG]}"
            elif __contains_word "-p" "${COMP_WORDS[@]}" || __contains_word "--print" "${COMP_WORDS[@]}"; then
                # no command with --print
                comps=''
            else
                comps=''
                compopt -o default
            fi
            ;;

        'help'|'version')
            # no arguments
            comps=''
            ;;

        *)
            comps=${verbs[*]}
            ;;
    esac

    COMPREPLY=( $(compgen -W '$comps' -- "$cur") )
    return 0
}

complete -F _udevadm udevadm
