Bash custom auto completion with path completion
Friday, July 18th, 2025
The problem
I have some custom bash auto completions configured in my .bashrc
. For example, one for completing SSH hosts:
_complete_ssh ()
{
cur="${COMP_WORDS[COMP_CWORD]}"
comp_words="(cat ~/.ssh/config | grep "^Host " | awk '{print $2}')"
COMPREPLY=($(compgen -W "${comp_words}" -- $cur))
return 0
}
complete -F _complete_ssh ssh scp sftp sshfs
This extracts hostnames from ~/.ssh/config
and when I use any of the ssh, scp, etc commands I can press to complete the hostname. This is really nice, however this breaks normal path completion. Not so much a problem for the ssh
command, but very annoying when using scp
.
I’ve looked around on the internet, and tried some things myself, but no solution seemed to work.
I think I’ve finally found one.
How completion works in bash
First let me explain a bit how the above code snippet works.
We define a bash function _complete_ssh()
. We then tell the complete
command that if I’m trying to run any of the ssh
, scp
, etc commands, it should attempt completion using the function (-F
).
We manually generate a list of all possible completions ($comp_words
). We then feed that to the compgen
program, along with the currently (partially) typed command by the user. An example of what this would look like:
$ compgen -W "foo foobar bar" fo
foo
foobar
complete
received this output (via COMPREPLY
) and shows it to the user. When only one option remains, bash completes to that.
Now you can see the problem: $comp_words
doesn’t contain files and directories, so they’re not included in the completion options.
Failed attempt
At first, very naively, I tried to include the contents of the current dir in $comp_words
simply by including output of ls -a
in the completion words:
comp_words="(cat ~/.ssh/config | grep "^Host " | awk '{print $2}')"
comp_words="$comp_words ls -a"
That kinda works, but it’s not true path completion. For one thing, given a directory like this (where “foo” is a directory with a file “bar”):
$ ls
foo
$ ls foo/
bar
It’ll complete foo
, but it’ll never complete foo/bar
for obvious reasons.
I tried fiddling around with the way I generate completion words, but fundamentally bash completion just doesn’t work this way. I needed a different solution.
Solution
The solution lies in extra command line options for both the compgen
and complete
commands. Some relevant parts from their manual pages:
compgen [-V varname] [option] [word]
Generate possible completion matches for word according to the
options, which may be any option accepted by the complete builtin
with the exceptions of [...]
The complete
command has these relevant options:
-o comp-option
The comp-option controls several aspects of the compspec’s
behavior beyond the simple generation of completions.
comp-option may be one of:
filenames
Tell Readline that the compspec generates filenames, so
it can perform any filename-specific processing (such as
adding a slash to directory names, quoting special characters,
or suppressing trailing spaces). This option is intended to
be used with shell functions specified with -F.
and:
-A action
The action may be one of the following to generate a list of
possible completions:
file
File and directory names, similar to Readline’s filename
completion. May also be specified as -f.
When we use that -A
option on compgen
, we get this:
$ touch some_file
$ mkdir some_dir
$ compgen -A file -W "foo foobar bar" fo
foo
foobar
$ compgen -A file -W "foo foobar bar" so
some_dir
some_file
As we can see, it now includes both our custom words, as well as files and directories in the possible completions.
Now all we need is for the complete
command to understand that some of these are file or directory names, which we can do with the -o filenames
parameter.
The final code looks like this:
_complete_ssh ()
{
COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
comp_words="(cat ~/.ssh/config | grep "^Host " | awk '{print $2}')"
COMPREPLY=($(compgen -A file -W "${comp_words}" -- $cur))
return 0
}
complete -o filenames -F _complete_ssh ssh scp sftp sshfs
This has been something that’s been bugging me for a long time, so I’m glad I finally managed to fix it.