@@@ work in progress
[runlisp] / common.c
diff --git a/common.c b/common.c
new file mode 100644 (file)
index 0000000..b0d5857
--- /dev/null
+++ b/common.c
@@ -0,0 +1,374 @@
+/* -*-c-*-
+ *
+ * Common functionality of a less principled nature
+ *
+ * (c) 2020 Mark Wooding
+ */
+
+/*----- Licensing notice --------------------------------------------------*
+ *
+ * This file is part of Runlisp, a tool for invoking Common Lisp scripts.
+ *
+ * Runlisp is free software: you can redistribute it and/or modify it
+ * under the terms of the GNU General Public License as published by the
+ * Free Software Foundation; either version 3 of the License, or (at your
+ * option) any later version.
+ *
+ * Runlisp is distributed in the hope that it will be useful, but WITHOUT
+ * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
+ * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
+ * for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with Runlisp.  If not, see <https://www.gnu.org/licenses/>.
+ */
+
+/*----- Header files ------------------------------------------------------*/
+
+#include "config.h"
+
+#include <assert.h>
+#include <ctype.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include <dirent.h>
+#include <pwd.h>
+#include <unistd.h>
+
+#include <sys/stat.h>
+
+#include "common.h"
+#include "lib.h"
+
+/*----- Public variables --------------------------------------------------*/
+
+struct config config = CONFIG_INIT;
+struct config_section *toplevel, *builtin, *common, *env;
+unsigned verbose = 1;
+
+/*----- Internal utilities ------------------------------------------------*/
+
+static void escapify(struct dstr *d, const char *p)
+{
+  size_t n;
+
+  for (;;) {
+    n = strcspn(p, "\"'\\");
+    if (n) { dstr_putm(d, p, n); p += n; }
+    if (!*p) break;
+    dstr_putc(d, '\\'); dstr_putc(d, *p++);
+  }
+  dstr_putz(d);
+}
+
+static void homedir(struct dstr *d)
+{
+  static const char *home = 0;
+  const char *p;
+  struct passwd *pw;
+
+  if (!home) {
+    p = my_getenv("HOME", 0);
+    if (p) home = p;
+    else {
+      pw = getpwuid(getuid());
+      if (!pw) lose("can't find user in password database");
+      home = xstrdup(pw->pw_dir);
+    }
+  }
+  dstr_puts(d, home);
+}
+
+static void user_config_dir(struct dstr *d)
+{
+  const char *p;
+
+  p = my_getenv("XDG_CONFIG_HOME", 0);
+  if (p) dstr_puts(d, p);
+  else { homedir(d); dstr_puts(d, "/.config"); }
+}
+
+/*----- Miscellany --------------------------------------------------------*/
+
+const char *my_getenv(const char *name, const char *dflt)
+{
+  struct config_var *var;
+
+  var = config_find_var(&config, env, 0, name);
+  return (var ? var->val : dflt);
+}
+
+long parse_int(const char *what, const char *p, long min, long max)
+{
+  long n;
+  int oerr = errno;
+  char *q;
+
+  errno = 0;
+  n = strtol(p, &q, 0);
+  while (ISSPACE(*q)) q++;
+  if (errno || *q) lose("invalid %s `%s'", what, p);
+  if (n < min || n > max)
+    lose("%s %ld out of range (must be between %ld and %ld)",
+        what, n, min, max);
+  errno = oerr;
+  return (n);
+}
+
+void argv_string(struct dstr *d, const struct argv *av)
+{
+  size_t i;
+
+  for (i = 0; i < av->n; i++) {
+    if (i) { dstr_putc(d, ','); dstr_putc(d, ' '); }
+    dstr_putc(d, '`'); escapify(d, av->v[i]); dstr_putc(d, '\'');
+  }
+  dstr_putz(d);
+}
+
+/*----- File utilities ----------------------------------------------------*/
+
+int file_exists_p(const char *path, unsigned f)
+{
+  struct stat st;
+
+  if (stat(path, &st)) {
+    if (f&FEF_VERBOSE) moan("file `%s' not found", path);
+    return (0);
+  } else if (!(S_ISREG(st.st_mode))) {
+    if (f&FEF_VERBOSE) moan("`%s' is not a regular file", path);
+    return (0);
+  } else if ((f&FEF_EXEC) && access(path, X_OK)) {
+    if (f&FEF_VERBOSE) moan("file `%s' is not executable", path);
+    return (0);
+  } else {
+    if (f&FEF_VERBOSE) moan("found file `%s'", path);
+    return (1);
+  }
+}
+
+int found_in_path_p(const char *prog, unsigned f)
+{
+  struct dstr p = DSTR_INIT, d = DSTR_INIT;
+  const char *path;
+  char *q;
+  size_t n, avail, proglen;
+  int i, rc;
+
+  if (strchr(prog, '/'))
+    return (file_exists_p(prog, f));
+  path = my_getenv("PATH", 0);
+  if (path)
+    dstr_puts(&p, path);
+  else {
+    dstr_puts(&p, ".:");
+    i = 0;
+  again:
+    avail = p.sz - p.len;
+    n = confstr(_CS_PATH, p.p + p.len, avail);
+    if (avail > n) { i++; assert(i < 2); dstr_ensure(&p, n); goto again; }
+  }
+
+  q = p.p; proglen = strlen(prog);
+  for (;;) {
+    n = strcspn(q, ":");
+    dstr_reset(&d);
+    if (n) dstr_putm(&d, q, n);
+    else dstr_putc(&d, '.');
+    dstr_putc(&d, '/');
+    dstr_putm(&d, prog, proglen);
+    dstr_putz(&d);
+    if (file_exists_p(d.p, verbose >= 4 ? f : f&~FEF_VERBOSE)) {
+      if (verbose == 2) moan("found program `%s'", d.p);
+      rc = 1; goto end;
+    }
+    q += n; if (!*q) break; else q++;
+  }
+
+  rc = 0;
+end:
+  dstr_release(&p); dstr_release(&d);
+  return (rc);
+}
+
+int try_exec(struct argv *av, unsigned f)
+{
+  struct dstr d = DSTR_INIT;
+  int rc;
+
+  assert(av->n); argv_appendz(av);
+  if (verbose >= 2) { argv_string(&d, av); moan("trying %s...", d.p); }
+  if (f&TEF_DRYRUN) {
+    if (found_in_path_p(av->v[0], f&TEF_VERBOSE ? FEF_VERBOSE : 0))
+      { rc = 0; goto end; }
+  } else {
+    execvp(av->v[0], (/*unconst*/ char **)av->v);
+    if (errno != ENOENT) {
+      moan("failed to exec `%s': %s", av->v[0], strerror(errno));
+      _exit(2);
+    }
+  }
+
+  if (verbose >= 2) moan("`%s' not found", av->v[0]);
+  rc = -1;
+end:
+  dstr_release(&d);
+  return (rc);
+}
+
+/*----- Configuration -----------------------------------------------------*/
+
+void read_config_file(const char *what, const char *file, unsigned f)
+{
+  if (!config_read_file(&config, file, f)) {
+    if (verbose >= 2)
+      moan("read %s configuration file `%s'", what, file);
+  } else {
+    if (verbose >= 3)
+      moan("ignoring missing %s configuration file `%s'", what, file);
+  }
+}
+
+static int order_strings(const void *xx, const void *yy)
+  { const char *const *x = xx, *const *y = yy; return (strcmp(*x, *y)); }
+
+void read_config_dir(const char *what, const char *path, unsigned f)
+{
+  struct argv av = ARGV_INIT;
+  struct dstr dd = DSTR_INIT;
+  struct stat st;
+  DIR *dir;
+  struct dirent *d;
+  size_t i, n, len;
+
+  dir = opendir(path);
+  if (!dir) {
+    if (!(f&CF_NOENTOK) || errno != ENOENT)
+      lose("failed to read %s configuration directory `%s': %s",
+          what, path, strerror(errno));
+    if (verbose >= 3)
+      moan("ignoring missing %s configuration directory `%s'", what, path);
+    return;
+  }
+
+  dstr_puts(&dd, path); dstr_putc(&dd, '/'); n = dd.len;
+  for (;;) {
+    d = readdir(dir); if (!d) break;
+    len = strlen(d->d_name);
+    if (len < 5 || STRCMP(d->d_name + len - 5, !=, ".conf")) continue;
+    dd.len = n; dstr_putm(&dd, d->d_name, len); dstr_putz(&dd);
+    if (stat(dd.p, &st))
+      lose("failed to read file metadata for `%s': %s",
+          dd.p, strerror(errno));
+    if (!S_ISREG(st.st_mode)) continue;
+    argv_append(&av, xstrdup(d->d_name));
+  }
+
+  qsort(av.v, av.n, sizeof(*av.v), order_strings);
+
+  for (i = 0; i < av.n; i++) {
+    dd.len = n; dstr_puts(&dd, av.v[i]);
+    read_config_file(what, dd.p, f&~CF_NOENTOK);
+  }
+
+  for (i = 0; i < av.n; i++) free((/*unconst*/ char *)av.v[i]);
+  argv_release(&av); dstr_release(&dd); closedir(dir);
+  return;
+}
+
+void read_config_path(const char *path, unsigned f)
+{
+  struct stat st;
+
+  if (!stat(path, &st) && S_ISDIR(st.st_mode))
+    read_config_dir("command-line specified ", path, f);
+  else
+    read_config_file("command-line specified", path, f);
+}
+
+int set_config_var(const char *assign)
+{
+  struct config_section *sect;
+  const char *p, *q;
+
+  p = strchr(assign, '=');
+  if (!p) { moan("missing `=' in option assignment"); return (-1); }
+  q = strchr(assign, ':');
+  if (!q || q > p)
+    { sect = toplevel; q = assign; }
+  else {
+    sect = config_find_section_n(&config, CF_CREAT, assign, q - assign);
+    q++;
+  }
+  config_set_var_n(&config, sect, CF_LITERAL | CF_OVERRIDE,
+                  q, p - q, p + 1, strlen(p + 1));
+  return (0);
+}
+
+void init_config(void)
+{
+  toplevel = config_find_section(&config, CF_CREAT, "@CONFIG");
+  builtin = config_find_section(&config, CF_CREAT, "@BUILTIN");
+  common = config_find_section(&config, CF_CREAT, "@COMMON");
+  env = config_find_section(&config, CF_CREAT, "@ENV");
+  config_set_fallback(&config, common);
+  config_set_parent(builtin, 0);
+  config_set_parent(common, builtin);
+  config_set_parent(toplevel, 0);
+  config_read_env(&config, env);
+
+  config_set_var(&config, toplevel, CF_LITERAL, "data-dir",
+                my_getenv("RUNLISP_DATADIR", DATADIR));
+  config_set_var(&config, toplevel, CF_LITERAL, "image-dir",
+                my_getenv("RUNLISP_IMAGEDIR", IMAGEDIR));
+
+#ifdef ECL_OPTIONS_GNU
+  config_set_var(&config, builtin, CF_LITERAL, "@ECLOPT", "--");
+#else
+  config_set_var(&config, builtin, CF_LITERAL, "@ECLOPT", "-");
+#endif
+}
+
+void load_default_config(void)
+{
+  const char *p;
+  struct dstr d = DSTR_INIT;
+
+  p = my_getenv("RUNLISP_SYSCONFIG", ETCDIR "/runlisp.conf");
+  read_config_file("system", p, 0);
+  p = my_getenv("RUNLISP_SYSCONFIG_DIR", ETCDIR "/runlisp.d");
+  read_config_dir("system", p, CF_NOENTOK);
+  p = my_getenv("RUNLISP_USERCONFIG", 0);
+  if (p)
+    read_config_file("user", p, CF_NOENTOK);
+  else {
+    dstr_reset(&d); homedir(&d); dstr_puts(&d, "/.runlisp.conf");
+    read_config_file("user", d.p, CF_NOENTOK);
+    dstr_reset(&d); user_config_dir(&d); dstr_puts(&d, "/runlisp.conf");
+    read_config_file("user", d.p, CF_NOENTOK);
+  }
+  dstr_release(&d);
+}
+
+void dump_config(void)
+{
+  struct config_section_iter si;
+  struct config_section *sect;
+  struct config_var_iter vi;
+  struct config_var *var;
+  struct dstr d = DSTR_INIT;
+
+  for (config_start_section_iter(&config, &si);
+       (sect = config_next_section(&si)); )
+    for (config_start_var_iter(sect, &vi);
+        (var = config_next_var(&vi)); ) {
+      dstr_reset(&d); escapify(&d, var->val);
+      moan("config %s:%s = `%s'",
+          CONFIG_SECTION_NAME(sect), CONFIG_VAR_NAME(var), d.p);
+    }
+  dstr_release(&d);
+}
+
+/*----- That's all, folks -------------------------------------------------*/