[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
- [PATCH] env: new option '-f/--file' to load variables from file,
mdeclerk <=