coreutils
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

[PATCH] env: new option '-f/--file' to load variables from file


From: mdeclerk
Subject: [PATCH] env: new option '-f/--file' to load variables from file
Date: Fri, 8 Oct 2021 19:32:14 +0000

Add support to load variables from file using '-f/--file' option.
Each line must match the pattern 'NAME=VALUE'. Empty lines and
comments are ignored; comments start with '#' or whitepsaces
followed by '#'. Values refering to one or more other variables
indicated by a leading '$' (i.e., NEW_PATH=$PATH1:$PATH2:/my/path)
are resolved with respect to the original environment. Unknown
references are replaced with an empty string. This feature is
intended to work in tandem with the '-i' option to setup a fresh
envrionment plus the ability to import variables from the
original environment as needed. Variables with the same name
specified via command line override variables defined in the file.
* src/env.c (main, shortopts, longopts): Add option '-f/--file'.
(resolve_env_vars, is_empty_or_comment, parse_line_from_envfile):
New functions.
(usage): Update usage output.
* doc/coreutils.texi (env invocation -- general options):
Documentation for '-f/--file'.
* tests/misc/env.sh: Add test.
---
 doc/coreutils.texi |  30 ++++++++++
 src/env.c          | 143 ++++++++++++++++++++++++++++++++++++++++++++-
 tests/misc/env.sh  |  36 ++++++++++++
 3 files changed, 208 insertions(+), 1 deletion(-)

diff --git a/doc/coreutils.texi b/doc/coreutils.texi
index 8ccee121a..0d99e8aba 100644
--- a/doc/coreutils.texi
+++ b/doc/coreutils.texi
@@ -17443,6 +17443,36 @@ environment.
 @opindex --ignore-environment
 Start with an empty environment, ignoring the inherited environment.

