Previous answers require bash 4, or aren't composable with other compgen-based completions for the same command. The following solution doesn't require compopt (so it works with bash 3.2) and it is composable (for example you can easily add additional filtering to only match certain file extensions). Skip to the bottom if impatient.
You can test compgen by running it directly on the command line:
compgen -f -- $cur
where $cur
is the word you would've typed so far during interactive tab-completion.
If I'm in a directory that has a file afile.py
and a sub-directory adir
, and cur=a
then the above command shows the following:
$ cur=a
$ compgen -f -- $cur
adir
afile
Note that compgen -f
shows files and directories. compgen -d
shows only directories:
$ compgen -d -- $cur
adir
Adding -S /
will add a trailing slash to every result:
$ compgen -d -S / -- $cur
adir/
Now, we can try listing all files and all directories:
$ compgen -f -- $cur; compgen -d -S / -- $cur
adir
afile.py
adir/
Note that there's nothing stopping you from calling compgen more than once! In your completion script you would use it like this:
cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=( $(compgen -f -- "$cur"; compgen -d -S / -- "$cur") )
Unfortunately, this still doesn't give us the behaviour that we want, because if you type ad<TAB>
you have two possible completions: adir
and adir/
. So ad<TAB>
will complete to adir
, at which point you still have to type in the /
to disambiguate.
What we need now is a function that will return all files but no directories. Here it is:
$ grep -v -F -f <(compgen -d -P '^' -S '$' -- "$cur") \
> <(compgen -f -P '^' -S '$' -- "$cur") |
> sed -e 's/^\^//' -e 's/\$$//'
afile.py
Let's break it down:
grep -f file1 file2
means show the lines in file2 that match any of the patterns in file1.
-F
means the patterns in file1 must match exactly (as a substring); they aren't regular expressions.
-v
means invert the match: Only show lines that aren't in file1.
<(...)
is bash process substitution. It allows you to run any command in the places where a file is expected.
So we're telling grep: here's a list of files -- remove any that match this list of directories.
$ grep -v -F -f <(compgen -d -P '^' -S '$' -- "$cur") \
> <(compgen -f -P '^' -S '$' -- "$cur")
^afile.py$
I've added beginning and end markers with compgen's -P ^
and -S '$'
because grep's -F
does substring matching and we don't want to remove a filename like a-file-with-adir-in-the-middle
just because its middle part matched a directory's name. Once we have the list of files we remove those markers with sed.
Now we can write a function that does what we want:
# Returns filenames and directories, appending a slash to directory names.
_mycmd_compgen_filenames() {
local cur="$1"
# Files, excluding directories:
grep -v -F -f <(compgen -d -P ^ -S '$' -- "$cur") \
<(compgen -f -P ^ -S '$' -- "$cur") |
sed -e 's/^\^//' -e 's/\$$/ /'
# Directories:
compgen -d -S / -- "$cur"
}
You use it like this:
_mycmd_complete() {
local cur=${COMP_WORDS[COMP_CWORD]}
COMPREPLY=( $(_mycmd_compgen_filenames "$cur") )
}
complete -o nospace -F _mycmd_complete mycmd
Note that -o nospace
means you don't get a space after a directory's /
. For normal files we added a space at the end with sed.
One nice thing about having this in a separate function is that it's easy to test! For example here's an automated test:
diff -u <(printf "afile.py \nadir/\n") <(_mycmd_compgen_filenames "a") \
|| { echo "error: unexpected completions"; exit 1; }
compopt +o
) seems not to be documented in the bash manual.. – Bishopric