#+TITLE: Bash Incremental Directory Completion #+DATE: 2023-11-18T13:59:18-05:00 #+DRAFT: true #+DESCRIPTION: #+TAGS[]: #+KEYWORDS[]: #+SLUG: #+SUMMARY: 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 =, 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 = and get a list of all websites. #+begin_src $ repo cd 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/ #+end_src 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. #+begin_src $ repo cd github.com/raysan5/ github.com/raysan5/physac github.com/raysan5/raygui github.com/raysan5/raylib #+end_src 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. =$(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. #+begin_src bash 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 #+end_src 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