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.

  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").


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")
        for ((i=0; i < ${#COMPREPLY[@]}; i++)); do
            [ -d "${COMPREPLY[$i]}" ] && COMPREPLY[$i]=${COMPREPLY[$i]}/

    return 0

complete -o nospace -F _xyz xyz

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

| improve this answer | |
  • This works great (though it's too bad it's not built in). Thanks! – Rob I Mar 13 '13 at 13:53
  • 1
    Any reason not to use "$2" instead of "${COMP_WORDS[COMP_CWORD]}"? – Edward Falk Aug 23 '16 at 20:52

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

  local cur=${COMP_WORDS[COMP_CWORD]}

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

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

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.