From b82f58bfe396b395bce3452bc0ba2f972fb01ab8 Mon Sep 17 00:00:00 2001 From: Ray Strode Date: Tue, 9 Aug 2016 10:20:22 -0400 Subject: [PATCH] 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.] --- man/environment.d.xml | 16 +++++++--- src/basic/env-util.c | 67 ++++++++++++++++++++++++++++++++++++++-- src/basic/env-util.h | 1 + src/basic/fileio.c | 6 ++-- src/test/test-env-util.c | 14 ++++++++- src/test/test-fileio.c | 16 ++++++++-- 6 files changed, 108 insertions(+), 12 deletions(-) diff --git a/man/environment.d.xml b/man/environment.d.xml index 4022c25c36..be7758a2f9 100644 --- a/man/environment.d.xml +++ b/man/environment.d.xml @@ -78,8 +78,16 @@ KEY=VALUE environment variable assignments, separated by newlines. The right hand side of these assignments may reference previously defined environment variables, using the ${OTHER_KEY} - and $OTHER_KEY format. No other elements of shell syntax are supported. - + and $OTHER_KEY format. It is also possible to use + + ${FOO:-DEFAULT_VALUE} + to expand in the same way as ${FOO} unless the + expansion would be empty, in which case it expands to DEFAULT_VALUE, + and use + ${FOO:+ALTERNATE_VALUE} + to expand to ALTERNATE_VALUE as long as + ${FOO} would have expanded to a non-empty value. + No other elements of shell syntax are supported. EachKEY must be a valid variable name. Empty lines and lines beginning with the comment character # are ignored. @@ -95,8 +103,8 @@ FOO_DEBUG=force-software-gl,log-verbose PATH=/opt/foo/bin:$PATH - LD_LIBRARY_PATH=/opt/foo/lib - XDG_DATA_DIRS=/opt/foo/share:${XDG_DATA_DIRS} + LD_LIBRARY_PATH=${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}/opt/foo/lib + XDG_DATA_DIRS=/opt/foo/share:${XDG_DATA_DIRS:-/usr/local/share/:/usr/share/} diff --git a/src/basic/env-util.c b/src/basic/env-util.c index 1b955ff1d5..2ca64c3301 100644 --- a/src/basic/env-util.c +++ b/src/basic/env-util.c @@ -525,12 +525,16 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) { CURLY, VARIABLE, VARIABLE_RAW, + TEST, + DEFAULT_VALUE, + ALTERNATE_VALUE, } state = WORD; - const char *e, *word = format; + const char *e, *word = format, *test_value; char *k; _cleanup_free_ char *r = NULL; - size_t i; + size_t i, len; + int nest = 0; assert(format); @@ -554,7 +558,7 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) { word = e-1; state = VARIABLE; - + nest++; } else if (*e == '$') { k = strnappend(r, word, e-word); if (!k) @@ -594,6 +598,63 @@ char *replace_env_n(const char *format, size_t n, char **env, unsigned flags) { free(r); 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; state = WORD; } diff --git a/src/basic/env-util.h b/src/basic/env-util.h index 43a1371f5e..e88fa6aac0 100644 --- a/src/basic/env-util.h +++ b/src/basic/env-util.h @@ -32,6 +32,7 @@ bool env_assignment_is_valid(const char *e); enum { REPLACE_ENV_USE_ENVIRONMENT = 1u, REPLACE_ENV_ALLOW_BRACELESS = 2u, + REPLACE_ENV_ALLOW_EXTENDED = 4u, }; char *replace_env_n(const char *format, size_t n, char **env, unsigned flags); diff --git a/src/basic/fileio.c b/src/basic/fileio.c index 8185f67e00..b9a9f74892 100644 --- a/src/basic/fileio.c +++ b/src/basic/fileio.c @@ -784,7 +784,9 @@ static int merge_env_file_push( } 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) return -ENOMEM; @@ -799,7 +801,7 @@ int merge_env_file( const char *fname) { /* 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); diff --git a/src/test/test-env-util.c b/src/test/test-env-util.c index 77a5219d82..dfcd9cb724 100644 --- a/src/test/test-env-util.c +++ b/src/test/test-env-util.c @@ -185,6 +185,12 @@ static void test_replace_env_argv(void) { "${FOO", "FOO$$${FOO}", "$$FOO${FOO}", + "${FOO:-${BAR}}", + "${QUUX:-${FOO}}", + "${FOO:+${BAR}}", + "${QUUX:+${BAR}}", + "${FOO:+|${BAR}|}}", + "${FOO:+|${BAR}{|}", 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[9], "FOO$BAR 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) { diff --git a/src/test/test-fileio.c b/src/test/test-fileio.c index b117335db8..b1d688c89e 100644 --- a/src/test/test-fileio.c +++ b/src/test/test-fileio.c @@ -229,6 +229,10 @@ static void test_merge_env_file(void) { "twentytwo=2${one}\n" "xxx_minus_three=$xxx - 3\n" "xxx=0x$one$one$one\n" + "yyy=${one:-fallback}\n" + "zzz=${one:+replacement}\n" + "zzzz=${foobar:-${nothing}}\n" + "zzzzz=${nothing:+${nothing}}\n" , false); 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[4], "xxx=0x222")); 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); 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[4], "xxx=0x222")); 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) {