Bash Autocompletion - How to pass this array to compgen without significant whitespace being collapsed?
Asked Answered
A

5

16

The following bash completion passes an array of possible words (i.e. completions) to compgen.

basenames=("foo" "fu bar" "baz");

COMPREPLY=($(compgen -W "${basenames[*]}" -- "${COMP_WORDS[COMP_CWORD]}"))

The problem is that the whitespace in the array elements is not preserved, that is "foo bar" is treated as to elements thanks to word splitting. Is there some way to preserve the whitespace, so that 3 elements are shown, and not 4?

EDIT

basenames contains filenames, that is nearly every character (apart from / and \0) is allowed.

EDIT 2

The -W flag expects a single word, that is something like foo bar foobar. Passing multiple elements to it (this is what ${basenames[@]} would do) won't work.

EDIT 3

Changed examplary basenames array (so the foo and the foo from foo bar won't get collapsed).

Using a newline to separate the words works:

local IFS=$'\n'
COMPREPLY=($(compgen -W "$(printf "%s\n" "${basenames[@]}")" --  ${COMP_WORDS[COMP_CWORD]}"))

Using \0 doesn't:

local IFS=$'\0'
COMPREPLY=($(compgen -W "$(printf "%s\0" "${basenames[@]}")" --  ${COMP_WORDS[COMP_CWORD]}"))
Agone answered 18/5, 2012 at 12:17 Comment(0)
S
11

Why bother with compgen? Just add them to COMPREPLY manually. The following will complete the matching filenames from /some/path, handling filenames safely.

some_completion_function() {
    local files=("/some/path/$2"*)
    [[ -e ${files[0]} ]] && COMPREPLY=( "${files[@]##*/}" )
}

It's not possible to have compgen handle filenames safely.

Stepparent answered 14/7, 2012 at 9:26 Comment(6)
+1 This is how I actually achieved what I wanted. Really good advice!Agone
what are "$2" and "##*/" ?Ventricular
##*/ removes the head until the last '/'; $2 is the current completion word; right?Ventricular
@oluc, that's right. And "$1" is the command being completed and "$3" is the previous word.Stepparent
Can you explain ${files[@]##*/} for those who aren't really familiar with bash arrays? Thanks.Ube
@JonathonReinhart It expands all elements of the array, removing the longest matching substring of the pattern (*/) from the start of each element. It's explained here: Parameter Expansion. See also BashFAQ 100.Stepparent
G
1

Perhaps this might do:

COMPREPLY=($(compgen -W "$(printf "%q " "${basenames[*]}")" -- "${COMP_WORDS[COMP_CWORD]}"))
Galyak answered 18/5, 2012 at 18:47 Comment(3)
It nearly works, but it treats "foo bar" as 2 elements, whereas I want it to display it as one element.Agone
@OliverWeiler: Did you try glenn's with a @?Ballman
@DennisWilliamson Yes, same result ("foo bar" is treated as 2 elements)Agone
E
1

I solved this problem by creating a bash function:

    # Helper function to complete arguments
    function __get_completion()
    {
        # No arguments or command failure
        (( ! $# || $? )) && COMPREPLY=() && return $?

        local comp_list=$*

        COMPREPLY=($(compgen -W "$comp_list" -- \'${COMP_WORDS[COMP_CWORD]}\'))
        return $?
    }

Now you can use it like so:

basenames=("foo" "fu bar" "baz")
switches=('--source' '--destination' '--flag')
__get_completion "${basenames[@]}" "${switches[@]}"; return $?

As you can see it even works with multiple arguments or passing a command in:

__get_completion "$(ls -a)"; return &?
Erena answered 25/1 at 17:19 Comment(0)
D
0

Read better solution there

Use readarray and printf

readarray -t words < /etc/passwd
compgen -W  "$(printf "'%s' " "${words[@]}")" -- "man"
# OUTPUTS: man:x:6:12:man:/var/cache/man:/usr/sbin/nologin
  • readarray: Avoid dealing with IFS
  • printf: Escape: or @Q parameter transformation after bash 4.4 (20170 man bash search parameter@operator
Desmonddesmoulins answered 6/9, 2020 at 3:16 Comment(0)
B
-3

You should almost always use an at sign instead of an asterisk to subscript an array.

COMPREPLY=($(compgen -W "${basenames[@]}" -- "${COMP_WORDS[COMP_CWORD]}"))

from man bash (or see the Bash Reference Manual):

If subscript is @ or *, the word expands to all members of name. These subscripts differ only when the word appears within double quotes. If the word is double-quoted, ${name[*]} expands to a single word with the value of each array member separated by the first character of the IFS special variable, and ${name[@]} expands each element of name to a separate word.

In other words, the at sign form "flattens" the array. The asterisk form preserves it.

Ballman answered 18/5, 2012 at 17:32 Comment(4)
If I replace the * with an @, auto completion stops working completly.Agone
compgen -W requires that all completions be provided in a single "word", which [@] doesn't do. See my answer to a previous question for a detailed explanation.Replenish
-1: This advice is so out of context that it's just plain incorrect. -W expects a single argument and using @ will give you an undefined number of them.Swick
As the last few comments said this answer is wrong.Lasky

© 2022 - 2024 — McMap. All rights reserved.