Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Complete command options with equal signs

Tags:

bash

I am trying to write a Bash completion script for commands that can take long options on the form --option or --param=value. If the user has already entered an option on the command line, that option should be excluded from the completion list (assuming it only makes sense to specify a given option once on the command line).

Here is a first try:

_myprog()
{
    local cur="${COMP_WORDS[$COMP_CWORD]}"

    local words=(--help --param1= --param-state --param2=)
    _exclude_cmd_line_opts
    COMPREPLY=( $(compgen -W "${words[*]}" -- "$cur") )
}
complete -F _myprog myprog

_exclude_cmd_line_opts() {
    local len=$(($COMP_CWORD - 1))
    local i
    for i in "${COMP_WORDS[@]:1:$len}" ; do
         [[ $i == --* ]] && words=( "${words[@]/$i}" )
    done
}

If source this script source script.sh and then write:

$ myprog --param1= <tab><tab>

I get the following completion list:

=              --help         --param2=      --param-state

so it works almost except for that I get a spurious '=' sign in the completion list.. Any suggestions?

like image 886
Håkon Hægland Avatar asked Apr 19 '15 10:04

Håkon Hægland


2 Answers

Entering an equal sign on the command line forces a word break due to the default content of COMP_WORDBREAKS. The effect seems to be that the equal sign enters as a separate word in COMP_WORDS. This is exploited in the following modification of _exclude_cmd_line_opts:

_exclude_cmd_line_opts() {
    local len=$(($COMP_CWORD - 1))
    local i
    for ((i=1 ; i<=len; i++)) ; do
        local j="${COMP_WORDS[$i]}"
        if [[ $j == --* ]] ; then
            (( i<len )) && [[ ${COMP_WORDS[$(( i + 1))]} == '=' ]] && j="$j="
            words=( "${words[@]/$j}" )
        fi
    done
}

The problem with the original version of _exclude_cmd_line_opts was that ${words[@]/$j} would give a spurious = when for example words=(param1=) and j="param1" (note the missing trailing equal sign in $j which was caused by COMP_WORDBREAKS)..

Update

I discovered another peculiarity with this. The cases above worked fine because I never had to type <tab> immediately after an = sign. However, if for example words=(--param= --param-info) and I type --par<tab> there is still two candidate completions and the current words is only partially completed to become --param. At this I would like to select the first of the two candidates, and I type an explicit = sign on the command line and then type <tab> what happens now is that Bash thinks that you have typed a space (since COMP_WORDBREAKS contains =) and the current completion word changes from --param= to =. This again, will make Bash readline omit insert the usual space, so the user is forced to type a space to continue completing next option.

It is possible to avoid having to type a space in the above case, by returning a COMPREPLY array with an empty string.

_myprog()
{
    local cur="${COMP_WORDS[$COMP_CWORD]}"
    local prev=""
    (( COMP_CWORD > 0 )) && prev="${COMP_WORDS[$(( COMP_CWORD - 1))]}"
    [[ $cur == '=' && $prev == --* ]] && { COMPREPLY=( "" ); return; }

    local words=(--param= --param-info)
    _exclude_cmd_line_opts
    COMPREPLY=( $(compgen -W "${words[*]}" -- "$cur") )
}
like image 136
Håkon Hægland Avatar answered Oct 02 '22 18:10

Håkon Hægland


This solution takes advantage of _init_completion function. It does not complete arguments for options that take such. It also accepts long options in form --param value. This does not examine line after cursor position for excluding options, but it could be modified to take into account whole command line, if needed.

_exclude_cmd_line_opts() {
    local i w j skip= sep_arg
    for ((i=1 ; i<cword; i++)) ; do
        [[ $skip ]] && { skip=; continue; }
        w=${words[$i]}
        if [[ $w == --* ]] ; then
            [[ $w == --?*=* ]] && {
                w=${w%%=*}
                sep_arg=
            } || sep_arg=1
            for j in ${!options[@]}; do
                [[ ${options[$j]%=} == $w ]] && {
                    [[ ${options[$j]} == *= && $sep_arg ]] && skip=1
                    unset -v options[$j]
                    break
                }
            done
        fi
    done
}

_myprog()
{
    IFS=$'\n'
    local cur prev words cword split # needed by _init_completion()
    # Do not treat = as word breaks even if they are in $COMP_WORDBREAKS:
    # Split option=value into option in $prev and value in $cur
    _init_completion -s || return 

    local options=(--param= --param-info --opt --opt-suffix)
    [[ $prev == --param ]] && { COMPREPLY=( ); return 0; }
    _exclude_cmd_line_opts

    local i
    for i in ${!options[*]}; do [[ ${options[$i]} == *= ]] || options[$i]+=' ' ; done
    COMPREPLY=( $(compgen -W "${options[*]}" -- "$cur") )
    compopt -o nospace
} && complete -F _myprog myprog
like image 21
jarno Avatar answered Oct 02 '22 19:10

jarno