basic: support default and alternate values for env expansion

Sometimes it's useful to provide a default value during an environment
expansion, if the environment variable isn't already set.

For instance $XDG_DATA_DIRS is suppose to default to:

/usr/local/share/:/usr/share/

if it's not yet set. That means callers wishing to augment
XDG_DATA_DIRS need to manually add those two values.

This commit changes replace_env to support the following shell
compatible default value syntax:

XDG_DATA_DIRS=/foo:${XDG_DATA_DIRS:-/usr/local/share/:/usr/share}

Likewise, it's useful to provide an alternate value during an
environment expansion, if the environment variable isn't already set.

For instance, $LD_LIBRARY_PATH will inadvertently search the current
working directory if it starts or ends with a colon, so the following
is usually wrong:

LD_LIBRARY_PATH=/foo/lib:${LD_LIBRARY_PATH}

To address that, this changes replace_env to support the following
shell compatible alternate value syntax:

LD_LIBRARY_PATH=/foo/lib${LD_LIBRARY_PATH:+:${LD_LIBRARY_PATH}}

[zj: gate the new syntax under REPLACE_ENV_ALLOW_EXTENDED switch, so
existing callers are not modified.]
This commit is contained in:
Ray Strode 2016-08-09 10:20:22 -04:00 committed by Zbigniew Jędrzejewski-Szmek
parent 4bed076c5f
commit b82f58bfe3
6 changed files with 108 additions and 12 deletions

View file

@ -78,8 +78,16 @@
<literal><replaceable>KEY</replaceable>=<replaceable>VALUE</replaceable></literal> environment <literal><replaceable>KEY</replaceable>=<replaceable>VALUE</replaceable></literal> environment
variable assignments, separated by newlines. The right hand side of these assignments may variable assignments, separated by newlines. The right hand side of these assignments may
reference previously defined environment variables, using the <literal>${OTHER_KEY}</literal> reference previously defined environment variables, using the <literal>${OTHER_KEY}</literal>
and <literal>$OTHER_KEY</literal> format. No other elements of shell syntax are supported. and <literal>$OTHER_KEY</literal> format. It is also possible to use
</para>
<literal>${<replaceable>FOO</replaceable>:-<replaceable>DEFAULT_VALUE</replaceable>}</literal>
to expand in the same way as <literal>${<replaceable>FOO</replaceable>}</literal> unless the
expansion would be empty, in which case it expands to <replaceable>DEFAULT_VALUE</replaceable>,
and use
<literal>${<replaceable>FOO</replaceable>:+<replaceable>ALTERNATE_VALUE</replaceable>}</literal>
to expand to <replaceable>ALTERNATE_VALUE</replaceable> as long as
<literal>${<replaceable>FOO</replaceable>}</literal> would have expanded to a non-empty value.
No other elements of shell syntax are supported.</para>
<para>Each<replaceable>KEY</replaceable> must be a valid variable name. Empty lines <para>Each<replaceable>KEY</replaceable> must be a valid variable name. Empty lines
and lines beginning with the comment character <literal>#</literal> are ignored.</para> and lines beginning with the comment character <literal>#</literal> are ignored.</para>
@ -95,8 +103,8 @@
<programlisting> <programlisting>
FOO_DEBUG=force-software-gl,log-verbose FOO_DEBUG=force-software-gl,log-verbose
PATH=/opt/foo/bin:$PATH PATH=/opt/foo/bin:$PATH
LD_LIBRARY_PATH=/opt/foo/lib LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}/opt/foo/lib
XDG_DATA_DIRS=/opt/foo/share:${XDG_DATA_DIRS} XDG_DATA_DIRS=/opt/foo/share:${XDG_DATA_DIRS:-/usr/local/share/:/usr/share/}
</programlisting> </programlisting>
</example> </example>
</refsect2> </refsect2>

View file

