Forcing bash to expand variables in a string loaded from a file
Asked Answered
U

14

119

I am trying to work out how to make bash (force?) expand variables in a string (which was loaded from a file).

I have a file called "something.txt" with the contents:

hello $FOO world

I then run

export FOO=42
echo $(cat something.txt)

this returns:

   hello $FOO world

It didn't expand $FOO even though the variable was set. I can't eval or source the file - as it will try and execute it (it isn't executable as it is - I just want the string with the variables interpolated).

Any ideas?

Unprincipled answered 21/5, 2012 at 10:13 Comment(5)
Be aware of the security implications of eval.Orcein
Did you mean the comment to be for the answer below?Unprincipled
I meant the comment for you. More than one answer proposes the use of eval and it's important to be aware of the implications.Orcein
@DennisWilliamson gotcha - I am a bit late replying but good tip. And of course in the intervening years we have had things like "shell shock" so your comment has stood the test of time! tips hatUnprincipled
Does this answer your question? Bash expand variable in a variableChris
T
151

I stumbled on what I think is THE answer to this question: the envsubst command:

echo "hello \$FOO world" > source.txt
export FOO=42
envsubst < source.txt

This outputs: hello 42 world

If you would like to continue work on the data in a file destination.txt, push this back to a file like this:

envsubst < source.txt > destination.txt

In case it's not already available in your distro, it's in the GNU package gettext.

@Rockallite

  • I wrote a little wrapper script to take care of the '$' problem.

(BTW, there is a "feature" of envsubst, explained at https://unix.stackexchange.com/a/294400/7088 for expanding only some of the variables in the input, but I agree that escaping the exceptions is much more convenient.)

Here's my script:

#! /bin/bash
      ## -*-Shell-Script-*-
CmdName=${0##*/}
Usage="usage: $CmdName runs envsubst, but allows '\$' to  keep variables from
    being expanded.
  With option   -sl   '\$' keeps the back-slash.
  Default is to replace  '\$' with '$'
"

if [[ $1 = -h ]]  ;then echo -e >&2  "$Usage" ; exit 1 ;fi
if [[ $1 = -sl ]] ;then  sl='\'  ; shift ;fi

sed 's/\\\$/\${EnVsUbDolR}/g' |  EnVsUbDolR=$sl\$  envsubst  "$@"
Thant answered 10/8, 2015 at 18:13 Comment(5)
+1. This is the only answer that's guaranteed to not do command substitutions, quote removal, argument processing, etc.Mali
It just works for fully exported shell vars though (in bash). e.g. S='$a $b'; a=set; export b=exported; echo $S |envsubst; yields: " exported"Cinelli
thanks - I think after all these years, I should accept this answer.Unprincipled
However, it doesn't handle dollar sign ($) escaping, e.g. echo '\$USER' | envsubst will output the current username with a prefixing backward slash, not $USER.Grampus
seems to get this on a mac with homebrew needs brew link --force gettextCarnotite
D
30

you can try

echo $(eval echo $(cat something.txt))
Dichroic answered 21/5, 2012 at 11:14 Comment(5)
Use echo -e "$(eval "echo -e \"`<something.txt`\"")" if you need new lines kept.Ferneferneau
Works like a charmStyrax
But only if your template.txt is text. This operation is dangerous because it executes(evalutes) commands if they are.Styrax
but this removed double-quoteCorinnecorinth
Blame it on the subshell.Chastain
E
29

Many of the answers using eval and echo kind of work, but break on various things, such as multiple lines, attempting to escaping shell meta-characters, escapes inside the template not intended to be expanded by bash, etc.

I had the same issue, and wrote this shell function, which as far as I can tell, handles everything correctly. This will still strip only trailing newlines from the template, because of bash's command substitution rules, but I've never found that to be an issue as long as everything else remains intact.

apply_shell_expansion() {
    declare file="$1"
    declare data=$(< "$file")
    declare delimiter="__apply_shell_expansion_delimiter__"
    declare command="cat <<$delimiter"$'\n'"$data"$'\n'"$delimiter"
    eval "$command"
}

For example, you can use it like this with a parameters.cfg which is really a shell script that just sets variables, and a template.txt which is a template that uses those variables:

. parameters.cfg
printf "%s\n" "$(apply_shell_expansion template.txt)" > result.txt

In practice, I use this as a sort of lightweight template system.

Ezana answered 1/12, 2013 at 20:1 Comment(3)
This is one of the best tricks I found so far :). Based on your answer it's so easy to built a function that expand a variable with a string containing references to other variables -- just replace $data with $1 in your function and here we go! I believe this is the best answer to the original question, BTW.Josefajosefina
Even this has usual eval pitfalls. Try adding ; $(eval date) at the end of any line inside input file and it will run date command.Incontrollable
@Incontrollable I'm not sure what you mean; that is actually the desired expansion behavior in this case. In other words, yes, you are supposed to be able to execute arbitrary shell code with this.Ezana
T
8

You don't want to print each line, you want to evaluate it so that Bash can perform variable substitutions.

FOO=42
while read; do
    eval echo "$REPLY"
done < something.txt

See help eval or the Bash manual for more information.

Tisiphone answered 21/5, 2012 at 10:23 Comment(1)
eval is dangerous; you get not only $varname expanded (as you would with envsubst), but $(rm -rf ~) as well.Northwards
U
7

Another approach (which seems icky, but I am putting it here anyway):

Write the contents of something.txt to a temp file, with an echo statement wrapped around it:

something=$(cat something.txt)

echo "echo \"" > temp.out
echo "$something" >> temp.out
echo "\"" >> temp.out

then source it back in to a variable:

RESULT=$(source temp.out)

and the $RESULT will have it all expanded. But it seems so wrong !


Single line solution that doesn't need temporary file :

RESULT=$(source <(echo "echo \"$(cat something.txt)\""))
#or
RESULT=$(source <(echo "echo \"$(<something.txt)\""))
Unprincipled answered 22/5, 2012 at 2:27 Comment(2)
icky but it is most straight-forward, simple, and works.Styrax
This is as dangerous as the other answers containing eval as it implies an implicit eval. Try echo '$(rm -rf /)' > something.txt and then your solution ...Filial
J
4

If you only want the variable references to be expanded (an objective that I had for myself) you could do the below.

contents="$(cat something.txt)"
echo $(eval echo \"$contents\")

(The escaped quotes around $contents is key here)

Jinnyjinrikisha answered 13/8, 2015 at 20:25 Comment(4)
I have tried this one, however the $() removes line endings in my case which breaks the resulting stringEcclesiasticus
I changed the eval line to eval "echo \"$$contents\"" and it preserved the whitespaceEcclesiasticus
@Ecclesiasticus I think you mean eval "echo \"$contents\"" with a single dollar sign? $$contents is the process ID of the script followed by the word contents.Saddletree
This is as dangerous as the other answers containing eval . Try echo '$(rm -rf /)' > something.txt and then your solution ...Filial
A
3
  1. If something.txt has only one line, a bash method, (a shorter version of Michael Neale's "icky" answer), using process & command substitution:

    FOO=42 . <(echo -e echo $(<something.txt))
    

    Output:

    hello 42 world
    

    Note that export isn't needed.

  2. If something.txt has one or more lines, a GNU sed evaluate method:

    FOO=42 sed 's/"/\\\"/g;s/.*/echo "&"/e' something.txt
    
Annitaanniversary answered 7/1, 2018 at 17:53 Comment(2)
I found the sed evaluate to best fit my purpose. I needed to produce scripts from templates, which would have values filled from variables exported in another config file. It handles properly escaping for command expansion e.g. put \$(date) in the template to get $(date) in the output. Thanks!Inferential
This is as dangerous as the other answers containing eval as it implies an implicit eval. Try echo '$(rm -rf /)' > something.txt and then your solution ...Filial
S
1

Following solution:

  • allows replacing of variables which are defined

  • leaves unchanged variables placeholders which are not defined. This is especially useful during automated deployments.

  • supports replacement of variables in following formats:

    ${var_NAME}

    $var_NAME

  • reports which variables are not defined in environment and returns error code for such cases



    TARGET_FILE=someFile.txt;
    ERR_CNT=0;

    for VARNAME in $(grep -P -o -e '\$[\{]?(\w+)*[\}]?' ${TARGET_FILE} | sort -u); do     
      VAR_VALUE=${!VARNAME};
      VARNAME2=$(echo $VARNAME| sed -e 's|^\${||g' -e 's|}$||g' -e 's|^\$||g' );
      VAR_VALUE2=${!VARNAME2};

      if [ "xxx" = "xxx$VAR_VALUE2" ]; then
         echo "$VARNAME is undefined ";
         ERR_CNT=$((ERR_CNT+1));
      else
         echo "replacing $VARNAME with $VAR_VALUE2" ;
         sed -i "s|$VARNAME|$VAR_VALUE2|g" ${TARGET_FILE}; 
      fi      
    done

    if [ ${ERR_CNT} -gt 0 ]; then
        echo "Found $ERR_CNT undefined environment variables";
        exit 1 
    fi
Summersummerhouse answered 16/3, 2017 at 18:9 Comment(0)
M
1
foo=45
file=something.txt       # in a file is written: Hello $foo world!
eval echo $(cat $file)
Materials answered 11/9, 2018 at 15:51 Comment(2)
Thank you for this code snippet, which might provide some limited, immediate help. A proper explanation would greatly improve its long-term value by showing why this is a good solution to the problem and would make it more useful to future readers with other, similar questions. Please edit your answer to add some explanation, including the assumptions you’ve made.Proctor
This is as dangerous as the other answers containing eval . Try echo '$(rm -rf /)' > something.txt and then your solution ...Filial
F
0
$ eval echo $(cat something.txt)
hello 42 world
$ bash --version
GNU bash, version 3.2.57(1)-release (x86_64-apple-darwin17)
Copyright (C) 2007 Free Software Foundation, Inc.
Fondly answered 27/7, 2018 at 20:21 Comment(1)
This is as dangerous as the other answers containing eval. Try echo '$(rm -rf /)' > something.txt and then your solution ...Filial
F
0
catexp () {
    local function_name="${FUNCNAME[0]}"
    local uuid="$(uuidgen)"
    if [[ $# = 0 && -t 0 ]]; then
        echo "$function_name: Missing argument and stdin." >&2
        return 1
    fi
    . <(
        printf -- '%s\n' "cat << ${uuid}"
        cat "$@" 2> >(perl -spe 's/^cat: /$function_name: /' -- -function_name="$function_name" >&2)
        exit_code=$?
        printf -- '\n%s\n' "${uuid}"
        printf -- '%s\n' "(exit ${exit_code})"
    ) | perl -0pe 's/\n$//'
    pipestatus
    return $?
}

pipestatus () {
    set "${PIPESTATUS[@]}"
    local i
    for (( i=1; i<$#; i++ )); do
        if [[ ${!i} -ge 1 ]]; then break; fi
    done
    return ${!i}
}

echo "hello \$FOO world" > source.txt
FOO=42 # Also without export
catexp source.txt

Or:

catexp < source.txt

Or:

cat source.txt | catexp

Also:

catexp file1 file2 file3 ...
Feudal answered 30/4, 2023 at 2:30 Comment(0)
F
-1
expenv () {
    LF=$'\n'
    echo "cat <<END_OF_TEXT${LF}$(< "$1")${LF}END_OF_TEXT" | bash
    return $?
}

expenv "file name"
Feudal answered 16/2, 2021 at 14:16 Comment(1)
Code dumps do not make for good answers. You should explain how and why this solves their problem. I recommend reading, How do I write a good answer?Lamp
P
-2

envsubst is a great solution (see LenW's answer) if the content you're substituting is of "reasonable" length.

In my case, I needed to substitute in a file's content to replace the variable name. envsubst requires that the content be exported as environment variables and bash has a problem when exporting environment variables that are more than a megabyte or so.

awk solution

Using cuonglm's solution from a different question:

needle="doc1_base64" # The "variable name" in the file. (A $ is not needed.)
needle_file="doc1_base64.txt" # Will be substituted for the needle 
haystack=$requestfile1 # File containing the needle
out=$requestfile2
awk "BEGIN{getline l < \"${needle_file}\"}/${needle}/{gsub(\"${needle}\",l)}1" $haystack > $out

This solution works for even large files.

Persist answered 21/8, 2018 at 9:14 Comment(1)
The question was "Forcing bash to expand variables in a string loaded from a file" and this is not an answer to it. Maybe the variable names are not even known upfront, maybe they can be whatever the user wants them to be. This is replacing text with text but that wasn't asked.Tucker
L
-3

The following works: bash -c "echo \"$(cat something.txt)"\"

Lansing answered 17/4, 2018 at 11:35 Comment(1)
This is not just inefficient, but also extremely dangerous; it doesn't just run safe expansions like $foo, but unsafe ones like $(rm -rf ~).Northwards

© 2022 - 2024 — McMap. All rights reserved.