6

I have a skeleton text file with placeholder strings:

blah blah blah
blah $PLACEHOLDER_1$
blah
$PLACEHOLDER_2$

and so on. Specific "form" of placeholders does not matter -- I may change them to whatever most comfortable for specific implementation.

I have a bash script where I know values for placeholders, and I need to generate a new file, with placeholders replaced with values.

#! /bin/sh
PLACEHOLDER_1 = 'string 1'
PLACEHOLDER_2 = 'multiline 
string 
2'
# TODO: Generate file output.txt from file output.template 
#       using placeholders above.

I may do this in multiple passes with sed, but it is not fun. I do not want to use Perl. I want to use textutils and bash itself only.

What is the best way to do what I want in a single pass?

Alexander Gladysh
  • 39,865
  • 32
  • 103
  • 160

5 Answers5

11

Here's a way to do it without sed:

First, a slightly modified template file in which the placeholders are bash variables:

blah blah blah
blah $PLACEHOLDER_1
blah
$PLACEHOLDER_2

And the script:

#! /bin/sh
templatefile=output.template
outputfile=output.txt

PLACEHOLDER_1='string 1'

PLACEHOLDER_2='multiline 
string 
2'

# DONE: Generate file output.txt from file output.template 
#       using placeholders above.

echo "$(eval "echo \"$(cat $templatefile)\"")" > $outputfile

Here's a version that demonstrates a template contained within the script, but with a twist. It also demonstrates default values, which can also be used in the template file version, plus you can do math in the template:

#! /bin/sh
template='blah blah blah
blah $PLACEHOLDER_1
blah
${PLACEHOLDER_2:-"some text"} blah ${PLACEHOLDER_3:-"some
lines
of
text"} and the total is: $((${VAL_1:-0} + ${VAL_2:-0}))'
# default operands to zero (or 1) to prevent errors due to unset variables
outputfile=output.txt

# gears spin, bells ding, values for placeholders are computed

PLACEHOLDER_1='string 1'

PLACEHOLDER_2='multiline 
string 
2'

VAL_1=2

VAL_2=4

unset PLACEHOLDER_3 # so we can trigger one of the defaults

# Generate file output.txt from variable $template 
#       using placeholders above.

echo "$(eval "echo \"$template\"")" > $outputfile

No sed, no loops, just hairy nesting and quotes. I'm pretty sure all the quoting will protect you from malicious stuff in a template file, but I'm not going to guarantee it.

Dennis Williamson
  • 346,391
  • 90
  • 374
  • 439
  • You can use -e on the either echo to interpret backslash escapes such as \n in your template and/or placeholders. – Dennis Williamson May 29 '09 at 20:57
  • Nope. Not safe. Templates can include "$(command)" which could do bad stuff. You must know whether you can trust your template. You have been warned. – Dennis Williamson May 29 '09 at 23:35
  • Who cares if it's safe, unless you are gonna give it to users? I need it to autogenerate some html based off of content I am in complete control of. :) +1! – Kalle Aug 16 '12 at 20:44
  • Btw if you need to preserve quotes in the result, you can escape them with sed first: echo "$(eval "echo \"$(sed 's/\"/\\\"/g' $template)\"")" > $outputfile – Kalle Aug 16 '12 at 20:45
  • The `echo "$(eval "echo \"$template\"")" > $outputfile` doesn't work for me in a script, but it works on the command line. Why? – Will Hains Aug 16 '13 at 00:24
  • Ah, it turns out I was trying to do too much in my template file. Simple substitution of variables works just fine. – Will Hains Aug 16 '13 at 00:39
9

You can still use sed to do the replace in a single pass. You just need to specify all the replacements in one command.

eg.

sed -i 's/PLACEHOLDER_1/string 1/g;s/PLACEHOLDER_2/string 2/g' <file>
Adam Peck
  • 6,930
  • 3
  • 23
  • 27
  • If you really know what you’re doing and don’t want a backup made, you need to provide an empty string on OSX for the -i option: `sed -i '' 's/PLACEHOLDER_1/string 1/g;s/PLACEHOLDER_2/string 2/g' ` ... reference: http://blog.mpdaugherty.com/2010/05/27/difference-with-sed-in-place-editing-on-mac-os-x-vs-linux/ – pulkitsinghal Feb 24 '13 at 17:25
  • Doesn't work, it seems to be doing a second pass. echo "AB" | sed 's/A/B/g;s/B/A/g' returns AA, not BA. Also happens with newline between A and B in input. – Ambroz Bizjak Jun 18 '14 at 22:12
  • What are you expecting to happen? You are saying to replace A->B but then replacing it right back to B->A. Did you want them to be executed exclusively or as a single expression? – Adam Peck Jun 20 '14 at 20:24
5

Building on the previous answer, perhaps use an array and compute the sed string?

#!/bin/sh
PLACEHOLDER[0]='string 1'
PLACEHOLDER[1]='multiline 
string 
2'

s="sed -i "
for(( i=0 ; i<${#PLACEHOLDER[*]} ; i++ )) ; do 
    echo ${PLACEHOLDER[$i]}
    s=$s"s/PLACEHOLDER_$i/${PLACEHOLDER[$i]}/g;"
done
echo $s

Seems to fail on the multi-line strings, though.

I don't know how portable Bash arrays might be. Above snippet tested with "GNU bash, version 3.2.17(1)-release (i386-apple-darwin9.0)"

David Poole
  • 3,432
  • 5
  • 34
  • 34
  • It's because you need quotes around parameters that have spaces in them. Change "sed -i" to "sed -i'" and after the loop added the other quote and then it should work. – Adam Peck Jan 02 '09 at 17:35
  • You say "tested with bash" but your shebang says "sh". Is sh linked to bash on your system? If so, does its behavior not depend on its name? – Dennis Williamson May 29 '09 at 05:46
  • sh is bash on my system but (oddly enough), it's not linked. I have /bin/sh and bin/bash, two different files. A quirk of the Mac, I suppose. I don't know if GNU bash changes its behavior if it's invoked as 'sh' vs being invoked as 'bash'. – David Poole May 29 '09 at 13:44
2

My bash only solution:

TEMPLATE='
foo
$var1
bar
$var2'
eval "echo \"$TEMPLATE\""
Andor
  • 21
  • 1
1

I just stumbled upon this question because I was just looking for the exact same, and I found envsubst(1).

You can use envsubst if you don't mind using environment variables:

PLACEHOLDER_1='string 1' PLACEHOLDER_2='multiline 
string 
2' envsubst < output.template 

If you have a lot of variables you can store them in a file and just source it (remember to use export at the end of the sourced file!)