@ -525,12 +525,16 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
CURLY, CURLY,
VARIABLE, VARIABLE,
VARIABLE_RAW, VARIABLE_RAW,
TEST,
DEFAULT_VALUE,
ALTERNATE_VALUE,
} state = WORD; } state = WORD;
const char *e, *word = format; const char *e, *word = format, *test_value;
char *k; char *k;
_cleanup_free_ char *r = NULL; _cleanup_free_ char *r = NULL;
size_t i; size_t i, len;
int nest = 0;
assert(format); assert(format);
@ -554,7 +558,7 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
word = e-1; word = e-1;
state = VARIABLE; state = VARIABLE;
nest++;
} else if (*e == '$') { } else if (*e == '$') {
k = strnappend(r, word, e-word); k = strnappend(r, word, e-word);
if (!k) if (!k)
@ -594,6 +598,63 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) {
free(r); free(r);
r = k; r = k;
word = e+1;
state = WORD;
} else if (*e == ':') {
if (!(flags & REPLACE_ENV_ALLOW_EXTENDED))
/* Treat this as unsupported syntax, i.e. do no replacement */
state = WORD;
else {
len = e-word-2;
state = TEST;
}
}
break;
case TEST:
if (*e == '-')
state = DEFAULT_VALUE;
else if (*e == '+')
state = ALTERNATE_VALUE;
else {
state = WORD;
break;
}
test_value = e+1;
break;
case DEFAULT_VALUE: /* fall through */
case ALTERNATE_VALUE:
assert(flags & REPLACE_ENV_ALLOW_EXTENDED);
if (*e == '{') {
nest++;
break;
}
if (*e != '}')
break;
nest--;
if (nest == 0) { // || !strchr(e+1, '}')) {
const char *t;
_cleanup_free_ char *v = NULL;
t = strv_env_get_n(env, word+2, len, flags);
if (t && state == ALTERNATE_VALUE)
t = v = replace_env_n(test_value, e-test_value, env, flags);
else if (!t && state == DEFAULT_VALUE)
t = v = replace_env_n(test_value, e-test_value, env, flags);
k = strappend(r, t);
if (!k)
return NULL;
free(r);
r = k;
word = e+1; word = e+1;
state = WORD; state = WORD;
} }

View file

@ -32,6 +32,7 @@ bool env_assignment_is_valid(const char *e);
enum { enum {
REPLACE_ENV_USE_ENVIRONMENT = 1u, REPLACE_ENV_USE_ENVIRONMENT = 1u,
REPLACE_ENV_ALLOW_BRACELESS = 2u, REPLACE_ENV_ALLOW_BRACELESS = 2u,
REPLACE_ENV_ALLOW_EXTENDED = 4u,
}; };
char *replace_env_n(const char *format, size_t n, char **env, unsigned flags); char *replace_env_n(const char *format, size_t n, char **env, unsigned flags);

View file

