@@@ more wip
[runlisp] / common.c
index b0d5857..1c0b0ce 100644 (file)
--- a/common.c
+++ b/common.c
 
 /*----- Public variables --------------------------------------------------*/
 
-struct config config = CONFIG_INIT;
-struct config_section *toplevel, *builtin, *common, *env;
-unsigned verbose = 1;
+struct config config = CONFIG_INIT;    /* main configuration */
+struct config_section *toplevel, *builtin, *common, *env; /* well-known
+                                                          * sections */
+unsigned verbose = 1;                  /* verbosity level */
 
-/*----- Internal utilities ------------------------------------------------*/
+/*----- Miscellany --------------------------------------------------------*/
+
+/* Look up the environment variable NAME.
+ *
+ * If it's found, return the value; otherwise return DFLT.  This function
+ * looks up the environment variable in the `@ENV' configuration section, so
+ * (a) it's likely more efficient than getenv(3), and (b) the `init_config'
+ * function must have been called earlier.
+ */
+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);
+}
+
+/* Parse and return an integer from the string P.
+ *
+ * Report an error if the string doesn't look like an integer, or if it's not
+ * between MIN and MAX (inclusive).  Qualify error messages using the
+ * adjective WHAT.
+ */
+long parse_int(const char *what, const char *p, long min, long max)
+{
+  long n;
+  int oerr = errno;
+  char *q;
 
-static void escapify(struct dstr *d, const char *p)
+  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);
+}
+
+/* Append a word P to string D, quoting and/or escaping it in shell style.
+ *
+ * It tries to pick a `good' way to protect metacharacters, but the precise
+ * details aren't guaranteed to remain stable.
+ */
+static void putword(struct dstr *d, const char *p)
 {
+  unsigned bare = 0, sq = 2, dq = 2;
+  const char *q, *e, *f;
   size_t n;
 
+  /* Pass one: count up how many extra escaping and/or quoting characters
+   * we'd need for each quoting strategy: `bare' is no quoting, just adding
+   * toothpicks before naughty characters; `dq' is double quotes, with fewer
+   * toothpicks; and `sq' is single quotes, with the somewhat awful rune
+   * `'\''' rune replacing embedded single quotes.  The quoting strategies
+   * start off with a two-character penalty for the surrounding quotes.
+   */
+  for (q = p; *q; q++)
+    switch (*q) {
+      case '\\': case '"': case '`': case '$': case '!': bare++; dq++; break;
+      case '\'': bare++; sq += 3; break;
+      case '^': case '|': case ';': case '&': case '(': case ')':
+      case '<': case '>':
+      case '*': case '?': case '[':
+      case '#':
+       bare++; break;
+      default:
+       if (ISSPACE(*q)) bare++;
+       break;
+    }
+
+  /* Prepare for the output loop: `q' will be a string of naughty characters
+   * which need escaping somehow; `e' is a sequence to insert before each
+   * naughty character, and `f' is a final string to add to the end.  We'll
+   * put the initial quote on ourselves, if necessary.
+   */
+  if (bare < dq && bare < sq)
+    { q = "\\\"`$!'^|;&()<>*?[# \b\f\n\r\t\v"; e = "\\"; f = ""; }
+  else if (dq < sq)
+    { q = "\\\"`$!"; e = "\\"; dstr_putc(d, '"'); f = "\""; }
+  else
+    { q = "'"; e = "'\\'"; dstr_putc(d, '\''); f = "'"; }
+
+  /* Work through the input string inserting escapes as we go. */
   for (;;) {
-    n = strcspn(p, "\"'\\");
+    n = strcspn(p, q);
     if (n) { dstr_putm(d, p, n); p += n; }
     if (!*p) break;
-    dstr_putc(d, '\\'); dstr_putc(d, *p++);
+    dstr_puts(d, e); dstr_putc(d, *p++);
+  }
+  dstr_puts(d, f);
+}
+
+/* Format string-vector AV as a sequence of possibly-quoted words.
+ *
+ * Append the resulting list to D.
+ */
+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, ' ');
+    putword(d, av->v[i]);
   }
   dstr_putz(d);
 }
 
