You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

3.6 KiB

Recursive completion — call container's own bash completion

The idea: instead of reimplementing completion, delegate to bash inside the container and get back its own completion results.


The approach

_incus_exec_delegate() {
    local cur="${COMP_WORDS[COMP_CWORD]}"
    local container="${COMP_WORDS[2]}"

    # find '--' position
    local dashdash_pos=0
    local i
    for (( i=0; i < ${#COMP_WORDS[@]}; i++ )); do
        [[ "${COMP_WORDS[$i]}" == "--" ]] && dashdash_pos=$i
    done

    # complete container names
    if (( COMP_CWORD == 2 )); then
        COMPREPLY=($(compgen -W \
            "$(incus list --format csv --columns n 2>/dev/null)" \
            -- "$cur"))
        return
    fi

    # before '--'
    if (( dashdash_pos == 0 )); then
        COMPREPLY=($(compgen -W "-- --env --user --cwd" -- "$cur"))
        return
    fi

    # after '--' — delegate full completion to container's bash
    local inner_words=("${COMP_WORDS[@]:$((dashdash_pos + 1))}")
    local inner_cword=$(( COMP_CWORD - dashdash_pos - 1 ))
    local inner_cmd="${inner_words[0]}"
    local inner_line="${inner_words[*]}"

    # ask container's bash to run completion
    COMPREPLY=($(incus exec "$container" -- bash -c "
        # load bash completion
        source /usr/share/bash-completion/bash_completion 2>/dev/null
        source /etc/bash_completion 2>/dev/null

        # rebuild COMP_ variables inside container
        COMP_WORDS=(${inner_words[*]@Q})
        COMP_CWORD=$inner_cword
        COMP_LINE=${inner_line@Q}
        COMP_POINT=\${#COMP_LINE}

        # find and call the completion function for the command
        local compfunc
        compfunc=\$(complete -p ${inner_cmd@Q} 2>/dev/null \
            | grep -oP '(?<=-F )\S+')

        if [[ -n \"\$compfunc\" ]]; then
            \$compfunc ${inner_cmd@Q} ${cur@Q} ${inner_words[$((inner_cword - 1))]@Q}
            printf '%s\n' \"\${COMPREPLY[@]}\"
        else
            # fallback to file completion
            compgen -f -- ${cur@Q}
        fi
    " 2>/dev/null))
}

complete -F _incus_exec_delegate incus

How it works

host$ incus exec mycontainer -- git che<TAB>
                                 ↑
                         inner_cmd="git"
                                 ↓
        incus exec mycontainer -- bash -c '
            source /usr/share/bash-completion/bash_completion
            COMP_WORDS=(git che)
            COMP_CWORD=1
            compfunc=$(complete -p git | grep -oP "(?<=-F )\S+")
            $compfunc ...          # calls __git_wrap__git_main
            printf "%s\n" "${COMPREPLY[@]}"
        '
                                 ↓
        COMPREPLY=(checkout cherry cherry-pick)

Caveats

# completion inside container requires bash-completion installed
incus exec mycontainer -- apt install -y bash-completion

# verify completion available for a command
incus exec mycontainer -- bash -c "complete -p git"

Fallback chain

container has completion function for cmd?
    ↓ yes → delegate to it
    ↓ no  → compgen -c (commands)
              ↓
            compgen -f (files)

Quick reference

Step What happens
rebuild COMP_WORDS strip incus exec CTR -- prefix
complete -p CMD find completion function in container
$compfunc call it with correct args
fallback compgen -f for file completion

The key insight is rebuilding COMP_WORDS and COMP_CWORD with the incus exec CTR -- prefix stripped — the container's bash then sees the command line exactly as if it were running locally.