14

I'm trying to get a bash completion script set up and having some trouble.

I would like to set it up so the completions listed are either files matching a particular extension, or directories (which may or may not contain files of that extension).

The trouble I'm having is that the only way I can get the completions to contain files and directories is by using something like -o plusdirs -f -X '!*.txt', but when I let bash complete one of the directories, it just adds a space onto the end, rather than a slash.

_xyz()
{
  local cur=${COMP_WORDS[COMP_CWORD]}
  local prev=${COMP_WORDS[COMP_CWORD-1]}

  #COMPREPLY=( $( compgen -f -X '!*.txt' -- $cur ) )
  #COMPREPLY=( $( compgen -f -G '*.txt' -- $cur ) )
  #COMPREPLY=( $( compgen -o filenames -f -X '!*.txt' -- $cur ) )
  #COMPREPLY=( $( compgen -o dirnames  -f -X '!*.txt' -- $cur ) )
  COMPREPLY=( $( compgen -o plusdirs  -f -X '!*.txt' -- $cur ) )
  return 0
}

complete -F _xyz xyz

I've tried all the commented-out lines too, but they don't even expand the directories.

For testing, I've been running this in a directory with one .txt file and one directory "dir" (with a .txt file inside it, though that doesn't matter yet). Typing xyz <TAB> with this function lists the directory and the .txt file, but typing xyz d<TAB> expands to xyz dir (well, with a space after "dir").

2 Answers 2

12

If you look at the function _cd() in /etc/bash_completion, you'll see that it appends the trailing slash itself and that complete gets called with the option -o nospace for cd.

You can do the same for xyz, but to have to verify separately if the found match is a directory (if so, append slash) or a file (if so, append space). This should be done in a for loop to process all found matches.

Also, to properly handle paths that contain spaces, you have to set the internal file separator to only newline and escape the spaces. Using IFS=$'\n' in combination with printf %q makes completion work with almost all characters.1 Special care has to be taken to not escape the trailing space.

The following should work:

_xyz ()
{
    local IFS=$'\n'
    local LASTCHAR=' '

    COMPREPLY=($(compgen -o plusdirs -f -X '!*.txt' \
        -- "${COMP_WORDS[COMP_CWORD]}"))

    if [ ${#COMPREPLY[@]} = 1 ]; then
        [ -d "$COMPREPLY" ] && LASTCHAR=/
        COMPREPLY=$(printf %q%s "$COMPREPLY" "$LASTCHAR")
    else
        for ((i=0; i < ${#COMPREPLY[@]}; i++)); do
            [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/
        done
    fi

    return 0
}

complete -o nospace -F _xyz xyz

1 The newline character is the obvious exception here, since it is an internal file separator.

2
  • This works great (though it's too bad it's not built in). Thanks!
    – Rob I
    Commented Mar 13, 2013 at 13:53
  • 1
    Any reason not to use "$2" instead of "${COMP_WORDS[COMP_CWORD]}"? Commented Aug 23, 2016 at 20:52
3

I think this simple solution works in that it:

  1. Matches directories and files that end in .txt
  2. Handles spaces in the file names
  3. Adds a slash at the end of folder completions with no trailing space
  4. Adds space at the end of a file completion match

The key was passing -o filenames to complete. This was tested on GNU bash 3.2.25 on RHEL 5.3 and GNU bash 4.3.18 on osx

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

  local IFS=$'\n'
  COMPREPLY=( $( compgen -o plusdirs  -f -X '!*.txt' -- $cur ) )
}

complete -o filenames -F _xyz xyz
1
  • Yes that does seem to work great. Much simpler, thanks for catching the important argument!
    – Rob I
    Commented Aug 28, 2014 at 14:03

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .