Repositories / jai.git

jai.git

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

Branch

add --complete argument

Author
David Mazieres <dm@uun.org>
Date
2026-03-26 16:48:28 -0700
Commit
6b8517b476bd478877ff035ef837827adea20413
Makefile.am
index d87a285..23b06c0 100644
--- a/Makefile.am
+++ b/Makefile.am
@@ -2,8 +2,8 @@ bin_PROGRAMS = jai
 
 AM_CXXFLAGS = $(MOUNT_CFLAGS) $(LIBACL_CFLAGS)
 
-jai_SOURCES = cred.cc default_conf.cc fs.cc jai.cc options.cc cred.h	\
-argtype.h defer.h err.h fs.h jai.h
+jai_SOURCES = complete.cc cred.cc default_conf.cc fs.cc jai.cc	\
+options.cc cred.h argtype.h defer.h err.h fs.h jai.h
 jai_LDADD = $(MOUNT_LIBS) $(LIBACL_LIBS)
 
 man1_MANS = jai.1
complete.cc
new file mode 100644
index 0000000..a5f8904
--- /dev/null
+++ b/complete.cc
@@ -0,0 +1,129 @@
+#include "jai.h"
+
+#include <cassert>
+#include <dirent.h>
+#include <print>
+
+using Completions = Options::Completions;
+
+struct CompSet {
+  const Completions &comp_;
+  std::set<std::string> out_;
+
+  CompSet(const Completions &comp) noexcept : comp_(comp) {}
+
+  auto opt() const { return comp_.arg(); }
+  auto arg() const { return comp_.arg(); }
+
+  // Format one completion to be output
+  template<typename... Args>
+  void output(std::format_string<Args...> fmt, Args &&...args)
+  {
+    out_.insert(
+        std::format("{}{}", comp_.prepend(),
+                    std::vformat(fmt.get(), std::make_format_args(args...))));
+  }
+};
+
+static void
+complete_path(int dfd, CompSet &c, bool dir_only)
+{
+  path arg(c.arg());
+  std::string stem = arg.filename();
+
+  auto d = try_opendir(dfd, arg.parent_path(), kFollow);
+  if (!d)
+    return;
+  while (auto de = readdir(*d)) {
+    std::string_view name = d_name(de);
+    if (name == "." || name == ".." || !name.starts_with(stem))
+      continue;
+    if (de->d_type == DT_UNKNOWN || de->d_type == DT_LNK) {
+      struct stat sb;
+      if (fstatat(dirfd(*d), d_name(de), &sb, 0) && S_ISDIR(sb.st_mode))
+        de->d_type = DT_DIR;
+    }
+    if (dir_only && de->d_type != DT_DIR)
+      continue;
+    c.output("{}{}", (arg.parent_path() / d_name(de)).string(),
+             de->d_type == DT_DIR ? "/" : "");
+  }
+}
+
+static void
+complete_config(int cfd, CompSet &c, std::string ext)
+{
+  auto d = try_opendir(cfd);
+  if (!d)
+    return;
+  while (auto de = readdir(*d)) {
+    path f = d_name(de);
+    if (f.extension() != ext)
+      continue;
+    std::string name = f.replace_extension();
+    if (!name.starts_with(c.arg()))
+      continue;
+    c.output("{}", name);
+  }
+}
+
+static void
+complete_env(CompSet &c, bool eq)
+{
+  std::string_view arg;
+  for (char **e = environ; *e; ++e) {
+    std::string_view var = *e;
+    if (auto pos = var.find('='); pos == var.npos)
+      continue;
+    else
+      var = var.substr(0, eq ? pos + 1 : pos);
+    c.output("{}", var);
+  }
+}
+
+int
+Config::complete(Completions c)
+{
+  using enum Completions::Disposition;
+  if (c.kind >= 0) {
+    std::println("_command_offset {}", c.kind);
+    return 0;
+  }
+  if (c.kind == kNoCompletions)
+    return 1;
+  else if (c.kind == kRawCompletions) {
+    for (const auto &v : c.vals)
+      std::println("{}", v);
+    return 0;
+  }
+  assert(c.kind == kArgCompletions);
+
+  std::string_view opt = c.vals[0], arg = c.vals[1], prefix = c.vals[2];
+  CompSet cs(c);
+
+  if (std::ranges::contains(std::array{"-m", "--mode"}, opt)) {
+    auto arg = c.arg();
+    for (std::string_view sv : {"casual", "strict", "bare"}) {
+      if (sv.starts_with(arg))
+        cs.output("{}", sv);
+    }
+  }
+  else if (std::ranges::contains(
+          std::array{"-d", "--dir", "-x", "--xdir", "--storage"}, opt))
+    complete_path(AT_FDCWD, cs, true);
+  else if (std::ranges::contains(std::array{"--mask", "--unmask"}, opt))
+    complete_path(home(), cs, false);
+  else if (std::ranges::contains(std::array{"-C", "--conf"}, opt))
+    complete_config(home_jai(), cs, ".conf");
+  else if (std::ranges::contains(std::array{"-j", "--jail"}, opt))
+    complete_config(storage(), cs, ".jail");
+  else if (opt == "--setenv")
+    complete_env(cs, true);
+  else if (opt == "--unsetenv")
+    complete_env(cs, false);
+
+  for (const auto comp : cs.out_)
+    std::println("{}", comp);
+
+  return 0;
+}
fs.h
index 768df5a..269ccb6 100644
--- a/fs.h
+++ b/fs.h
@@ -229,21 +229,36 @@ xdup(int fd, int minfd = 3)
   return ret;
 }
 
-inline RaiiHelper<closedir>
-xopendir(int dfd, path file = {}, FollowLinks follow = kNoFollow)
+inline std::expected<RaiiHelper<closedir>, std::system_error>
+try_opendir(int dfd, path file = {}, FollowLinks follow = kNoFollow)
 {
-  Fd fd;
   if (file.empty())
-    fd = xdup(dfd);
-  else
-    fd = xopenat(dfd, file,
-                 O_RDONLY | O_DIRECTORY |
-                     (follow == kNoFollow ? O_NOFOLLOW : 0));
+    // re-open in case dfd is O_PATH and to avoid messing with the
+    // offset of dfd if we read the directory multiple times
+    file = ".";
+  Fd fd =
+      openat(dfd, file.c_str(),
+             O_RDONLY | O_DIRECTORY | (follow == kNoFollow ? O_NOFOLLOW : 0));
+  if (!fd)
+    return std::unexpected{
+        std::system_error(errno, std::system_category(), fdpath(dfd, file))};
+
   if (auto d = fdopendir(*fd)) {
     fd.release();
     return d;
   }
-  syserr("fdopendir({})", fdpath(dfd, file));
+  return std::unexpected{
+      std::system_error(errno, std::system_category(),
+                        std::format("{}: fdopendir", fdpath(*fd)))};
+}
+
+inline RaiiHelper<closedir>
+xopendir(int dfd, path file = {}, FollowLinks follow = kNoFollow)
+{
+  if (auto r = try_opendir(dfd, file, follow))
+    return std::move(*r);
+  else
+    throw r.error();
 }
 
 // dirent::d_name is an array, so won't convert properly to types that
jai.1.md
index 2aff14e..2b3f853 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -405,6 +405,16 @@ uses `\` to escape the next character.
 `--version`
 : Prints the version number and copyright and exit.
 
+`--complete`
+: This option is only valid as the first option on the command line.
+  It tells jai not to do anything, but it prints a list of completions
+  to assist shells in doing command completion.  For example: `jai
+  --complete -m "c"` prints `casual`.  If the commands line is
+  complete, then it will output the special string `_command_offset
+  `*N*, which indicates that argument *N* is the start of a new
+  command that should be completed according to the rules for that
+  command.
+
 # ENVIRONMENT
 
 The following environment variables affect jai's operation:
jai.cc
index 966d581..e57cfcd 100644
--- a/jai.cc
+++ b/jai.cc
@@ -602,7 +602,7 @@ clean_root_owned_dir(int dfd, path file)
   }
 }
 
-void
+bool
 Config::unmountall()
 {
   Fd lock;
@@ -643,9 +643,9 @@ Config::unmountall()
   unlinkat(run_jai_user(), ".lock", 0);
   lock.reset();
   unlinkat(run_jai(), user_.c_str(), AT_REMOVEDIR);
-}
 
-extern "C" char **environ;
+  return unmount_ok;
+}
 
 std::vector<const char *>
 Config::make_env()
@@ -1100,7 +1100,7 @@ version 3 or later; see the file named COPYING for details.)",
   exit(0);
 }
 
-void
+int
 do_main(int argc, char **argv)
 {
   Config conf;
@@ -1137,6 +1137,9 @@ The default is CMD.conf if it exists, otherwise default.conf)",
       "Show default contents of $JAI_CONFIG_DIR/.defaults");
   option_help = opts->help();
 
+  if (argc > 2 && !strcmp(argv[1], "--complete"))
+    return conf.complete(opts->complete_args(2, argc, argv));
+
   std::vector<char *> cmd;
   try {
     cmd.assign_range(opts->parse_argv(argc, argv));
@@ -1160,7 +1163,7 @@ The default is CMD.conf if it exists, otherwise default.conf)",
     std::println(
         "Run {} --print-defaults to see the original contents of that file.",
         prog.filename().string());
-    exit(0);
+    return 0;
   }
 
   if (opt_u) {
@@ -1169,8 +1172,7 @@ The default is CMD.conf if it exists, otherwise default.conf)",
       usage(2);
     }
     restore.reset();
-    conf.unmountall();
-    return;
+    return conf.unmountall() ? 0 : 1;
   }
 
   if (!opt_C.empty()) {
@@ -1208,6 +1210,7 @@ The default is CMD.conf if it exists, otherwise default.conf)",
   auto fd = conf.make_mnt_ns();
   cmd.push_back(nullptr);
   conf.exec(*fd, cmd.data());
+  return 0;
 }
 
 int
@@ -1227,10 +1230,9 @@ main(int argc, char **argv)
 #endif
 
   try {
-    do_main(argc, argv);
+    exit(do_main(argc, argv));
   } catch (const ToCatch &e) {
     warn("{}", e.what());
-    return 1;
   }
-  return 0;
+  return 1;
 }
jai.h
index 063800e..bb3e3ec 100644
--- a/jai.h
+++ b/jai.h
@@ -14,6 +14,8 @@
 #include <sys/syscall.h>
 #include <unistd.h>
 
+extern "C" char **environ;
+
 inline const char *
 env_or_empty(std::string_view var)
 {
@@ -115,9 +117,10 @@ struct Config {
   Fd make_mnt_ns();
   void exec(int nsfd, char **argv);
   void unmount();
-  void unmountall();
+  bool unmountall();
   std::unique_ptr<Options> opt_parser(bool dotjail = false);
 
+  int complete(Options::Completions c);
   void parse_config_fd(int fd, Options *opts = nullptr);
   bool parse_config_file(path file, Options *opts = nullptr);
   std::vector<const char *> make_env();
options.h
index fe0c2ef..d1fed5a 100644
--- a/options.h
+++ b/options.h
@@ -88,14 +88,15 @@
 
 #include "err.h"
 
+#include <cassert>
 #include <charconv>
 #include <concepts>
 #include <cstring>
-#include <format>
 #include <functional>
 #include <initializer_list>
 #include <map>
 #include <memory>
+#include <print>
 #include <span>
 #include <utility>
 #include <vector>
@@ -188,15 +189,18 @@ struct Option : std::string_view {
 
 struct Options {
   struct Completions {
-    static constexpr int kNoCompletions = -1;
-    static constexpr int kRawCompletions = -3;
-    static constexpr int kArgCompletions = -4;
+    enum Disposition {
+      kNoCompletions = -1,
+      kRawCompletions = -2,
+      kArgCompletions = -3,
+    };
 
     // If kind >= 0, then argv[kind] is the first non-option argument,
-    // meaning the first argument that is not a valid syntactic
-    // argument (at least 2 characters, first character '-', total
-    // length 2 if and only if the second argument is not '-').  If
-    // there is a "--" argument, then kind is the position after that.
+    // meaning the first argument that is not a valid syntactic option
+    // (at least 2 characters, first character '-', total length 2 if
+    // and only if the second argument is not '-') or the argument to
+    // such an option.  If there is a "--" argument, then kind is the
+    // position after that.
     //
     // If kind is kNoCompletions, then something went wrong (an
     // invalid argument somewhere) and nothing can be completed.
@@ -206,21 +210,22 @@ struct Options {
     // E.g., if completing "-", it would include all options (both
     // short and long).
     //
-    // If kind is kArgCompletions, then vals must contain exactly 3
-    // elements as follows:
+    // If kind is kArgCompletions, then we are completing the argument
+    // to a fully determined option,and vals has 3 elements as
+    // follows:
     //
-    //    - arg[0] is the argument to be completed (e.g., "-d" or "--dir")
+    //    - opt() is the option being completed (e.g., "-d" or "--dir")
     //
-    //    - arg[1] is the current prefix of the argument.  E.g., if
-    //      the argument being completed is "--dir=/usr/lo" or just
-    //      "/usr/lo" following "--dir", then it would be "/usr/lo".
+    //    - arg() is the current argument stem.  E.g., if the argument
+    //      being completed is "--dir=/usr/lo" or just "/usr/lo"
+    //      following "--dir", then it would be "/usr/lo".
     //
-    //    - arg[2] is the prefix to prepend to completions generated.
-    //      In the case that the value is a separate argv element
-    //      (e.g., completing {"--dir", "/usr/lo"}) arg[2] will be
-    //      empty.  In the case that the option and argument are in
-    //      one argv element such as "--dir=/usr/lo" or "-mcasu", then
-    //      it argv[2] would be "--dir=" or "-m".
+    //    - prepend() is what to prepend to completions generated.  In
+    //      the case that the value is a separate argv element (e.g.,
+    //      completing {"--dir", "/usr/lo"}) prepend() will be empty.
+    //      In the case that the option and argument are in one argv
+    //      element such as "--dir=/usr/lo" or "-mcasu", then
+    //      prepend() would be "--dir=" or "-m".
     int kind = kNoCompletions;
 
     std::vector<std::string> vals;
@@ -230,6 +235,15 @@ struct Options {
     Completions(int k, std::vector<std::string> v) noexcept
       : kind(k), vals(std::move(v))
     {}
+
+    std::string_view index_vals(size_t n) const
+    {
+      assert(kind == kArgCompletions && vals.size() == 3 && n < 3);
+      return vals[n];
+    }
+    auto opt() const { return index_vals(0); }
+    auto arg() const { return index_vals(1); }
+    auto prepend() const { return index_vals(2); }
   };
 
   using enum Action::HasArg;