bash-completion: Adding a quoted suggestion with a space

I’m adding bash-completion to an existing tool.

After someone passes the -s flag, I’d like to offer several options on bash-completion:

  • Closed
  • Feedback
  • "In Progress"
  • New
  • Rejected
  • Resolved

The space in "In Progress" is causing a problem.

#!/usr/bin/env bash
_remote-redmine()
{
    local cur

    COMPREPLY=()
    cur=${COMP_WORDS[$COMP_CWORD]}

    case "${COMP_WORDS[($COMP_CWORD-1)]}" in
        -s)
            s_opts=( "New" "In Progress" "Resolved" "Feedback" "Closed" "Rejected" )
            COMPREPLY=( $( compgen -W "${s_opts[*]}" -- "$cur") )
            ;;
    esac
    return 0
}
complete -F _remote-redmine remote-redmine

When I run the tool, I get:

$ remote-redmine -s <tab><tab>
Closed    Feedback  In        New       Progress  Rejected  Resolved

But I’d like to get:

$ remote-redmine -s <tab><tab>
Closed    Feedback  "In Progress"       New       Rejected  Resolved

or

$ remote-redmine -s <tab><tab>
Closed    Feedback  In Progress        New       Rejected  Resolved

Things I’ve tried:

  • s_opts=( ... "In Progress" ... ): No difference

  • s_opts( ... "In\ Progress" ...): No difference

  • s_opts( ... "In\ Progress" ...): Two options are In and Progress

  • s_opts( ... ""In Progress"" ...): No difference

  • compgen -o nospace ...: No difference

  • compgen -o noquote ...: No difference

  • At least compgen can put In Progress on one line, but that doesn’t translate into complete

    $ compgen -W 'New "In Progress"'
    New
    In Progress
    
  • Escaping extra quotes helps with compgen, but that doesn’t translate into complete:

    $ compgen -W 'New ""In Progress""'
    New
    "In Progress"
    
  • Escaping the space three times does help, but it still doesn;’t translate into complete:

    $ compgen -W 'New In\ Progress'
    New
    In Progress
    
  • I thought escaping the space seven times would be promising because then complete would resolve a few of those, but that didn’t help either:

    $ compgen -W 'New In\\\ Progress'
    New
    In\ Progress
    ...
    $ remote-redmine -s <tab><tab>
    ...   In\  New  Progress ...
    
  • Bypassing compgen will work, but then I lose the auto-fill on single-tabs and filtering on double-tabls

    COMPREPLY=( ... 'In Progress' ... )
    COMPREPLY=( ,,, '"In Progress"' ... )
    
Asked By: Stewart

||

This has to do with word splitting and the default value of IFS, no surprise.

In

compgen -W "${s_opts[*]}"

The array expansion with [*] joins the array elements to a single string, using the first character of IFS as the separator. It’s space by default, so the distinction between a space in a value and a space as a separator is lost. Luckily, compgen -W interprets the argument as a string of values separated by IFS characters, so it works correctly with [*] if you just change IFS to something like : or anything else that doesn’t appear in the values.

Then, in

COMPREPLY=( $( compgen ...) )

you’re implicitly splitting the string printed by compgen using IFS (and passing them through globbing), but compgen appears to print the values separated by newlines specifically. Doing that with splitting would then require setting IFS to the newline, but really would be more suited for reading with readarray, since it uses newlines anyway and doesn’t glob.

So, you could do

_remote-redmine() {
    local cur
    local IFS=:

    COMPREPLY=()
    cur=${COMP_WORDS[$COMP_CWORD]}

    case "${COMP_WORDS[($COMP_CWORD-1)]}" in
        -s)
            s_opts=( "New" "In Progress" "Resolves" "Feedback" "Closed" "Rejected" )
            readarray -t COMPREPLY < <( printf "%qn" $(compgen -W "${s_opts[*]}" -- "$cur"))
            ;;
    esac
    return 0
}

Or, since a newline works in both places, you could just set IFS=$'n' and go with splitting the output as before, but you should still disable globbing for the duration of the function, and that’s one more piece of global state to save and restore.

Answered By: ilkkachu

This works for me. Rather than using compgen, we just loop over our array and add the items whose prefix is $cur:

_remote-redmine()
{
    local cur
    local item

    COMPREPLY=()
    cur=${COMP_WORDS[$COMP_CWORD]}

    case "${COMP_WORDS[($COMP_CWORD-1)]}" in
        -s)
            local s_opts=( "New" "In Progress" "Resolved" "Feedback" "Closed" "Rejected" )
            for item in "${s_opts[@]}"; do
                case "$item" in
                    ( "$cur"* )
                        COMPREPLY+=( "$item" )
                        ;;
                esac
            done
            ;;
    esac
    return 0
}
complete -F _remote-redmine remote-redmine

Note the backslash I added: "In Progress". That’s a literal part of the item. When you type In and hit Tab, you get In Progress in the command line: the escape is outside of a double quote now and thus active: it prevents the word splitting.

Answered By: Kaz
Categories: Answers Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.