Repositories / jai.git

jai.git

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

Branch

expand environment variables in setenv and storage

Author
David Mazieres <dm@uun.org>
Date
2026-03-20 23:19:12 -0700
Commit
0959b420244c2a8f2257e885630cfea684e715e4
jai.1.md
index 928811c..e3873f0 100644
--- a/jai.1.md
+++ b/jai.1.md
@@ -286,6 +286,11 @@ opencode`):
     If the argument contains `=`, then *var* is always treated as a
   variable, not a pattern, and it is assigned *value* in the jail.
 
+    If *value* contains the pattern `${`*envvar*`}`, it will be
+  replaced by the value of the evironment variable *envvar* at the
+  time jai was invoked.  If value contains `\`, it escapes the next
+  character.
+
 `--storage` *dir*
 : Specify an alternate location in which to store private home
   directories and overlays.  The default is `$JAI_CONFIG_DIR` if set,
@@ -293,6 +298,9 @@ opencode`):
   you may wish to use storage on a local file system, as NFS does not
   support the extended attributes required by overlay file systems.
 
+    Like `--setenv`, `--storage` expands `${`*envvar*`}` patterns and
+uses `\` to escape the next character.
+
 `--command` *bash-command*
 : jai launches the jailed program you specify by running "`/bin/bash
   -c` *bash-command* *cmd* *arg*...".  By default, *bash-command* just
jai.cc
index efeb509..4d3bc77 100644
--- a/jai.cc
+++ b/jai.cc
@@ -125,7 +125,8 @@ Config::parse_config_file(path file, Options *opts)
 {
   bool slash = std::ranges::distance(file.begin(), file.end()) > 1;
 
-  if (struct stat sb; !slash && file.extension() != ".conf" &&
+  if (struct stat sb;
+      !slash && file.extension() != ".conf" &&
       fstatat(home_jai(), file.c_str(), &sb, 0) && errno == ENOENT &&
       ~fstatat(home_jai(), cat(file, ".conf").c_str(), &sb, 0) &&
       S_ISREG(sb.st_mode))
@@ -971,8 +972,11 @@ Config::opt_parser()
   opts(
       "--setenv",
       [this](std::string var) {
-        if (auto pos = var.find('='); pos != var.npos)
-          setenv_.insert_or_assign(var.substr(0, pos), var);
+        if (auto pos = var.find('='); pos != var.npos) {
+          auto var_eq_val = std::format("{}{}", var.substr(0, pos + 1),
+                                        var_expand(var.substr(pos + 1)));
+          setenv_.insert_or_assign(var.substr(0, pos), var_eq_val);
+        }
         else if (auto it = env_filter_.find(var); it != env_filter_.end())
           env_filter_.erase(it);
         else if (var.contains(' '))
@@ -993,10 +997,11 @@ Config::opt_parser()
   opts(
       "--storage",
       [this](std::string_view s) {
+        auto sd = var_expand(s);
         if (dir_relative_to_home_)
-          storagedir_ = homepath_ / s;
+          storagedir_ = homepath_ / sd;
         else
-          storagedir_ = s;
+          storagedir_ = sd;
       },
       R"(Store overlay and private home directories in DIR
 (default: $JAI_CONFIG_DIR or $HOME/.jai))",
jai.h
index 956f210..d5cf80b 100644
--- a/jai.h
+++ b/jai.h
@@ -10,6 +10,33 @@
 #include <sys/syscall.h>
 #include <unistd.h>
 
+inline const char *
+env_or_empty(std::string_view var)
+{
+  const char *p = getenv(std::string(var).c_str());
+  return p ? p : "";
+}
+
+// Calls exp("VAR"sv) to expand strings like "123${VAR}456".
+template<typename Exp = decltype(env_or_empty)>
+std::string
+var_expand(std::string_view in, Exp &&exp = env_or_empty)
+    requires requires(std::string r) { r += exp(in); }
+{
+  std::string ret;
+  for (std::size_t i = 0, e = in.size(); i < e;)
+    if (in[i] == '\\')
+      ret += (++i < e ? in[i++] : '\\');
+    else if (size_t j;
+             in.substr(i, 2) == "${" && (j = in.find('}', i + 2)) != in.npos) {
+      ret += exp(in.substr(i + 2, j - i - 2));
+      i = j + 1;
+    }
+    else
+      ret += in[i++];
+  return ret;
+}
+
 inline pid_t
 xfork(std::uint64_t flags = 0)
 {