I was just working on a bash completion for my repo management tool (link) and came across a completion problem I couldn't find an answer to.

In my tool, all repositories are stored under a root source directory ~/src, and are three directories deep under that.

The top directory is the website, the second is the user, and the third is the repo itself. For example github.com/zig/ziglang.

My tool repo has a command repo cd <spec>, which will try to find a project matching the path you specify. It will check all paths three deep under the root and try to find one that matches the spec you give it. You don't have to specify the full path if you know there's only one project with the spec as part of its name. For example repo cd repo2 would match ~/src/github.com/dantecatalfamo/repo2. It will then move you to that directory.

I've gotten to the point where I have enough repositories from enough websites that I would like to be able to incrementally tab-complete the full three-tier path, as I sometimes forget which projects I've already cloned.

For example I would like to be able to type repo cd <tab><tab> and get a list of all websites.

$ repo cd <tab><tab>
chromium.googlesource.com/  gitlab.com/            git.meli.delivery/  git.zx2c4.com/      webrtc.googlesource.com/
bitbucket.org/              code.orgmode.org/      gitlab.winehq.org/  git.musl-libc.org/  humungus.tedunangst.com/
c9x.me/                     git.savannah.gnu.org/  github.com/         git.sr.ht/          mumble.net/

Then I'd like to be able to have that complete, and tab complete the user under that website, and then the project under that user.

$ repo cd github.com/raysan5/<tab><tab>
github.com/raysan5/physac  github.com/raysan5/raygui  github.com/raysan5/raylib

And then finally add a space once we've reached the third directory deep.

The problem is that there's no obvious way to accomplish that using the complete and compgen commands.

My first attempt was to use compgen -d "~/src/", but that came with some issues.

The first being that it would include the whole path to the directory, and since I wanted the command to work from anywhere, I had to use the absolute path to the source root and that full path would show up in the completions, which wasn't what I wanted.

I then tried using cd ~/src && compgen -d. This worked better but I ran into what ultimately ended up being the issue that made me create this post.

compgen would complete one level of directories, and then add a space after the completion instead of completing the directories under it.

My next attempt was to use the find command.

dirs=$(find ${src_root} -maxdepth 3 -mindepth 3 -type d -printf "%P\n")"
COMPREPLY=($(repo cd && compgen -W "${dirs}" "${COMP_WORDS[2]}"))

This roughly worked but came with its own issues. Since it was listing all three path components at once, between the hundreds of repositories I've cloned from several sites, it would produce so many results that I would always get the Display all 4967 possibilities? (y or n) warning every time. The other issue is that for the first double tab, I only want a list of sites, where this will list all repos of one site first, then all of another, pushing the actual list of sites very far apart.

It's possible to add your completion using the -o nospace command like this complete -F _repo_completions -o nospace repo, but that means it will never append a space to the end of your completions, which I don't want since I have subcommands other than cd that I want to always place a space after.

The solution I ended up with was this.

function _repo_completions {
    if [ "${#COMP_WORDS[@]}" -eq 2 ]; then
        COMPREPLY=($(compgen -W "cd clone help shell env ls new root reload" "${COMP_WORDS[1]}"))
        return;
    fi

    if [ "${#COMP_WORDS[@]}" -eq 3 ] && [ "${COMP_WORDS[1]}" == "cd" ]; then
        local only_slashes="${COMP_WORDS[2]//[^\/]}"
        local num_slashes="${#only_slashes}"
        if [ $num_slashes -lt 2 ]; then
            compopt -o nospace
            COMPREPLY=($(repo cd && compgen -d -S / "${COMP_WORDS[2]}"))
        else
            COMPREPLY=($(repo cd && compgen -d "${COMP_WORDS[2]}"))
        fi

        return;
    fi
}

complete -F _repo_completions repo

In the above code, repo cd changes directory to ~/src and the -S / option for compgen will append a trailing (suffix) slash to the end of the suggestion.

The key here is that you can use compopt to dynamically change completion options while running the completion, which lets me check how many directories deep we are into the completion by counting slashes, and disable appending a space until we're a full three directories deep into the completion.

Here is a good resource on bash completion for further reading. https://iridakos.com/programming/2018/03/01/bash-programmable-completion-tutorial