# (C) 2010-2019 magicant

# This file is autoloaded when completion is first performed by the shell.
# This file contains utility functions that are commonly used by many
# completion functions.


# This is the function that is called when completing a command argument.
if ! command -vf completion//argument >/dev/null 2>&1; then
        function completion//argument {
                command -f completion//reexecute -f
        }
fi


# This function parses array $WORDS and variable $TARGETWORD according to array
# $OPTIONS and modifies $WORDS so that the caller can easily parse $WORDS.
#
# $WORDS is an array containing command line words that have been already
# entered by the user before the word being completed. The first word of $WORDS
# is considered the command name and ignored. The other words are considered
# arguments to the command and parsed.
#
# $TARGETWORD is a variable containing the word being completed.
#
# $OPTIONS is an array containing data about parsed options. The value of each
# element must follow the following syntax.
#   element     := optionlist
#                | optionlist ";" description
#   optionlist  := option
#                | optionlist " " option
#   option      := optionvalue
#                | optionvalue ":"
#                | optionvalue "::"
#   optionvalue := character
#                | "-" characters
# An element value consists of an option list that is possibly followed by a
# description of the options. The option list and the description is separated
# by a semicolon and the description may contain any characters.
# The option list consists of any number of options separated by whitespaces.
# An option is an option value possibly followed by one or two colons.
# An option that takes no, mandatory, and optional argument should be specified
# with zero, one, and two colons, respectively.
# The option value may be either a single character or a character string
# preceded by one or more hyphens. Note that a single-character option must be
# specified without a hyphen though it is preceded by a hyphen on the command
# line. Also note that multiple single-character options can be combined
# together after one hyphen on the command line. Long options, on the other
# hand, must be specified including their preceding hyphen(s).
#
# The following options can be specified to this function:
#   -e  Without -e, options are parsed only before the first operand word in
#       $WORDS. Words after the first operand word are all considered operands.
#       With -e, options are parsed for any words in $WORDS (except after
#       "--"). Options and operands can be intermixed.
#   -n  Without -n, array $WORDS is updated to reflect the parse result.
#       Multiple single-character options after one hyphen are separated and
#       separate option arguments are combined with the options so that the
#       option and its argument are in the same command line word. When -s is
#       specified or -e is not specified, options and operands in $WORDS are
#       separated by "--".
#       With -n, $WORDS is left intact.
#   -s  Only meaningful when with -e and without -n.
#       Without -s, words in $WORDS are not reordered.
#       With -s, words in $WORDS are reordered so that all options come
#       before operands.
#
# After words in $WORDS are parsed, $TARGETWORD is parsed as well and it is
# determined how $TARGETWORD should be completed. The results are returned as
# two variables $ARGOPT and $PREFIX.
# If $TARGETWORD is an operand to the command, $ARGOPT and $PREFIX are empty
# strings. If $TARGETWORD is one or more single-character option, $ARGOPT is
# "-" and $PREFIX is $TARGETWORD. If $TARGETWORD is a long option or "-",
# $ARGOPT is "-" and $PREFIX is an empty string. If $TARGETWORD is an option
# followed by an argument to that option or a separate option argument (in
# which case $TARGETWORD should be completed as an option argument), $ARGOPT
# is the name of that option (as specified by "optionvalue" in the syntax
# above) and $PREFIX is the part of $TARGETWORD that is not the argument.
#
# Note:
# Single-hyphened long options cannot be abbreviated.
# Optional option arguments are not supported for single-hyphened long options.
# Ambiguous options, invalid options, etc. are silently ignored.
function completion//parseoptions {

        # parse options to this function itself
        typeset opt= OPTIND=1
        typeset extension=false update=true sort=false
        while getopts :ens opt; do
                case $opt in
                        (e) extension=true;;
                        (n) update=false;;
                        (s) sort=true;;
                esac
        done

        if [ "${WORDS+set}" != "set" ] ||
                        [ ${WORDS[#]} -le 0 ] ||
                        [ "${OPTIONS+set}" != "set" ] ||
                        [ "${TARGETWORD+set}" != "set" ]; then
                return 1
        fi

        # results
        typeset result options operands
        result=() options=() operands=()

        # parse $WORDS
        typeset index=1 nomoreoptions=false
        typeset matches
        ARGOPT= PREFIX=
        while index=$((index+1)); [ $index -le ${WORDS[#]} ]; do
                typeset word="${WORDS[index]}"
                case $word in
                (--)
                        result=("$result" "${WORDS[index,-1]}")
                        operands=("$operands" "${WORDS[index+1,-1]}")
                        nomoreoptions=true
                        break
                        ;;
                (--?*=*) # double-hyphened long option with argument
                        matches=()
                        for opt in ${OPTIONS%%;*}; do
                                case ${${opt%:}%:} in
                                ("${word%%=*}")
                                        matches=("$opt")
                                        break
                                        ;;
                                ("${word%%=*}"*)
                                        matches=("$matches" "$opt")
                                        ;;
                                esac
                        done
                        if [ ${matches[#]} -eq 1 ]; then
                                opt=${matches[1]}
                                case $opt in (*:) # option argument allowed
                                        opt=${${opt%:}%:} word=${word#*=}
                                        result=("$result" "$opt=$word")
                                        options=("$options" "$opt=$word")
                                esac
                        fi
                        ;;
                (--?*) # double-hyphened long option without argument
                        matches=()
                        for opt in ${OPTIONS%%;*}; do
                                case ${${opt%:}%:} in
                                ("$word")
                                        matches=("$opt")
                                        break
                                        ;;
                                ("$word"*)
                                        matches=("$matches" "$opt")
                                        ;;
                                esac
                        done
                        if [ ${matches[#]} -eq 1 ]; then
                                opt=${matches[1]}
                                case $opt in
                                (*[!:]:) # option argument required
                                        if [ $index -eq ${WORDS[#]} ]; then
                                                # $TARGETWORD is argument
                                                ARGOPT=${opt%:} # PREFIX=
                                                break
                                        else
                                                index=$((index+1))
                                                # ${WORDS[index]} is argument
                                                result=("$result" "${opt%:}=${WORDS[index]}")
                                                options=("$options" "${opt%:}=${WORDS[index]}")
                                        fi
                                        ;;
                                (*) # no option argument
                                        result=("$result" "${opt%::}")
                                        options=("$options" "${opt%::}")
                                        ;;
                                esac
                        fi
                        ;;
                (-?*) # single-hyphened option
                        # first check for single-hyphened long option
                        for opt in ${OPTIONS%%;*}; do
                                case ${${opt%:}%:} in ("${word%%=*}")
                                        case $opt in
                                        (*::) # optional argument not supported
                                                ;;
                                        (*:) # requires option argument
                                                case $word in
                                                (*=*)
                                                        result=("$result" "$word")
                                                        options=("$options" "$word")
                                                        ;;
                                                (*) # argument is next word
                                                        if [ $index -eq ${WORDS[#]} ]; then
                                                                # $TARGETWORD is argument
                                                                ARGOPT=${opt%:} # PREFIX=
                                                                break 2
                                                        else
                                                                index=$((index+1))
                                                                # ${WORDS[index]} is argument
                                                                result=("$result" "${opt%:}=${WORDS[index]}")
                                                                options=("$options" "${opt%:}=${WORDS[index]}")
                                                        fi
                                                        ;;
                                                esac
                                                ;;
                                        (*) # no option argument
                                                case $word in
                                                (*=*) # Bad!
                                                        ;;
                                                (*) # OK!
                                                        result=("$result" "$opt")
                                                        options=("$options" "$opt")
                                                        ;;
                                                esac
                                                ;;
                                        esac
                                        continue 2
                                esac
                        done
                        # Next, check for single-character options
                        while word=${word#?}; [ "$word" ]; do
                                for opt in ${OPTIONS%%;*}; do
                                        case $opt in
                                        ("${word[1]}") # no option argument
                                                result=("$result" "-$opt")
                                                options=("$options" "-$opt")
                                                ;;
                                        ("${word[1]}":) # requires option argument
                                                if [ "${word#?}" ]; then
                                                        # rest of $word is argument
                                                        result=("$result" "-$word")
                                                        options=("$options" "-$word")
                                                elif [ $index -eq ${WORDS[#]} ]; then
                                                        # $TARGETWORD is argument
                                                        ARGOPT=${opt%:} # PREFIX
                                                        break 3
                                                else
                                                        index=$((index+1))
                                                        # ${WORDS[index]} is argument
                                                        result=("$result" "-${opt%:}${WORDS[index]}")
                                                        options=("$options" "-${opt%:}${WORDS[index]}")
                                                fi
                                                break 2
                                                ;;
                                        ("${word[1]}"::) # optional option argument
                                                result=("$result" "-$word")
                                                options=("$options" "-$word")
                                                break 2
                                                ;;
                                        esac
                                done
                        done
                        ;;
                (*) # operand
                        if $extension; then
                                result=("$result" "$word")
                                operands=("$operands" "$word")
                        else
                                result=("$result" "${WORDS[index,-1]}")
                                operands=("$operands" "${WORDS[index,-1]}")
                                nomoreoptions=true
                                break
                        fi
                        ;;
                esac
        done

        # determine if $TARGETWORD should be completed as an option
        if ! $nomoreoptions && [ -z "$ARGOPT" ]; then
                case $TARGETWORD in
                (--*=*)
                        matches=()
                        for opt in ${OPTIONS%%;*}; do
                                case ${${opt%:}%:} in
                                ("${TARGETWORD%%=*}")
                                        matches=("$opt")
                                        break
                                        ;;
                                ("${TARGETWORD%%=*}"*)
                                        matches=("$matches" "$opt")
                                        ;;
                                esac
                        done
                        if [ ${matches[#]} -eq 1 ]; then
                                opt=${matches[1]}
                                case $opt in (*:) # option argument allowed
                                        ARGOPT=${${opt%:}%:}
                                        PREFIX=${TARGETWORD%%=*}=
                                esac
                        fi
                        ;;
                (-|--*)
                        ARGOPT=- # PREFIX=
                        ;;
                (-*)
                        ARGOPT=- # PREFIX=
                        # first check for single-hyphened long option
                        typeset long=false
                        for opt in ${OPTIONS%%;*}; do
                                case ${opt%:} in ("${TARGETWORD%%=*}"*)
                                        long=true
                                        case $TARGETWORD in (*=*)
                                                if [ "${TARGETWORD%%=*}" = "${opt%:}" ]; then
                                                        ARGOPT=${opt%:}
                                                        PREFIX=${TARGETWORD%%=*}=
                                                        break
                                                fi
                                        esac
                                esac
                        done
                        # Next, check for single-character options
                        if ! $long; then
                                typeset word="$TARGETWORD"
                                while word=${word#?}; [ "$word" ]; do
                                        for opt in ${OPTIONS%%;*}; do
                                                case $opt in
                                                ("${word[1]}")
                                                        result=("$result" "-$opt")
                                                        options=("$options" "-$opt")
                                                        ;;
                                                ("${word[1]}":|"${word[1]}"::)
                                                        ARGOPT=${${opt%:}%:}
                                                        PREFIX=${TARGETWORD%${word#?}}
                                                        break 2
                                                        ;;
                                                esac
                                        done
                                done
                                if [ -z "$word" ]; then
                                        PREFIX=$TARGETWORD
                                fi
                        fi
                        ;;
                esac
        fi

        # update $WORDS
        if $update; then
                if ! $extension || $sort; then
                        WORDS=("${WORDS[1]}" "$options" -- "$operands")
                else
                        WORDS=("${WORDS[1]}" "$result")
                fi
        fi

}


# This function calls the "complete" built-in to generate candidates to
# complete $TARGETWORD as an option. Candidates are generated according to
# array $OPTIONS specifying options in the same syntax as the
# "completion//parseoptions" function above. If more than one option matches
# $TARGETWORD, only the first match is used to generate the candidate.
#
# This function accepts either of the -s and -S options. With -s, this function
# completes single-character options only (in which case $TARGETWORD may have
# other single-character options already entered). With -S, $TARGETWORD is
# completed as a single option. These options are mutually exclusive. If
# specified both, the last specified one is effective. If specified neither,
# it defaults to whether $PREFIX is empty or not.
#
# Normally, a long option that takes an argument is completed with an `=' sign.
# But if you specify the -e option, the option is completed with a space like a
# normal word.
#
# The exit status of this function is 0 if at least one candidate was
# generated. Otherwise, the exit status is 1.
#
# If variable $ARGOPT is defined but its value is not "-" or if $TARGETWORD
# does not start with a hyphen, this function does nothing but returning the
# exit status of 2 unless the -f option is specified.
function completion//completeoptions {

        # check $PREFIX
        if [ "${PREFIX-}" ]; then
                typeset single=true
        else
                typeset single=false
        fi

        # parse options to this function itself
        typeset opt= OPTIND=1 longargeq=true force=false
        while getopts :efsS opt; do
                case $opt in
                        (e) longargeq=;;
                        (f) force=true;;
                        (s) single=true;;
                        (S) single=false;;
                esac
        done

        if ! $force; then
                if [ "${OPTIONS+set}" != "set" ] ||
                                [ "${TARGETWORD+set}" != "set" ] ||
                                [ "${ARGOPT--}" != "-" ]; then
                        return 2
                fi
                case $TARGETWORD in
                        (-*) ;;
                        (*)  return 2 ;;
                esac
        fi

        # generate candidates
        typeset opts desc
        typeset generated=false
        for opts in "$OPTIONS"; do

                # get description
                case $opts in
                (*\;*)
                        desc=${opts#*;}
                        while true; do  # trim surrounding spaces
                                case $desc in
                                ([[:space:]]*) desc=${desc#[[:space:]]} ;;
                                (*[[:space:]]) desc=${desc%[[:space:]]} ;;
                                (*)            break ;;
                                esac
                        done
                        ;;
                (*)
                        desc=
                        ;;
                esac

                if $single; then
                        # generate single-character option
                        for opt in ${opts%%;*}; do
                                case $opt in
                                ([!-]|[!-]:)
                                        complete -O -P "$TARGETWORD" ${desc:+-D "$desc"} "${opt%:}" &&
                                        generated=true
                                        break
                                        ;;
                                ([!-]::)
                                        complete -OT -P "$TARGETWORD" ${desc:+-D "$desc"} "${opt%::}" &&
                                        generated=true
                                        break
                                        ;;
                                esac
                        done
                else
                        # generate candidate for first match
                        for opt in ${opts%%;*}; do
                                case $opt in
                                (-*:) # long option that takes argument
                                        case ${${opt%:}%:} in ("$TARGETWORD"*)
                                                complete ${desc:+-D "$desc"} ${longargeq:+-T -S =} -O -- "${${opt%:}%:}" &&
                                                generated=true
                                                break
                                        esac
                                        ;;
                                (-*) # long option that does not take argument
                                        case $opt in ("$TARGETWORD"*)
                                                complete ${desc:+-D "$desc"} -O -- "$opt" &&
                                                generated=true
                                                break
                                        esac
                                        ;;
                                (?::) # single-char option w/ optional argument
                                        if [ "$TARGETWORD" = "-" ]; then
                                                complete ${desc:+-D "$desc"} -OT -- "-${opt%::}" &&
                                                generated=true
                                                break
                                        fi
                                        ;;
                                (?|?:) # other single-char option
                                        if [ "$TARGETWORD" = "-" ]; then
                                                complete ${desc:+-D "$desc"} -O -- "-${opt%:}" &&
                                                generated=true
                                                break
                                        fi
                                        ;;
                                esac
                        done
                fi
        done

        # return 0 if at least one candidate was generated
        $generated

}


# This function removes one or more elements from the $WORDS array.
# When this function is called, the array is supposed to contain the following
# elements (in this order):
#  * a command name word
#  * any number of options that start with a hyphen
#  * a "--" separator (optional)
#  * any number of operands
# This function removes any words other than operands.
function completion//getoperands {

        typeset i=2
        while [ $i -le ${WORDS[#]} ]; do
                case ${WORDS[i]} in
                (--)
                        i=$((i+1))
                        break
                        ;;
                (-?*)
                        i=$((i+1))
                        ;;
                (*)
                        break
                        ;;
                esac
        done
        WORDS=("${WORDS[i,-1]}")

}


# This function re-executes the completion function using the current $WORDS.
# If the -e option is specified, only external commands are completed as the
# command name.
# If the -f option is specified, the following fall-back functions are not used:
#   * completion//command
#   * completion//argument
# If an operand is given to this function, it is used as the command name
# instead of ${WORDS[1]} and the -f option is assumed.
# This function does not change $WORDS (but the completion function may do).
function completion//reexecute {

        # parse options
        typeset opt= OPTIND=1 extonly=false fallback=true
        while getopts :ef opt; do
                case $opt in
                        (e) extonly=true;;
                        (f) fallback=false;;
                esac
        done

        # if there's no words, complete the command name
        if [ ${WORDS[#]} -le 0 ]; then
                if $fallback && command -vf completion//command >/dev/null 2>&1; then
                        unset opt OPTIND extonly fallback
                        command -f completion//command
                else
                        complete -P "${PREFIX-}" -T -S / -d
                        case ${TARGETWORD#"${PREFIX-}"} in
                        (*/*)
                                complete -P "${PREFIX-}" --executable-file
                                ;;
                        (*)
                                if $extonly; then
                                        complete -P "${PREFIX-}" --external-command
                                else
                                        complete -P "${PREFIX-}" -R '*/*' -ck --normal-alias
                                fi
                                ;;
                        esac
                fi
                return
        fi

        if [ $OPTIND -le $# ]; then
                set -- "${@[OPTIND]}"
                fallback=false
        else
                set -- "${WORDS[1]}"
        fi

        if $fallback && command -vf completion//argument >/dev/null 2>&1; then
                unset opt OPTIND extonly fallback
                command -f completion//argument
                return
        fi

        typeset compfunc= cmdbase
        if ! command -f completion//findcompfunc "$1"; then
                cmdbase="${1%.*}"
                if [ "$cmdbase" != "$1" ] && [ "${PATHEXT:+set}" = set ] &&
                        grep -iqx "${{PATHEXT//';'/'\|'}//'.'/'\.'}" <<< "${1#"$cmdbase"}" &&
                        command -f completion//findcompfunc "$cmdbase" &&
                        [ $OPTIND -gt $# ]; then
                        WORDS=("$cmdbase" "${WORDS[2,-1]}")
                fi
        fi
        set -- "$compfunc"
        unset opt OPTIND extonly fallback compfunc cmdbase

        if [ "$1" ]; then
                command -f "$1"
        else
                complete -P "${PREFIX-}" -f
        fi

}

# $1 = command name
# The result is set to the "compfunc" variable
function completion//findcompfunc
        if command -vf "completion/$1" >/dev/null 2>&1; then
                compfunc="completion/$1"
        elif command -vf "completion/${1##*/}" >/dev/null 2>&1; then
                compfunc="completion/${1##*/}"
        elif . -AL "completion/$1" 2>/dev/null
                        command -vf "completion/$1" >/dev/null 2>&1; then
                compfunc="completion/$1"
        elif command -vf "completion/${1##*/}" >/dev/null 2>&1; then
                compfunc="completion/${1##*/}"
        elif . -AL "completion/${1##*/}" 2>/dev/null
                        command -vf "completion/$1" >/dev/null 2>&1; then
                compfunc="completion/$1"
        elif command -vf "completion/${1##*/}" >/dev/null 2>&1; then
                compfunc="completion/${1##*/}"
        else
                compfunc=
                false
        fi


# Prints all commands in $PATH.
# Given an argument, it is treated as a pattern that filters results.
function completion//allcommands {
        typeset IFS

        # check if $PATH is an array
        case $(typeset -p PATH 2>/dev/null) in
        (PATH=\(*)
                ;;
        ('')
                typeset PATH
                PATH=()
                ;;
        (*)
                # re-define $PATH as local array
                typeset IFS=: savepath="$PATH"
                typeset PATH
                PATH=($savepath)
                ;;
        esac
        IFS=' '

        # use "find" if it supports "-min/maxdepth"
        if find -L . -mindepth 0 -maxdepth 0 -prune >&2; then
                find -L "$PATH" -mindepth 1 -maxdepth 1 \
                        ${1+-name "$1"} -type f -perm -u+x
                return
        fi 2>/dev/null

        typeset dir file pat="${1-*}" oldopt="$(set +o)"
        set -o glob -o dotglob -o nullglob -o caseglob -o markdirs
        for dir in "$PATH"; do
                case $dir in
                        (*/)
                                ;;
                        (*)
                                dir=$dir/ ;;
                esac
                for file in "$dir"*; do
                        case $file in
                                (*/.|*/..|*/)
                                        ;;
                                (*/$pat)
                                        if [ -x "$file" ]; then
                                                printf '%s\n' "$file"
                                        fi
                                        ;;
                        esac
                done
        done
        eval "$oldopt"
}


# The completion function for the "." built-in
if ! command -vf completion/. >/dev/null 2>&1; then
        function completion/. {
        # load the "_dot" file and call the "completion/." function in it
                . -AL completion/_dot && command -f completion/. "$@"
        }
fi


# vim: set ft=sh ts=8 sts=8 sw=8 et:
