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