summaryrefslogtreecommitdiffstats
path: root/content/posts/bash-incremental-directory-completion/index.org
diff options
context:
space:
mode:
Diffstat (limited to 'content/posts/bash-incremental-directory-completion/index.org')
-rw-r--r--content/posts/bash-incremental-directory-completion/index.org124
1 files changed, 124 insertions, 0 deletions
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 <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.
+
+#+begin_src
+$ 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/
+#+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/<tab><tab>
+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