Repositories / jai.git

jai.git

Clone (read-only): git clone http://git.guha-anderson.com/git/jai.git

Branch

add support for bash command-line completion

Author
David Mazieres <dm@uun.org>
Date
2026-03-26 18:04:38 -0700
Commit
ce7c8e2e2b566155105e63a2adbbec4062e81f79
Makefile.am
index 23b06c0..8e389c5 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -14,7 +14,10 @@ jai.1: jai.1.md
 sysusersdir = $(prefix)/lib/sysusers.d
 sysusers_DATA = jai.conf
 
-EXTRA_DIST = jai.1 jai.1.md jai.conf.in logo.svg
+bashcompdir = $(datadir)/bash-completion/helpers
+bashcomp_DATA = bash-completion/jai
+
+EXTRA_DIST = jai.1 jai.1.md jai.conf.in jai.bash logo.svg
 
 CLEANFILES = *~
 DISTCLEANFILES = jai.conf
@@ -36,11 +39,13 @@ install-exec-hook:
 	-chmod 04511 $(DESTDIR)$(bindir)/jai
 
 install-data-hook:
-	test ! -r $(DESTDIR)/etc/passwd || systemd-sysusers --root=$(DESTDIR)/
+	test ! -r "$(DESTDIR)/etc/passwd" || \
+		systemd-sysusers --root="$(DESTDIR)/"
 
 uninstall-hook:
-	@if gecos=$$(getent passwd @UNTRUSTED_USER@ 2>/dev/null | cut -d: -f5) \
+	@if test -r "$(DESTDIR)/etc/passwd" && \
+	gecos=$$(getent passwd @UNTRUSTED_USER@ 2>/dev/null | cut -d: -f5) \
 	    && test "$$gecos" = "JAI sandbox untrusted user"; then \
 	  echo "userdel @UNTRUSTED_USER@"; \
-	  userdel @UNTRUSTED_USER@; \
+	  userdel -R "$(DESTDIR)/" @UNTRUSTED_USER@; \
 	fi
bash-completion/jai
new file mode 100644
index 0000000..79210c7
--- /dev/null
+++ b/bash-completion/jai
@@ -0,0 +1,36 @@
+# -*- shell-script -*-
+
+_jai()
+{
+    local cur prev words cword
+    _init_completion -n = || return
+
+    # Ask jai itself for completions.  Pass every word except $0,
+    # including the current (possibly incomplete) word.
+    local -a comp_args=( "${words[@]:1}" )
+    local output
+    output=$(jai --complete "${comp_args[@]}" 2>/dev/null) || return
+
+    # If jai returns the special completion "_command_offset N", it
+    # means subcommand starts at offset N, so delegate to
+    # bash-completion's _command_offset which handles completing an
+    # arbitrary command (c.f. sudo).
+    if [[ $output == _command_offset\ * ]]; then
+        local offset=${output#_command_offset }
+        _command_offset "$offset"
+        return
+    fi
+
+    # Otherwise jai returned sorted candidate completions, one per
+    # line, including a trailing space if the argument is complete, so
+    # no need for bash to add spaces.
+    compopt -o nospace -o nosort
+
+    COMPREPLY=()
+    while IFS= read -r line; do
+	# Note bash COMP_WORDBREAKS splits on '=' and only completes
+	# the part after it, so we need to strip off the first =.
+        COMPREPLY+=( "${line#--*=}" )
+    done <<< "$output"
+} &&
+complete -F _jai jai
complete.cc
index 81013ca..f77a880 100644
--- a/complete.cc
+++ b/complete.cc
@@ -46,7 +46,7 @@ complete_path(int dfd, CompSet &c, bool dir_only)
     if (dir_only && de->d_type != DT_DIR)
       continue;
     c.output("{}{}", (arg.parent_path() / d_name(de)).string(),
-             de->d_type == DT_DIR ? "/" : "");
+             de->d_type == DT_DIR ? "/" : " ");
   }
 }
 
@@ -76,9 +76,9 @@ complete_env(CompSet &c, bool eq)
     if (auto pos = var.find('='); pos == var.npos)
       continue;
     else
-      var = var.substr(0, eq ? pos + 1 : pos);
+      var = var.substr(0, pos);
     if (var.starts_with(arg))
-      c.output("{}", var);
+      c.output("{}{}", var, eq ? "=" : " ");
   }
 }
 
@@ -87,7 +87,7 @@ Config::complete(Completions c)
 {
   using enum Completions::Disposition;
   if (c.kind >= 0) {
-    std::println("_command_offset {}", c.kind);
+    std::println("_command_offset {}", c.kind - 1);
     return 0;
   }
   if (c.kind == kNoCompletions)
@@ -106,7 +106,7 @@ Config::complete(Completions c)
     auto arg = c.arg();
     for (std::string_view sv : {"casual", "strict", "bare"}) {
       if (sv.starts_with(arg))
-        cs.output("{}", sv);
+        cs.output("{} ", sv);
     }
   }
   else if (std::ranges::contains(