Reputation: 29975
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)
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?
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?
Upvotes: 198
Views: 213134
Reputation: 7968
read -rp "name (default: Carson): " NAME
NAME=${NAME:-Carson}
read -rp "email (default: [email protected]): " EMAIL
EMAIL=${EMAIL:[email protected]}
export NAME EMAIL
envsubst < template.gitconfig > ~/1.gitconfig # You can write it like this, but if your alias or other content has $xxx, it will also be affected by `envsubst`
# envsubst can also specify the variable to be replaced, so as to affect as little of the other content as possible
envsubst '$NAME,$EMAIL' < template.gitconfig > ~/2.gitconfig
template.gitconfig
[user]
name = ${NAME}
email = ${EMAIL}
# ${Test123}
~/1.gitconfig
[user]
name = Carson
email = [email protected]
#
~/2.gitconfig
[user]
name = Carson
email = [email protected]
# ${Test123}
Upvotes: 0
Reputation: 111
I was looking for a standard-utils-only solution and also I was not comfortable with evaling an arbitrary template – this can go wrong only too easily. Eventually I came up with the following approach.
Template file:
$ cat addusers.template.sh
# Available variables:
# $USER_NAME
# $USER_ID
# $USER_GROUP_ID
adduser --uid $USER_ID --gid $USER_GROUP_ID --gecos '' --disabled-password $USER_NAME
cp -v /tmp/bash.rc /home/$USER_NAME/.bashrc
chown -v $USER_ID:$USER_GROUP_ID /home/$USER_NAME/.bashrc
chmod 444 /home/$USER_NAME/.bashrc
Bash script:
$ cat cook-addusers.sh
# Read template file skipping comments and empty lines
TEMPLATE="$(grep -vE -e '^[[:blank:]]*#' -e '^[[:blank:]]*$' addusers.template.sh)"
# Cook regex template with the list of substituted variables
for VAR in USER_NAME USER_ID USER_GROUP_ID; do
RE="${RE}"'s/\$'$VAR'/$'$VAR'/g\; '
done
# Fill in the template for a list of users
for USER_NAME in admin superuser supervisor; do
USER_ID=$(id -u "$USER_NAME")
USER_GROUP_ID=$(id -g "$USER_NAME")
sed "$(eval echo "$RE")" <<<"${TEMPLATE}"
done >generated.sh
And the result of running it:
$ ./cook-addusers.sh
$ cat generated.sh
adduser --uid 1001 --gid 101 --gecos '' --disabled-password admin
cp -v /tmp/bash.rc /home/admin/.bashrc
chown -v 1001:101 /home/admin/.bashrc
chmod 444 /home/admin/.bashrc
adduser --uid 1010 --gid 101 --gecos '' --disabled-password superuser
cp -v /tmp/bash.rc /home/superuser/.bashrc
chown -v 1010:101 /home/superuser/.bashrc
chmod 444 /home/superuser/.bashrc
adduser --uid 1100 --gid 101 --gecos '' --disabled-password supervisor
cp -v /tmp/bash.rc /home/supervisor/.bashrc
chown -v 1100:101 /home/supervisor/.bashrc
chmod 444 /home/supervisor/.bashrc
It's rather crude though seems to be working as intended both on typical Linux and BSD… sorry, I mean macOS ;-)
Upvotes: 1
Reputation: 5910
You can use a template file with environment variables and run
cat template-file.yaml |envsubst
Upvotes: 1
Reputation: 40688
I agree with using sed: it is the best tool for search/replace. Here is my approach:
$ cat template.txt
the number is ${i}
the dog's name is ${name}
$ cat replace.sed
s/${i}/5/g
s/${name}/Fido/g
$ sed -f replace.sed template.txt > out.txt
$ cat out.txt
the number is 5
the dog's name is Fido
Upvotes: 42
Reputation: 2551
Try envsubst
$ cat envsubst-template.txt
Variable FOO is (${FOO}).
Variable BAR is (${BAR}).
$ FOO=myfoo
$ BAR=mybar
$ export FOO BAR
$ cat envsubst-template.txt | envsubst
Variable FOO is (myfoo).
Variable BAR is (mybar).
Upvotes: 245
Reputation: 3526
A heredoc is a builtin way to template a conf file.
STATUS_URI="/hows-it-goin"; MONITOR_IP="10.10.2.15";
cat >/etc/apache2/conf.d/mod_status.conf <<EOF
<Location ${STATUS_URI}>
SetHandler server-status
Order deny,allow
Deny from all
Allow from ${MONITOR_IP}
</Location>
EOF
Regarding yottsa's answer: envsubst
was new to me. Fantastic.
Upvotes: 85
Reputation: 713
Here's another pure bash solution:
$ cat code
#!/bin/bash
LISTING=$( ls )
cat_template() {
echo "cat << EOT"
cat "$1"
echo EOT
}
cat_template template | LISTING="$LISTING" bash
Input:
$ cat template
(with trailing newlines and double quotes)
<html>
<head>
</head>
<body>
<p>"directory listing"
<pre>
$( echo "$LISTING" | sed 's/^/ /' )
<pre>
</p>
</body>
</html>
Output:
<html>
<head>
</head>
<body>
<p>"directory listing"
<pre>
code
template
<pre>
</p>
</body>
</html>
Upvotes: 7
Reputation: 117
Perfect case for shtpl. (project of mine, so it is not widely in use and lacks in documentation. But here is the solution it offers anyhow. May you want to test it.)
Just execute:
$ i=1 word=dog sh -c "$( shtpl template.txt )"
Result is:
the number is 1
the word is dog
Have fun.
Upvotes: 4
Reputation: 4156
I think eval
works really well. It handles templates with linebreaks, whitespace, and all sorts of bash stuff. If you have full control over the templates themselves of course:
$ cat template.txt
variable1 = ${variable1}
variable2 = $variable2
my-ip = \"$(curl -s ifconfig.me)\"
$ echo $variable1
AAA
$ echo $variable2
BBB
$ eval "echo \"$(<template.txt)\"" 2> /dev/null
variable1 = AAA
variable2 = BBB
my-ip = "11.22.33.44"
This method should be used with care, of course, since eval can execute arbitrary code. Running this as root is pretty much out of the question. Quotes in the template need to be escaped, otherwise they will be eaten by eval
.
You can also use here documents if you prefer cat
to echo
$ eval "cat <<< \"$(<template.txt)\"" 2> /dev/null
@plockc provoded a solution that avoids the bash quote escaping issue:
$ eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
Edit: Removed part about running this as root using sudo...
Edit: Added comment about how quotes need to be escaped, added plockc's solution to the mix!
Upvotes: 28
Reputation: 17
You can also use printf to fill a template.
#!/bin/bash
IFS='' read -rd '' TEMPL <<-'EOB'
The number is %d
The word is "%s"
Birds of Massachusetts:
%s
EOB
N=12
WORD="Bird"
MULTILINE="Eastern Bluebirds
Common Grackles"
echo "START"
printf "${TEMPL}" ${N} ${WORD} "${MULTILINE}"
echo "END"
Here's the output, with quotes and whitespace intact:
START
The number is 12
The word is "Bird"
Birds of Massachusetts:
Eastern Bluebirds
Common Grackles
END
Upvotes: 0
Reputation: 200
To follow up on plockc's answer on this page, here is a dash-suitable version, for those of you looking to avoid bashisms.
eval "cat <<EOF >outputfile
$( cat template.in )
EOF
" 2> /dev/null
Upvotes: 4
Reputation: 53604
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:
read
will interpret backslashes.read -r
will not interpret backslashes, but still will drop the last line if it does not end with a newline."$(…)"
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).Upvotes: 69
Reputation: 3940
Look at simple variables substitution python script here: https://github.com/jeckep/vsubst
It is very simple to use:
python subst.py --props secure.properties --src_path ./templates --dst_path ./dist
Upvotes: 1
Reputation: 2884
Instead of reinventing the wheel go with envsubst Can be used in almost any scenario, for instance building configuration files from environment variables in docker containers.
If on mac make sure you have homebrew then link it from gettext:
brew install gettext
brew link --force gettext
./template.cfg
# We put env variables into placeholders here
this_variable_1 = ${SOME_VARIABLE_1}
this_variable_2 = ${SOME_VARIABLE_2}
./.env:
SOME_VARIABLE_1=value_1
SOME_VARIABLE_2=value_2
./configure.sh
#!/bin/bash
cat template.cfg | envsubst > whatever.cfg
Now just use it:
# make script executable
chmod +x ./configure.sh
# source your variables
. .env
# export your variables
# In practice you may not have to manually export variables
# if your solution depends on tools that utilise .env file
# automatically like pipenv etc.
export SOME_VARIABLE_1 SOME_VARIABLE_2
# Create your config file
./configure.sh
Upvotes: 17
Reputation: 40688
Here is another solution: generate a bash script with all the variables and the contents of the template file, that script would look like this:
word=dog
i=1
cat << EOF
the number is ${i}
the word is ${word}
EOF
If we feed this script into bash it would produce the desired output:
the number is 1
the word is dog
Here is how to generate that script and feed that script into bash:
(
# Variables
echo word=dog
echo i=1
# add the template
echo "cat << EOF"
cat template.txt
echo EOF
) | bash
cat
command with HEREDOCIf you want to redirect this output into a file, replace the last line with:
) | bash > output.txt
Upvotes: 7
Reputation: 1207
# Usage: template your_file.conf.template > your_file.conf
template() {
local IFS line
while IFS=$'\n\r' read -r line ; do
line=${line//\\/\\\\} # escape backslashes
line=${line//\"/\\\"} # escape "
line=${line//\`/\\\`} # escape `
line=${line//\$/\\\$} # escape $
line=${line//\\\${/\${} # de-escape ${ - allows variable substitution: ${var} ${var:-default_value} etc
# to allow arithmetic expansion or command substitution uncomment one of following lines:
# line=${line//\\\$\(/\$\(} # de-escape $( and $(( - allows $(( 1 + 2 )) or $( command ) - UNSECURE
# line=${line//\\\$\(\(/\$\(\(} # de-escape $(( - allows $(( 1 + 2 ))
eval "echo \"${line}\"";
done < "$1"
}
This is the pure bash function adjustable to your liking, used in production and should not break on any input. If it breaks - let me know.
Upvotes: 3
Reputation: 909
I needed to keep double quotes in my configuration file so double escaping double quotes with sed helps:
render_template() {
eval "echo \"$(sed 's/\"/\\\\"/g' $1)\""
}
I can't think of keeping trailing new lines, but empty lines in between are kept.
Although it is an old topic, IMO I found out more elegant solution here: http://pempek.net/articles/2013/07/08/bash-sh-as-template-engine/
#!/bin/sh
# render a template configuration file
# expand variables + preserve formatting
render_template() {
eval "echo \"$(cat $1)\""
}
user="Gregory"
render_template /path/to/template.txt > path/to/configuration_file
All credits to Grégory Pakosz.
Upvotes: 18
Reputation: 244
Here's a modified perl
script based on a few of the other answers:
perl -pe 's/([^\\]|^)\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}/$1.$ENV{$2}/eg' -i template
Features (based on my needs, but should be easy to modify):
Upvotes: 0
Reputation: 437238
If using Perl is an option and you're content with basing expansions on environment variables only (as opposed to all shell variables), consider Stuart P. Bentley's robust answer.
This answer aims to provide a bash-only solution that - despite use of eval
- should be safe to use.
The goals are:
${name}
and $name
variable references.$(...)
and legacy syntax `...`
) $((...))
and legacy syntax $[...]
).\
(\${name}
)."
and \
instances.Function expandVars()
:
expandVars() {
local txtToEval=$* txtToEvalEscaped
# If no arguments were passed, process stdin input.
(( $# == 0 )) && IFS= read -r -d '' txtToEval
# Disable command substitutions and arithmetic expansions to prevent execution
# of arbitrary commands.
# Note that selectively allowing $((...)) or $[...] to enable arithmetic
# expressions is NOT safe, because command substitutions could be embedded in them.
# If you fully trust or control the input, you can remove the `tr` calls below
IFS= read -r -d '' txtToEvalEscaped < <(printf %s "$txtToEval" | tr '`([' '\1\2\3')
# Pass the string to `eval`, escaping embedded double quotes first.
# `printf %s` ensures that the string is printed without interpretation
# (after processing by by bash).
# The `tr` command reconverts the previously escaped chars. back to their
# literal original.
eval printf %s "\"${txtToEvalEscaped//\"/\\\"}\"" | tr '\1\2\3' '`(['
}
Examples:
$ expandVars '\$HOME="$HOME"; `date` and $(ls)'
$HOME="/home/jdoe"; `date` and $(ls) # only $HOME was expanded
$ printf '\$SHELL=${SHELL}, but "$(( 1 \ 2 ))" will not expand' | expandVars
$SHELL=/bin/bash, but "$(( 1 \ 2 ))" will not expand # only ${SHELL} was expanded
${HOME:0:10}
, as long as they contain no embedded command or arithmetic substitutions, such as ${HOME:0:$(echo 10)}
$(
and `
instances are blindly escaped).${HOME
(missing closing }
) BREAK the function.\$name
prevents expansion.\
not followed by $
is preserved as is.\
instances, you must double them; e.g.:
\\
-> \
- the same as just \
\\\\
-> \\
0x1
, 0x2
, 0x3
.eval
.If you're looking for a more restrictive solution that only supports ${name}
expansions - i.e., with mandatory curly braces, ignoring $name
references - see this answer of mine.
Here is an improved version of the bash-only, eval
-free solution from the accepted answer:
The improvements are:
${name}
and $name
variable references.\
-escaping variable references that shouldn't be expanded.eval
-based solution above,
IFS= read -d '' -r lines # read all input from stdin at once
end_offset=${#lines}
while [[ "${lines:0:end_offset}" =~ (.*)\$(\{([a-zA-Z_][a-zA-Z_0-9]*)\}|([a-zA-Z_][a-zA-Z_0-9]*))(.*) ]] ; do
pre=${BASH_REMATCH[1]} # everything before the var. reference
post=${BASH_REMATCH[5]}${lines:end_offset} # everything after
# extract the var. name; it's in the 3rd capture group, if the name is enclosed in {...}, and the 4th otherwise
[[ -n ${BASH_REMATCH[3]} ]] && varName=${BASH_REMATCH[3]} || varName=${BASH_REMATCH[4]}
# Is the var ref. escaped, i.e., prefixed with an odd number of backslashes?
if [[ $pre =~ \\+$ ]] && (( ${#BASH_REMATCH} % 2 )); then
: # no change to $lines, leave escaped var. ref. untouched
else # replace the variable reference with the variable's value using indirect expansion
lines=${pre}${!varName}${post}
fi
end_offset=${#pre}
done
printf %s "$lines"
Upvotes: 5
Reputation: 5487
Here's a bash function that preserves whitespace:
# Render a file in bash, i.e. expand environment variables. Preserves whitespace.
function render_file () {
while IFS='' read line; do
eval echo \""${line}"\"
done < "${1}"
}
Upvotes: 0
Reputation: 21
You can also use bashible (which internally uses the evaluating approach described above/below).
There is an example, how to generate a HTML from multiple parts:
https://github.com/mig1984/bashible/tree/master/examples/templates
Upvotes: 1
Reputation: 4511
I have a bash solution like mogsie but with heredoc instead of herestring to allow you to avoid escaping double quotes
eval "cat <<EOF
$(<template.txt)
EOF
" 2> /dev/null
Upvotes: 39
Reputation: 10675
A longer but more robust version of the accepted answer:
perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\})?;substr($1,0,int(length($1)/2)).($2&&length($1)%2?$2:$ENV{$3||$4});eg' template.txt
This expands all instances of $VAR
or ${VAR}
to their environment values (or, if they're undefined, the empty string).
It properly escapes backslashes, and accepts a backslash-escaped $ to inhibit substitution (unlike envsubst, which, it turns out, doesn't do this).
So, if your environment is:
FOO=bar
BAZ=kenny
TARGET=backslashes
NOPE=engi
and your template is:
Two ${TARGET} walk into a \\$FOO. \\\\
\\\$FOO says, "Delete C:\\Windows\\System32, it's a virus."
$BAZ replies, "\${NOPE}s."
the result would be:
Two backslashes walk into a \bar. \\
\$FOO says, "Delete C:\Windows\System32, it's a virus."
kenny replies, "${NOPE}s."
If you only want to escape backslashes before $ (you could write "C:\Windows\System32" in a template unchanged), use this slightly-modified version:
perl -pe 's;(\\*)(\$([a-zA-Z_][a-zA-Z_0-9]*)|\$\{([a-zA-Z_][a-zA-Z_0-9]*)\});substr($1,0,int(length($1)/2)).(length($1)%2?$2:$ENV{$3||$4});eg' template.txt
Upvotes: 9
Reputation: 17117
Taking the answer from ZyX using pure bash but with new style regex matching and indirect parameter substitution it becomes:
#!/bin/bash
regex='\$\{([a-zA-Z_][a-zA-Z_0-9]*)\}'
while read line; do
while [[ "$line" =~ $regex ]]; do
param="${BASH_REMATCH[1]}"
line=${line//${BASH_REMATCH[0]}/${!param}}
done
echo $line
done
Upvotes: 5
Reputation: 35344
If you want to use Jinja2 templates, see this project: j2cli.
It supports:
Upvotes: 13
Reputation: 57
This page describes an answer with awk
awk '{while(match($0,"[$]{[^}]*}")) {var=substr($0,RSTART+2,RLENGTH -3);gsub("[$]{"var"}",ENVIRON[var])}}1' < input.txt > output.txt
Upvotes: 3
Reputation: 681
I'd have done it this way, probably less efficient, but easier to read/maintain.
TEMPLATE='/path/to/template.file'
OUTPUT='/path/to/output.file'
while read LINE; do
echo $LINE |
sed 's/VARONE/NEWVALA/g' |
sed 's/VARTWO/NEWVALB/g' |
sed 's/VARTHR/NEWVALC/g' >> $OUTPUT
done < $TEMPLATE
Upvotes: 13