+/*----- Internal utilities ------------------------------------------------*/
+
+/* Append the user's home directory to D. */
 static void homedir(struct dstr *d)
 {
   static const char *home = 0;
@@ -70,6 +168,7 @@ static void homedir(struct dstr *d)
   struct passwd *pw;
 
   if (!home) {
+
     p = my_getenv("HOME", 0);
     if (p) home = p;
     else {
@@ -81,6 +180,7 @@ static void homedir(struct dstr *d)
   dstr_puts(d, home);
 }
 
+/* Append the user's XDG configuration directory to D. */
 static void user_config_dir(struct dstr *d)
 {
   const char *p;
@@ -90,46 +190,15 @@ static void user_config_dir(struct dstr *d)
   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 ----------------------------------------------------*/
 
+/* Return whether PATH names an existing file.
+ *
+ * This will return zero if PATH names something which isn't a regular file.
+ * If `FEF_EXEC' is set in F, then additionally ensure that it's executable
+ * by the (real) calling uid.  If `FEF_VERBOSE' is set in F, then report on
+ * the outcome of the check to standard error.
+ */
 int file_exists_p(const char *path, unsigned f)
 {
   struct stat st;
@@ -149,6 +218,13 @@ int file_exists_p(const char *path, unsigned f)
   }
 }
 
+/* Return whether PROG can be found in the `PATH'.
+ *
+ * If PROG is a pathname (absolute or relative -- i.e., if it contains a
+ * `/'), then just check that it names an executable program.  Otherwise
+ * check to see whether `DIR/PROG' exists and is executable for any DIR in
+ * the `PATH'.  The flags F are as for `file_exists_p'.
+ */
 int found_in_path_p(const char *prog, unsigned f)
 {
   struct dstr p = DSTR_INIT, d = DSTR_INIT;
@@ -158,7 +234,7 @@ int found_in_path_p(const char *prog, unsigned f)
   int i, rc;
 
   if (strchr(prog, '/'))
-    return (file_exists_p(prog, f));
+    return (file_exists_p(prog, f | FEF_EXEC));
   path = my_getenv("PATH", 0);
   if (path)
     dstr_puts(&p, path);
@@ -180,7 +256,7 @@ int found_in_path_p(const char *prog, unsigned f)
     dstr_putc(&d, '/');
     dstr_putm(&d, prog, proglen);
     dstr_putz(&d);
-    if (file_exists_p(d.p, verbose >= 4 ? f : f&~FEF_VERBOSE)) {
+    if (file_exists_p(d.p, (verbose >= 4 ? f : f&~FEF_VERBOSE) | FEF_EXEC)) {
       if (verbose == 2) moan("found program `%s'", d.p);
       rc = 1; goto end;
     }
@@ -193,6 +269,15 @@ end:
   return (rc);
 }
 
+/* Try to run a program as indicated by the argument list AV.
+ *
+ * This is essentially execvp(3).  If `TEF_VERBOSE' is set in F then trace
+ * what's going on to standard error.  If `TEF_DRYRUN' is set in F then don't
+ * actually try to run the program: just check whether it exists and is
+ * vaguely plausible.  Return -1 if there was a problem, or 0 if it was
+ * successful but didn't actually run the program because of the flags
+ * settings.
+ */
 int try_exec(struct argv *av, unsigned f)
 {
   struct dstr d = DSTR_INIT;
@@ -204,7 +289,7 @@ int try_exec(struct argv *av, unsigned f)
     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);
+    execvp(av->v[0], av->v);
     if (errno != ENOENT) {
       moan("failed to exec `%s': %s", av->v[0], strerror(errno));
       _exit(2);
@@ -220,6 +305,54 @@ end:
 
 /*----- Configuration -----------------------------------------------------*/
 
+/* Initialize the configuration machinery.
+ *
+ * This establishes the standard configuration sections `@CONFIG',
+ * `@BUILTIN', `@COMMON', and `@ENV', setting the corresponding global
+ * variables, and populates `@BUILTIN' (from compile-time configuration) and
+ * `@ENV' (from the environment variables).
+ */
+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(env, 0);
+  config_set_parent(toplevel, 0);
+  config_read_env(&config, env);
+
+  config_set_var(&config, builtin, CF_LITERAL,
+                "@%data-dir", DATADIR);
+  config_set_var(&config, builtin, 0,
+                "@data-dir", "${@ENV:RUNLISP_DATADIR?"
+                              "${@CONFIG:data-dir?"
+                                "${@BUILTIN:@%data-dir}}}");
+  config_set_var(&config, builtin, CF_LITERAL,
+                "@%image-dir", IMAGEDIR);
+  config_set_var(&config, builtin, 0,
+                "@image-dir", "${@ENV:RUNLISP_IMAGEDIR?"
+                               "${@CONFIG:image-dir?"
+                                 "${@BUILTIN:@%image-dir}}}");
+
+#ifdef ECL_OPTIONS_GNU
+  config_set_var(&config, builtin, CF_LITERAL, "@%ecl-opt", "--");
+#else
+  config_set_var(&config, builtin, CF_LITERAL, "@%ecl-opt", "-");
+#endif
+  config_set_var(&config, builtin, 0,
+                "@ecl-opt", "${@CONFIG:ecl-opt?${@BUILTIN:@%ecl-opt}}");
+}
+
+/* Read a named configuration FILE.
+ *
+ * WHAT is an adjective describing the configuration file, to be used in
+ * diagnostics; FILE is the actual filename to read; and F holds `CF_...'
+ * flags for `config_read_file', which actually does most of the work.
+ */
 void read_config_file(const char *what, const char *file, unsigned f)
 {
   if (!config_read_file(&config, file, f)) {
@@ -231,9 +364,24 @@ void read_config_file(const char *what, const char *file, unsigned f)
   }
 }
 
+/* Order strings lexicographically.
+ *
+ * This function is intended to be passed an argument to qsort(3).
+ */
 static int order_strings(const void *xx, const void *yy)
   { const char *const *x = xx, *const *y = yy; return (strcmp(*x, *y)); }
 
+/* Read all of the configuration files in directory PATH.
+ *
+ * WHAT is an adjective describing the configuration directory, to be used in
+ * diagnostics; FILE is the actual filename to read; and F holds `CF_...'
+ * flags for `config_read_file', which actually reads the files.
+ *
+ * All of the files named `*.conf' in the directory are read, in ascending
+ * lexicographical order by name.  If `CF_NOENTOK' is set in F, then ignore
+ * an error explaining that the directory doesn't exist.  (This only ignores
+ * `ENOENT': any other problem is still a fatal error.)
+ */
 void read_config_dir(const char *what, const char *path, unsigned f)
 {
   struct argv av = ARGV_INIT;
@@ -273,11 +421,17 @@ void read_config_dir(const char *what, const char *path, unsigned f)
     read_config_file(what, dd.p, f&~CF_NOENTOK);
   }
 
-  for (i = 0; i < av.n; i++) free((/*unconst*/ char *)av.v[i]);
+  for (i = 0; i < av.n; i++) free(av.v[i]);
   argv_release(&av); dstr_release(&dd); closedir(dir);
   return;
 }
 
+/* Read configuration from a file or directory PATH.
+ *
+ * If PATH exists and names a directory then process all of the files within,
+ * as for `read_config_dir'; otherwise try to read it as a file, as for
+ * `read_config_file'. The flags F are passed to the respective function.
+ */
 void read_config_path(const char *path, unsigned f)
 {
   struct stat st;
@@ -288,6 +442,13 @@ void read_config_path(const char *path, unsigned f)
     read_config_file("command-line specified", path, f);
 }
 
+/* Apply a configuration variable setting in command-line syntax.
+ *
+ * ASSIGN should be a string in the form `[SECT:]VAR=VALUE'.  Set VAR to
+ * VALUE in section SECT (defaults to `@CONFIG').  The variable is set with
+ * `CF_OVERRIDE' set to prevent the setting from being overwritten by a
+ * configuration file.
+ */
 int set_config_var(const char *assign)
 {
   struct config_section *sect;
@@ -298,48 +459,33 @@ int set_config_var(const char *assign)
   q = strchr(assign, ':');
   if (!q || q > p)
     { sect = toplevel; q = assign; }
+  else if (q == assign)
+    lose("expected section or variable name in option assignment");
   else {
     sect = config_find_section_n(&config, CF_CREAT, assign, q - assign);
     q++;
   }
+  if (p == q) lose("expected variable name in option assignment");
   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
-}
-
+/* Load the default configuration files.
+ *
+ * This will read `ETCDIR/runlisp.d/*.conf', `ETCDIR/runlisp.conf',
+ * `~/.runlisp.conf', and `~/.config/runlisp.conf'.
+ */
 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_SYSCONFIG", ETCDIR "/runlisp.conf");
+  read_config_file("system", p, 0);
+
   p = my_getenv("RUNLISP_USERCONFIG", 0);
   if (p)
     read_config_file("user", p, CF_NOENTOK);
@@ -352,23 +498,20 @@ void load_default_config(void)
   dstr_release(&d);
 }
 
+/* Dump the configuration to standard error. */
 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);
+    for (config_start_var_iter(&config, sect, &vi);
+        (var = config_next_var(&vi)); )
+      moan("config %s:%s = %s",
+          CONFIG_SECTION_NAME(sect), CONFIG_VAR_NAME(var), var->val);
 }
 
 /*----- That's all, folks -------------------------------------------------*/