diff --git a/plugins/dotenv/dotenv.plugin.zsh b/plugins/dotenv/dotenv.plugin.zsh index 81659ad5e..e3ad00661 100644 --- a/plugins/dotenv/dotenv.plugin.zsh +++ b/plugins/dotenv/dotenv.plugin.zsh @@ -97,6 +97,7 @@ _parse_dotenv_content() { key="${match[1]}" value="${match[2]}" + local raw_value="$value" # Filter out variables to be ignored for security reasons (best effort) if [[ "$key" == (${~forbidden}) ]]; then @@ -145,14 +146,39 @@ _parse_dotenv_content() { # Unquote the value to handle special characters and multiline values value="${(Q)value}" - # Expand variables from in-file parsed vars (same as double-quoted) - local expanded="$value" - for var_name in "${(@k)parsed_vars}"; do - local var_value="${parsed_vars[$var_name]}" - expanded="${expanded//\$\{${var_name}\}/${var_value}}" - expanded="${expanded//\$${var_name}/${var_value}}" + # Single-quoted values are fully literal and must not participate in expansion. + if [[ "$raw_value" == \'*\' ]]; then + parsed_vars[$key]="$value" + if [[ "$mode" == "export" ]]; then + typeset -x "$key"="$value" + fi + continue + fi + + # Expand previously parsed in-file variables without partial name matches. + local expanded="" prefix remainder="$value" var_name + while [[ "$remainder" == *'$'* ]]; do + prefix="${remainder%%\$*}" + expanded+="$prefix" + remainder="${remainder#$prefix}" + + if [[ "$remainder" =~ '^\$\{([a-zA-Z_][a-zA-Z0-9_]*)\}(.*)$' ]]; then + var_name="${match[1]}" + remainder="${match[2]}" + elif [[ "$remainder" =~ '^\$([a-zA-Z_][a-zA-Z0-9_]*)(.*)$' ]]; then + var_name="${match[1]}" + remainder="${match[2]}" + else + expanded+='$' + remainder="${remainder#?}" + continue + fi + + if [[ -v "parsed_vars[$var_name]" ]]; then + expanded+="${parsed_vars[$var_name]}" + fi done - value="$expanded" + value="${expanded}${remainder}" # Store in parsed vars (for in-file expansion) parsed_vars[$key]="$value" diff --git a/plugins/dotenv/tests/basic-parsing.zunit b/plugins/dotenv/tests/basic-parsing.zunit index 5b6f0650e..3ec0378cd 100644 --- a/plugins/dotenv/tests/basic-parsing.zunit +++ b/plugins/dotenv/tests/basic-parsing.zunit @@ -305,3 +305,25 @@ EOF assert "DOTENV_TEST_VARS" var_same_as "expected_vars" } + +@test 'parse in-file variable expansion prefers the longest matching variable name' { + > "$fixture" <<'EOF' +A=1 +ABC=2 +X=$ABC +Y=${ABC} +Z=$ABCD +EOF + + expected_vars=( + A '1' + ABC '2' + X '2' + Y '2' + Z '' + ) + + _parse_dotenv_test "$fixture" + + assert "DOTENV_TEST_VARS" var_same_as "expected_vars" +}