How can I loop over possible completions?

I’d like to be able to get my hands on an array of possible completions for a given partial command. For example, the partial command service um has the following possible completions:

$ service um<TAB><TAB>
umountfs      umountnfs.sh  umountroot

I’d like to have a function completions with the following behavior:

$ for x in $(completions 'service um'); do
> echo $x
> done
umountfs
umountnfs.sh
umountroot

Partial progress: what I’ve learned so far

Here is an approach which I think can be made into a full answer. I’d definitely like to see that full answer, but given that the relatively simple <TAB><TAB> gives the same functionality in a non-programmatic way, it feels like there could also be a slicker solution.

I can find out that the completion mechanism for the service command is the _service function:

$ complete -p service
complete -F _service service

When this completion function _service is called, a bunch of environment variables are set (namely COMP_{LINE,POINT,KEY,TYPE,WORDS,CWORD}; see the bash man page), the function is given as arguments the command being completed, word being completed, and previous word, and it populates COMPREPLY with the possible completions. So my desired completions function could be defined something like this:

function completions() {
    # Produce an array of tokens in the input.
    read -a words <<< $1

    # Use "complete -p ${words[0]}" to determine how the 
    # completions are computed. This could be complicated
    # if complete is given flags other than -F.
    completion_func=???

    # Set all those COMP_* environment variables appropriately.

    # Run the function to populate COMPREPLY. This version
    # assumes words has length at least 2, but that can be
    # fixed.
    $completion_func ${words[0]} ${words[-1]} ${words[-2]}

    echo ${COMPREPLY[@]}
}

Aside from the complexity relative over <TAB><TAB>, a drawback of this approach is that it changes the environment.

Here’s a rudimentary function that I think can serve as starting point. It can fail in several ways, hopefully others here can improve:

completions () (
    if [ -f /usr/share/bash-completion/bash_completion ]; then
        . /usr/share/bash-completion/bash_completion
    elif [ -f /etc/bash_completion ]; then
        . /etc/bash_completion
    fi

    IFS="$COMP_WORDBREAKS" read -a words <<<"$1"
    complete_setting=($(complete -p "${words[0]}"))
    complete_optstring=":abcdefgjksuvprDEo:A:G:W:F:C:X:P:S:"
    while getopts "$complete_optstring" option "${complete_setting[@]:1}"
    do
        case $option in
            F) complete_functon="$OPTARG"
                ;;
            *) # Run around screaming!                    
                ;;
        esac
    done
    COMP_WORDS=("${words[@]}")
    COMP_LINE="$1"
    COMP_POINT="${#COMP_LINE}"
    COMP_CWORD=$((${#COMP_WORDS[@]} - 1))
    "$complete_functon" 
    printf "%sn" "${COMPREPLY[@]}"
)

Notes:

  • If you’re using this as a function for your interactive shell, the initial sourcing isn’t needed.
  • complete looks for words split using COMP_WORDBREAKS, so we set IFS to it for read.
  • complete -p prints out the current completion setting in a reusable way, so we can go about parsing the options the way it would.
  • The function uses a subshell (() instead of {}), so your current environment shouldn’t be disturbed.
Answered By: muru
Categories: Answers Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.