Skip to content

Instantly share code, notes, and snippets.

@ernstki
Last active November 23, 2024 18:24
Show Gist options
  • Save ernstki/569fd38c4d164104b0074a539b689645 to your computer and use it in GitHub Desktop.
Save ernstki/569fd38c4d164104b0074a539b689645 to your computer and use it in GitHub Desktop.
Bash programmable completion example for "cuddled" options + arguments

Bash programmable completion for -nvalue / --name=value options

I was reading an old debian-administration.org post about Bash programmable completion, and I had the same question as this person:

I created a script and I use bash_completion, but I cannot figure out how to allow the completion of "--name=value1 --name=value2" (the "=" sign in the middle stop any other completion, and I tried to play with suffixes/prefixes without any success :s

Inspired by that frustrated emoticon (:s—so expressive!), here's what I finally found that worked.

The situation

Imagine you have a script with an option -n / --name that you can give a value in any one of the following:

  • -n VALUE (case 1)
  • -nVALUE (case 2)
  • --name VALUE (case 3)
  • --name=VALUE (case 4)

With cases 1 and 3, your job is really easy, and it works just like any Bash programmable completion tutorial you're likely to find on the web.

For case 2, you have to remove the -n part before passing it to compgen for completion against the list of possible values, then put the -n back on; this is discussed in more detail below.

However, in case 4, the "current" option (that's the $cur variable in the programmable completion script below) is =, that's when you have the biggest challenge. You need to look at two options ago to see if it's --table before deciding how to proceed.

For both cases 2 and 4, see this gist for an uncuddle function that will return the VALUE part of -nVALUE or --name=VALUE. This has nothing to do with programmable completion, but you'll find it helpful if you want to be able to parse that kind of option-value pair in your own shell scripts.

Dealing with the = is what makes it complicated

Some discussion in the SO thread "Bash completion for path in argument (with equals sign present)" suggests that messing with COMP_WORDBREAKS (the way npm once did, that apparently caused other problems) is possibly one way to tackle the problem. The default value of COMP_WORDBREAKS on my system is

"'><=;|&(:

which kind of makes sense why I was having trouble with quote characters in some of my other Bash completion adventures.

The phrase "word breaks" suggest that these would be ignored by the completion facilities, but no. You totally can get = as the "current" option for completion, and have to deal with that by looking at previous options. That's what the purpose of the upto array is in the programmable completion script below.

Another way of stating the above is that you will never get a completion where $cur is --name=; the --name and the = are always received as separate completion candidates.

Template programmable completion script

_myutil() {
    local cur prev upto
    COMPREPLY=()
    cur="${COMP_WORDS[COMP_CWORD]}"
    prev="${COMP_WORDS[COMP_CWORD-1]}"

    # all arguments up to but not including the "current" one; the second
    # argument to Bash's array subscripting is length, _not_ ending index, so
    # it's not possible to use a negative index to drop items from the end
    upto=( "${COMP_WORDS[@]:0:$((${#COMP_WORDS[@]}-1))}" )

    # populate the _opts and _values arrays only once per session
    [[ $_opts ]] ||
        export _opts=(
            $( myutil --help | awk '<crazy Awk program to parse out args>' )
        )

    # theoretical option to list the allowed values for '-n' / '--name'
    [[ $_values ]] || export _values=( $( myutil -n list ) )

    case "$cur" in
        -n*)
            # '-n' option completion when the 'value' is "cuddled" with the
            # option; must remove the '-t' from the candidate given to
            # 'compgen', then have'compgen' put the '-t' back on as a prefix
            cur=${cur/-t}
            COMPREPLY=( $(compgen -W "${_values[*]}" -P "-t" -- "$cur" ) )
            return 0
            ;;
        =)
            if [[ $prev == --table ]]; then
                cur=${cur/--table=}
                COMPREPLY=( $(compgen -W "${_values[*]}" -P "--table=" \
                                        -- "$cur" ) )
                return
            fi
            ;;
        ""|-*)
            # complete any 'myutil' options after '<TAB>' ('$cur' is empty)
            # or '-<TAB>' but not in the middle of one of the '--name' values
            COMPREPLY=( $(compgen -W "${_opts[*]}" -- "$cur") )
            return
            ;;
    esac

    case "$prev" in
        -t|--table)
            # the most straightforward case where the option is completely
            # separate from its argument; as simple as they get
            COMPREPLY=( $(compgen -W "${_values[*]}" -- "$cur" ) )
            return
            ;;
        =)
            # here, the cursor is already completing a partial 'value', as in
            # '--name=val<TAB>'; since '=' is itself a candidate for
            # completion (which seems contrary to the meaning of
            # COMP_WORDBREAKS, but whatever), you have to check _two_
            # arguments ago to see if it was '--table', _then_ proceed
            #
            # apart from that, it's a very straightforward call to 'compgen'
            if [[ ${upto[-2]} == --table ]]; then
                COMPREPLY=( $(compgen -W "${_values[*]}" -- "$cur" ) )
                return
            fi
            ;;
    esac

} # _myutil

complete -F _myutil myutil

As mentioned above, the trick to making it -nvalue work is to trim off the option part (-t (with the ${var/search/repl} parameter expansion) before passing it to compgen, but ask compgen to add the -t back on as a prefix (-P option) when actually passing the list of completions to the readline library.

That insight was gained from (gasp—reading the manual!) this statement in the "Programmable Completion" section of the manual:

Finally, any prefix and suffix specified with the -P and -S options are added to each member of the completion list, and the result is returned to the Readline completion code as the list of possible completions.

One small downside of the way the above completion script works is that you can't just use a catch-all default (*)) case condition to get a list of program options when completing on an "empty" command line (that is myutil <TAB>). If the completion were in the middle of one of the allowable values for -n / --name, you'd wreck that.

The simple workaround requires checking whether $cur is either an empty string or begins with a dash (""|-*).

TODO

Include a more detailed discusion of the "crazy Awk script" that parses command-line options out of the output of myutil --help.

# when the "usage:" section looks like this:
#
#   usage:
#     myutil [-?|--help|--help-all] [-x|--examples] [-V|--version]
#
myutil --help \
  | awk -F'[][|]' '
      /usage:/, /options:/ {
          for (i=0; i<NF; i++) {
              # if it begins with a dash, this is an option
              if ($i ~ /^-/) {
                  # discard anything after a space (arguments)
                  split($i,A," "); print A[1]
              }
          }
      }'

# and the "options:" section looks like this:
#
#   options:
#     -a|--option-a            description of "A" option
#     -b|--option-b A1,A2,...  description of "B" option; takes an argument
#     -c|--option-c [ARG]      description of "C" option w/ optional argument
#
#   Please report bugs to https://gist.github.com/ernstki/569fd38c4d164104b0074a539b689645
#
myutil --help \
  | awk -F'[ |]+' '
      /options:/, /Please report/ { if (/-.\|/) print $2,$3 }'

See also

  • this gist for an uncuddle Bash shell function that will return the VALUE part of -nVALUE or --name=VALUE

Author

Kevin Ernst ernstki -at- mail.uc.edu

Creative Commons License
This work is licensed under a Creative Commons Attribution 3.0 Unported License.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment