From ccb3fbd483bcff750733fa41957f43f1f017f496 Mon Sep 17 00:00:00 2001 From: Dante Catalfamo Date: Sat, 18 Nov 2023 17:16:00 -0500 Subject: Begin shaky article on compgen --- .../index.org | 124 +++++++++++++++++++++ 1 file changed, 124 insertions(+) create mode 100644 content/posts/bash-incremental-directory-completion/index.org diff --git a/content/posts/bash-incremental-directory-completion/index.org b/content/posts/bash-incremental-directory-completion/index.org new file mode 100644 index 0000000..1bce8b3 --- /dev/null +++ b/content/posts/bash-incremental-directory-completion/index.org @@ -0,0 +1,124 @@ +#+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 + +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. + + +existing resouces https://iridakos.com/programming/2018/03/01/bash-programmable-completion-tutorial -- cgit v1.2.3