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