bashtemplatestemplating

Bash Templating: How to build configuration files from templates with Bash?


I'm writing a script to automate creating configuration files for Apache and PHP for my own webserver. I don't want to use any GUIs like CPanel or ISPConfig.

I have some templates of Apache and PHP configuration files. Bash script needs to read templates, make variable substitution and output parsed templates into some folder. What is the best way to do that? I can think of several ways. Which one is the best or may be there are some better ways to do that? I want to do that in pure Bash (it's easy in PHP for example)

  1. How to replace ${} placeholders in a text file?

template.txt:

The number is ${i}
The word is ${word}

script.sh:

#!/bin/sh

#set variables
i=1
word="dog"
#read in template one line at the time, and replace variables
#(more natural (and efficient) way, thanks to Jonathan Leffler)
while read line
do
    eval echo "$line"
done < "./template.txt"

BTW, how do I redirect output to external file here? Do I need to escape something if variables contain, say, quotes?

  1. Using cat & sed for replacing each variable with its value:

Given template.txt (see above)

Command:

cat template.txt | sed -e "s/\${i}/1/" | sed -e "s/\${word}/dog/"

Seems bad to me because of the need to escape many different symbols and with many variables the line will be tooooo long.

Can you think of some other elegant and safe solution?


Solution

  • You can use this:

    perl -p -i -e 's/\$\{([^}]+)\}/defined $ENV{$1} ? $ENV{$1} : $&/eg' < template.txt
    

    to replace all ${...} strings with corresponding enviroment variables (do not forget to export them before running this script).

    For pure bash this should work (assuming that variables do not contain ${...} strings):

    #!/bin/bash
    while read -r line ; do
        while [[ "$line" =~ (\$\{[a-zA-Z_][a-zA-Z_0-9]*\}) ]] ; do
            LHS=${BASH_REMATCH[1]}
            RHS="$(eval echo "\"$LHS\"")"
            line=${line//$LHS/$RHS}
        done
        echo "$line"
    done
    

    . Solution that does not hang if RHS references some variable that references itself:

    #!/bin/bash
    line="$(cat; echo -n a)"
    end_offset=${#line}
    while [[ "${line:0:$end_offset}" =~ (.*)(\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})(.*) ]] ; do
        PRE="${BASH_REMATCH[1]}"
        POST="${BASH_REMATCH[4]}${line:$end_offset:${#line}}"
        VARNAME="${BASH_REMATCH[3]}"
        eval 'VARVAL="$'$VARNAME'"'
        line="$PRE$VARVAL$POST"
        end_offset=${#PRE}
    done
    echo -n "${line:0:-1}"
    

    WARNING: I do not know a way to correctly handle input with NULs in bash or preserve the amount of trailing newlines. Last variant is presented as it is because shells “love” binary input:

    1. read will interpret backslashes.
    2. read -r will not interpret backslashes, but still will drop the last line if it does not end with a newline.
    3. "$(…)" will strip as many trailing newlines as there are present, so I end with ; echo -n a and use echo -n "${line:0:-1}": this drops the last character (which is a) and preserves as many trailing newlines as there was in the input (including no).