+@item -f @var{filename}
+@itemx --file=@var{filename}
+@opindex -f
+@opindex --file
+Load variables from @var{filename} before invoking @var{command}.
+Each line must match the pattern @env{NAME=VALUE}. Empty lines and comments
+are ignored; comments start with @samp{#} or whitepsaces followed by @samp{#}.
+Values refering to one or more other variables indicated by a leading @samp{$}
+(i.e., @env{NEW_PATH=$PATH1:$PATH2:/my/path3}) are resolved with respect to the
+original environment. Unknown references are replaced with an empty string.
+For example:
+
+@example
+# Comments and empty lines are ignored.
+# Define a variable like so 'NAME=VALUE':
+VAR1=123
+VAR2=
+# Resolve references with respect to original environment:
+VAR3=$SOME_PATH
+# Another example with multiple variable references:
+VAR4=$SOME_PATH:$SOME_OTHER_PATH/bin
+# Unknown references are replaced with an empty string:
+VAR5=$UNKNOWN_VAR
+@end example
+
+This feature is intended to work in tandem with the @samp{-i} option to setup
+a fresh envrionment plus the ability to import variables from the original
+environment as needed. Variables with the same name specified via command line
+override variables defined in the file.
+
 @item -C @var{dir}
 @itemx --chdir=@var{dir}
 @opindex -C
diff --git a/src/env.c b/src/env.c
index 685c24adb..333db7875 100644
--- a/src/env.c
+++ b/src/env.c
@@ -77,7 +77,7 @@ static bool report_signal_handling;
 /* The isspace characters in the C locale.  */
 #define C_ISSPACE_CHARS " \t\n\v\f\r"

-static char const shortopts[] = "+C:iS:u:v0" C_ISSPACE_CHARS;
+static char const shortopts[] = "+C:if:S:u:v0" C_ISSPACE_CHARS;

 /* For long options that have no equivalent short option, use a
    non-character as a pseudo short option, starting with CHAR_MAX + 1.  */
@@ -92,6 +92,7 @@ enum
 static struct option const longopts[] =
 {
   {"ignore-environment", no_argument, NULL, 'i'},
+  {"file", required_argument, NULL, 'f'},
   {"null", no_argument, NULL, '0'},
   {"unset", required_argument, NULL, 'u'},
   {"chdir", required_argument, NULL, 'C'},
@@ -124,6 +125,7 @@ Set each NAME to VALUE in the environment and run 
COMMAND.\n\

       fputs (_("\
   -i, --ignore-environment  start with an empty environment\n\
+  -f, --file=FILENAME  load environment variables from file\n\
   -0, --null           end each output line with NUL, not newline\n\
   -u, --unset=NAME     remove variable from the environment\n\
 "), stdout);
@@ -751,12 +753,117 @@ initialize_signals (void)
   return;
 }

+
+/* Resolve references to environment variables indicated by leading '$'
+   in input 'string' with respect to given 'environment'.  */
+static char *
+resolve_env_vars (char const *string, char **environment)
+{
+  char *resolved = xstrdup (string);
+
+  size_t i = 0;
+  while (resolved[i] != '\0')
+    {
+      /* Parse name substring of pattern '$VARIABLE'  */
+      size_t nbeg = i, nend = i + 1;
+      if (resolved[nbeg] == '$'
+          && (c_isalpha (resolved[nend]) || resolved[nend] == '_'))
+        {
+          ++nend;
+          while (c_isalnum (resolved[nend]) || resolved[nend] == '_')
+            ++nend;
+        }
+      size_t nlen = nend - nbeg;
+      if (nlen == 1)
+        {
+          ++i;
+          continue;
+        }
+
+      /* Get associated value  */
+      char *value = NULL, **e = environment, *eq;
+      while (! value && *e)
+        {
+          if ((eq = strchr (*e, '='))
+              && (eq - *e == (ptrdiff_t)nlen - 1)
+              && STREQ_LEN (*e, resolved + nbeg + 1, nlen - 1))
+            value = eq + 1;
+          else
+            ++e;
+        }
+
+      /* Replace name substring with value or ""  */
+      size_t rlen = strlen (resolved);
+      size_t vlen = value ? strlen (value) : 0;
+      int grow = vlen - nlen;
+
+      if (grow > 0)
+        resolved = xrealloc (resolved, rlen + grow + 1);
+
+      memmove (resolved + nbeg + vlen, resolved + nend, rlen - nend + 1);
+      memcpy (resolved + nbeg, value, vlen);
+
+      if (grow < 0)
+        resolved = xrealloc (resolved, rlen + grow + 1);
+
+      i += vlen;
+    }
+
+  return resolved;
+}
+
+static bool _GL_ATTRIBUTE_PURE
+is_empty_or_comment (char const *line)
+{
+  while (c_isspace (*line))
+    ++line;
+  return *line == '\0' || *line == '#';
+}
+
+/* Parse line of pattern 'NAME=VALUE' and return result via
+   'nameptr' and 'valueptr' params.  Return value is line length
+   on success or -1 on EOF or error.  */
+static ssize_t
+parse_line_from_envfile (char **nameptr, char **valueptr, FILE *fp)
+{
+  static char *line = NULL;
+  static size_t size = 0;
+
+  ssize_t len;
+  while ((len = getline (&line, &size, fp)) != -1
+         && is_empty_or_comment (line))
+    ;
+
+  char *eq = strchr (line, '=');
+  if (len != -1 && eq)
+    {
+      *eq = '\0';
+      if (line[len - 1] == '\n')
+        line[len - 1] = '\0';
+      *nameptr = line;
+      *valueptr = eq + 1;
+      return len;
+    }
+  else
+    {
+      if (len != -1 && ! eq)
+        errno = EINVAL;
+      free (line);
+      line = NULL;
+      size = 0;
+      return -1;
+    }
+}
+
+
 int
 main (int argc, char **argv)
 {
   int optc;
   bool ignore_environment = false;
   bool opt_nul_terminate_output = false;
+  char **original_environ = environ;
+  char const *envfile = NULL;
   char const *newdir = NULL;

   initialize_main (&argc, &argv);
@@ -777,6 +884,9 @@ main (int argc, char **argv)
         case 'i':
           ignore_environment = true;
           break;
+        case 'f':
+          envfile = optarg;
+          break;
         case 'u':
           append_unset_var (optarg);
           break;
@@ -836,6 +946,37 @@ main (int argc, char **argv)
   else
     unset_envvars ();

+  if (envfile)
+    {
+      devmsg ("load environment file %s\n", quoteaf (envfile));
+
+      FILE *fp = fopen (envfile, "r");
+      if (! fp)
+        die (EXIT_CANCELED, errno, _("cannot open file %s"),
+          quoteaf (envfile));
+
+      char *name = NULL, *value = NULL;
+      while (parse_line_from_envfile (&name, &value, fp) != -1)
+        {
+          /* If value refers to one or more variables (i.e., VAR=$PATH),
+             resolve references with respect to original environment. */
+          char *resolved = resolve_env_vars (value, original_environ);
+
+          devmsg ("setenv:   %s=%s\n", name, resolved);
+          if (setenv (name, resolved, 1))
+            die (EXIT_CANCELED, errno, _("cannot set %s"),
+              quote (name));
+
+          free (resolved);
+        }
+
+      if (errno != 0)
+        die (EXIT_CANCELED, errno, _("error parsing file %s"),
+          quoteaf (envfile));
+
+      fclose (fp);
+    }
+
   char *eq;
   while (optind < argc && (eq = strchr (argv[optind], '=')))
     {
diff --git a/tests/misc/env.sh b/tests/misc/env.sh
index 0dcc93b27..006ef7079 100755
--- a/tests/misc/env.sh
+++ b/tests/misc/env.sh
@@ -42,12 +42,48 @@ env -i -- a=b > out || fail=1
 echo a=b > exp || framework_failure_
 compare exp out || fail=1

+# Verify loading variables from file
+cat <<EOF >test_file || framework_failure_
+# This is a comment.
+
+VAR_1=42
+VAR_2=
+VAR_3=$a
+VAR_4=$a:$a
+VAR_5=$b
+EOF
+env -i -f test_file > out || fail=1
+cat <<EOF >exp || framework_failure_
+VAR_1=42
+VAR_2=
+VAR_3=1
+VAR_4=1:1
+VAR_5=
+EOF
+compare exp out || fail=1
+
+# Verify that command line definitions override file definitions
+env -i -f test_file VAR_1= VAR_2= VAR_3= VAR_4= VAR_5= > out || fail=1
+cat <<EOF >exp || framework_failure_
+VAR_1=
+VAR_2=
+VAR_3=
+VAR_4=
+VAR_5=
+EOF
+compare exp out || fail=1
+
 # These tests verify exact status of internal failure.
 returns_ 125 env --- || fail=1 # unknown option
 returns_ 125 env -u || fail=1 # missing option argument
 returns_ 2 env sh -c 'exit 2' || fail=1 # exit status propagation
 returns_ 126 env . || fail=1 # invalid command
 returns_ 127 env no_such || fail=1 # no such command
+returns_ 125 env -f unknown_file || fail=1 # unknown file
+cat <<EOF >err_file || framework_failure_
+illegal line
+EOF
+returns_ 125 env -f err_file || fail=1 # parse error

 # POSIX is clear that environ may, but need not be, sorted.
 # Environment variable values may contain newlines, which cannot be
--
2.25.1




reply via email to

[Prev in Thread] Current Thread [Next in Thread]