6

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?

Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
  • 1
    Change `--param1=` to `--param1` in the `local words` assignment. – Barmar Apr 19 '15 at 10:59
  • Yes that works, but I would like to avoid having the user type the `=` sign if possible.. – Håkon Hægland Apr 19 '15 at 11:04
  • Oh, you're talking about the `=` at the beginning, not the `=` after `--param2`. I didn't see that before. – Barmar Apr 19 '15 at 11:06
  • I cannot reproduce since I am in cygwin. However, what if you quote every argument in the array? `local words=("--help" "--param1=" ...)`. It looks like it sees `=` as another argument. – fedorqui Apr 19 '15 at 11:11
  • @fedorqui It does not help. I think it might be related to COMP_WORDBREAKS .. – Håkon Hægland Apr 19 '15 at 11:14
  • Possible duplicate of [Bash completions with equals sign and enumerable flag values](http://stackoverflow.com/questions/5040883/bash-completions-with-equals-sign-and-enumerable-flag-values) – miken32 Mar 14 '16 at 21:17
  • See also [this one](https://stackoverflow.com/q/58156681/4414935) – jarno Oct 22 '19 at 16:36
  • You can just remove the `=` in `COMPREPLY` using `sed`, `awk`, etc. Also, if you want to append space to short option suggestions but not to the long options, check my answer [here](https://stackoverflow.com/a/66151065/6474744) – Pedram Feb 11 '21 at 09:10

2 Answers2

1

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") )
}
Håkon Hægland
  • 39,012
  • 21
  • 81
  • 174
  • This is what I would do too. (I played around but you answered earlier.) :) Also it seems there is no other way then post-sanitize this, since you already mentioned that setting `COMP_WORDBREAKS` could affect the behaviour of other programs. – hek2mgl Apr 19 '15 at 12:08
  • Suppose you have two options `--opt` and `--opt-suffix`. If you type `myprog --opt ` then `-suffix` will be completed as a separate word. – jarno Nov 05 '19 at 22:18
  • Also `--param=-` expands to `--param=--param-info` – jarno Nov 06 '19 at 08:08
1

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
jarno
  • 787
  • 10
  • 21