bashshellbash-completion

How to include completions for one command in those of another


I have a command ts which takes as its arguments a command and that command's arguments. I want tab-completion to "do the right thing", namely:

When I type, for example ts foo<tab> bash should try to auto-complete and find any command beginning with foo. I've got this working thus:

complete -F __ts_bash_completions ts

__ts_bash_completions () {
    COMPREPLY=();

    # if there's only one word on the command line
    if [ "$COMP_CWORD" -eq 1 ]; then
        COMPREPLY=($(compgen -c -- "${COMP_WORDS[COMP_CWORD]}"));

Once it has a complete command name, I want tab-completions for subsequent arguments to use the tab-completion semantics of that first argument. If there is no completion function defined for that first argument I have that working just fine too. Carrying on from the above, this works:

    # otherwise ...
    else
        local command_completion_function="$(complete -p ${COMP_WORDS[1]} 2>/dev/null|sed 's/.*-F \([^ ]*\) .*/\1/')"

        if [ -z "$command_completion_function" ]; then
            COMPREPLY=($(compgen -W "$(ls)" -- "${COMP_WORDS[COMP_CWORD]}"));

But where I'm struggling is with the last requirement, that if there is a completion function available it should be used, and I just can't get that damned thing to work. I've seen this question and its answer and tried to generalise it thus. Again, carrying on from the above:

        else
            echo using $command_completion_function
            echo COMP_CWORD="$COMP_CWORD"
            echo COMP_LINE="$COMP_LINE"
            echo COMP_WORDS="${COMP_WORDS[*]}"

            COMP_CWORD=$(( $COMP_CWORD - 1 ))
            COMP_LINE=$(echo $COMP_LINE|sed "s/^${COMP_WORDS[0]} //")
            unset COMP_WORDS[0]
            echo COMP_CWORD="$COMP_CWORD"
            echo COMP_LINE="$COMP_LINE"
            echo COMP_WORDS="${COMP_WORDS[*]}"

            $command_completion_function
        fi
    fi
}

Everything looks OK to me as far as I can tell from the echo of the various COMP_* variables, but can anyone spot the silly mistake?


Solution

  • I've figured it out. The error was that if there was a completion function already defined for a command I would call that, but I wasn't dealing with the case where that returns nothing. My test case was ls, which on my machine has ...

    $ complete -p ls
    complete -o bashdefault -o default -F _fzf_path_completion ls
    

    Note the -o bashdefault -o default, which kick in if the function doesn't set a COMPREPLY. -o bashdefault says "if we've got nothing, apply the bash defaults". -o default says "if we've still got nothing, apply the readline defaults".

    So the fix is to say this at the top of the file:

    complete -o bashdefault -o default -F __ts_bash_completions ts
    

    There's probably a way of applying those only if the command passed as the first argument has them defined for its completions, but just applying them globally is good enough for my use cases.

    The complete fixed script reads:

    complete -o bashdefault -o default -F __ts_bash_completions ts
    
    __ts_bash_completions () {
        COMPREPLY=();
    
        if [ "$COMP_CWORD" -eq 1 ]; then
            COMPREPLY=($(compgen -c -- "${COMP_WORDS[COMP_CWORD]}"));
        else
            local command_completion_function="$(complete -p ${COMP_WORDS[1]} 2>/dev/null|sed 's/.*-F \([^ ]*\) .*/\1/')"
    
            if [ ! -z "$command_completion_function" ]; then
                COMP_CWORD=$(( COMP_CWORD - 1 ))
                COMP_LINE=$(echo $COMP_LINE|sed "s/^${COMP_WORDS[0]} //")
                COMP_WORDS=( "${COMP_WORDS[@]:1}" )
    
                $command_completion_function "${COMP_WORDS[0]}" "$2" "$3"
            fi
        fi
    }