@ -784,7 +784,9 @@ static int merge_env_file_push(
} }
expanded_value = replace_env(value, *env, expanded_value = replace_env(value, *env,
REPLACE_ENV_USE_ENVIRONMENT|REPLACE_ENV_ALLOW_BRACELESS); REPLACE_ENV_USE_ENVIRONMENT|
REPLACE_ENV_ALLOW_BRACELESS|
REPLACE_ENV_ALLOW_EXTENDED);
if (!expanded_value) if (!expanded_value)
return -ENOMEM; return -ENOMEM;
@ -799,7 +801,7 @@ int merge_env_file(
const char *fname) { const char *fname) {
/* NOTE: this function supports braceful and braceless variable expansions, /* NOTE: this function supports braceful and braceless variable expansions,
* unlike other exported parsing functions. * plus "extended" substitutions, unlike other exported parsing functions.
*/ */
return parse_env_file_internal(f, fname, NEWLINE, merge_env_file_push, env, NULL); return parse_env_file_internal(f, fname, NEWLINE, merge_env_file_push, env, NULL);

View file

@ -185,6 +185,12 @@ static void test_replace_env_argv(void) {
"${FOO", "${FOO",
"FOO$$${FOO}", "FOO$$${FOO}",
"$$FOO${FOO}", "$$FOO${FOO}",
"${FOO:-${BAR}}",
"${QUUX:-${FOO}}",
"${FOO:+${BAR}}",
"${QUUX:+${BAR}}",
"${FOO:+|${BAR}|}}",
"${FOO:+|${BAR}{|}",
NULL NULL
}; };
_cleanup_strv_free_ char **r = NULL; _cleanup_strv_free_ char **r = NULL;
@ -202,7 +208,13 @@ static void test_replace_env_argv(void) {
assert_se(streq(r[8], "${FOO")); assert_se(streq(r[8], "${FOO"));
assert_se(streq(r[9], "FOO$BAR BAR")); assert_se(streq(r[9], "FOO$BAR BAR"));
assert_se(streq(r[10], "$FOOBAR BAR")); assert_se(streq(r[10], "$FOOBAR BAR"));
assert_se(strv_length(r) == 11); assert_se(streq(r[11], "${FOO:-waldo}"));
assert_se(streq(r[12], "${QUUX:-BAR BAR}"));
assert_se(streq(r[13], "${FOO:+waldo}"));
assert_se(streq(r[14], "${QUUX:+waldo}"));
assert_se(streq(r[15], "${FOO:+|waldo|}}"));
assert_se(streq(r[16], "${FOO:+|waldo{|}"));
assert_se(strv_length(r) == 17);
} }
static void test_env_clean(void) { static void test_env_clean(void) {

View file

@ -229,6 +229,10 @@ static void test_merge_env_file(void) {
"twentytwo=2${one}\n" "twentytwo=2${one}\n"
"xxx_minus_three=$xxx - 3\n" "xxx_minus_three=$xxx - 3\n"
"xxx=0x$one$one$one\n" "xxx=0x$one$one$one\n"
"yyy=${one:-fallback}\n"
"zzz=${one:+replacement}\n"
"zzzz=${foobar:-${nothing}}\n"
"zzzzz=${nothing:+${nothing}}\n"
, false); , false);
assert(r >= 0); assert(r >= 0);
@ -245,7 +249,11 @@ static void test_merge_env_file(void) {
assert_se(streq(a[3], "twentytwo=22")); assert_se(streq(a[3], "twentytwo=22"));
assert_se(streq(a[4], "xxx=0x222")); assert_se(streq(a[4], "xxx=0x222"));
assert_se(streq(a[5], "xxx_minus_three= - 3")); assert_se(streq(a[5], "xxx_minus_three= - 3"));
assert_se(a[6] == NULL); assert_se(streq(a[6], "yyy=2"));
assert_se(streq(a[7], "zzz=replacement"));
assert_se(streq(a[8], "zzzz="));
assert_se(streq(a[9], "zzzzz="));
assert_se(a[10] == NULL);
r = merge_env_file(&a, NULL, t); r = merge_env_file(&a, NULL, t);
assert_se(r >= 0); assert_se(r >= 0);
@ -260,7 +268,11 @@ static void test_merge_env_file(void) {
assert_se(streq(a[3], "twentytwo=22")); assert_se(streq(a[3], "twentytwo=22"));
assert_se(streq(a[4], "xxx=0x222")); assert_se(streq(a[4], "xxx=0x222"));
assert_se(streq(a[5], "xxx_minus_three=0x222 - 3")); assert_se(streq(a[5], "xxx_minus_three=0x222 - 3"));
assert_se(a[6] == NULL); assert_se(streq(a[6], "yyy=2"));
assert_se(streq(a[7], "zzz=replacement"));
assert_se(streq(a[8], "zzzz="));
assert_se(streq(a[9], "zzzzz="));
assert_se(a[10] == NULL);
} }
static void test_merge_env_file_invalid(void) { static void test_merge_env_file_invalid(void) {