Boris Bukh
Boris Bukh

Reputation: 167

How to ensure that a lazily-loaded bash completion is loaded

I have my own script extending scp, with the same syntax. Naturally, I want to use the same autocomplete function. Following the suggestion in another question I added the following to .bash_aliases

complete -o nospace -F _scp my_scp

Unfortunately, if I type my_scp <tab> <tab> in the terminal, I get the error bash: completion: function `_scp' not found. If instead, I first type scp <tab> <tab> then the autocompletion function is loaded, and I can then use my_scp completion without a problem.

How do I ensure that the lazily-loaded function _scp is loaded when it is needed for my custom command?

ADDED IN RESPONSE TO A COMMENT: Output of complete -p my_scp scp before typing scp <tab> <tab> in the terminal:

complete -o nospace -F _scp my_scp
bash: complete: scp: no completion specification

Output of complete -p my_scp scp after typing scp <tab> <tab> in the terminal:

complete -o nospace -F _scp my_scp
complete -o nospace -F _scp scp

Similarly, type _scp gives bash: type: _scp: not found before, and a long source code after.

Upvotes: 0

Views: 91

Answers (2)

Philippe Carphin
Philippe Carphin

Reputation: 287

First we need to find out where the function _scp is defined

  1. Ensure the function is loaded. As OP says, after pressing TAB, the completion is loaded. complete -p scp should show a function FUNC.
  2. Find out where that function is defined. Run shopt -s extdebug then declare -F FUNC which will print the path of the file FILE where the function is defined.

Next we set it up as the completion function for our command my_scp also with the same options which you seem to have done

A better way would be to trigger bash_completion to load the function the way it normally does that:

First without any error checks

use_lazy_loaded_bash_completion(){
    local my_cmd=$1
    local cmd=$2
    __load_compltion "${cmd}"
    
    local spec="$(complete -p "${cmd}")"
    ${spec} "${my_cmd}"
}

Assuming bash_completion <2.12, when it is loaded, bash_completion sets a default completion function

complete -D _completion_loader

which calls __load_completion which sets up completion for the command name it receives as an argument.

We use complete -p to get the completion specification, then run that line with an extra argument so we set the exact same completion specification for our command.

Now since bash_completion changed things up in 2.12,

  • _scp is now _comp_cmd_scp
  • __load_completion is now _comp_load the following code works for both and has error checks:
# Whatever the comp spec is for cmd, use the same comp spec for my_cmd
# For example, complete -p scp -> `complete -o bashdefault -F _comp_cmd_scp`
# Then we run `complete -o bashdefault -F _comp_cmd_scp scp my_scp`
# We don't need to parse anything, we just need to add another positional
# argument at the end of the complete command.
use_same_completion_spec(){
    local cmd=$1
    local my_cmd=$2

    if declare -f __load_completion >/dev/null ; then
        if ! __load_completion "${cmd}" ; then
            echo "__load_completion failed to load completion for '${cmd}'" >&2
            return 1
        fi
    elif declare -f _comp_load >/dev/null ; then
        if ! _comp_load "${cmd}" ; then
            echo "_comp_load failed to load completion for '${cmd}'" >&2
            return 1
        fi
    else
        echo "__load_completion and _comp_load do not exist" >&2
        return 1
    fi

    if ! spec="$(complete -p "${cmd}")" ; then
        echo "Error getting completion specification for '${cmd}'" >&2
    fi
    ${spec} ${my_cmd}
}

And

# Demo for scp:
use_same_completion_spec scp my_scp
complete -p my_scp

should print complete -F _comp_cmd_sscp my_scp (or -F _scp depending on your version of bash-completion).

In the above code, we use declare -f FUNC to check if a certain shell function is defined in the current environment. That way we can know which of the tow functions __load_completion or _comp_load we need to call. If declare -f FUNC succeeds, that is like a "true" condition so we enter the if.

So the first if-elif-else statement does this:

  • Does __load_completion exist? If so try to call it and if that fails print a message and return 1 from the function.
  • Otherwise, does _comp_load exist? If so try to call it and output a message and return 1 if it fails.
  • Otherwise, none of the two functions exist, return 1

And if we made it this far, get the compspec for the original function: something like complete -o bashdefault -F _comp_cmd_scp scp and if we succeed, then we run what we got but with our function added to the end which would be

complete -o bashdefault -F _comp_cmd_scp scp my_scp

which sets _comp_cmd_scp as the completion function for both scp and my_scp. We set the completion for scp a second time this way but that's not a problem.

EDIT 1: Calling use_same_completion_spec or any code that uses __load_completion or anything from bash-completion should generally only be done in interactive shells in something like if [[ $- == *I* ]] ; then ... ; fi since on most systems, the normal configuration for bash-completion is to have it only be loaded for interactive shells. To prepare this answer, I worked in a script that I ran non-interactively and at the top of that script, I loaded bash completion manually.

EDIT 2: In my code, I did some checks to support both __load_completion and _comp_load. For personal tools, I don't actually do this, I just use __load_completion. But since the internet is forever, I wanted this answer to be future proof.

Upvotes: 2

Philippe
Philippe

Reputation: 26727

The function _scp is defined in /usr/share/bash-completion/completions/scp, to load it, put following in .bash_aliases :

shopt -s extglob; source /usr/share/bash-completion/completions/scp

Upvotes: 1

Related Questions