Electricmonk

Ferry Boender

Programmer, DevOpper, Open Source enthusiast.

Blog

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.

The text of all posts on this blog, unless specificly mentioned otherwise, are licensed under this license.