Reputation: 1148
I have a need to embed a fragment of a shell script in a heredoc as part of the creation of a cloud-init script to provision an Ubuntu 14.04 LTE machine. A simplified version of the script demonstrating the problem is as follows:
#!/bin/bash
cloudconfig=$(cat <<EOF
if host \$NAMESERVER 1>/dev/null 2>&1; then
case \$reason in
BOUND|RENEW|REBIND|REBOOT) nsupdate -k /var/lib/dhcp/nsupdate.key << EOX
server \$NAMESERVER
update delete \$HOST_NAME A
update add \$HOST_NAME \$TTL A \$HOST_ADDR
send
EOX
;;
esac
fi
EOF
)
echo "${cloudconfig}"
Running the above script fails as follows:
Little-Net:orchestration minfrin$ bash /tmp/test.sh
could not read key from /var/lib/dhcp/nsupdate.{private,key}: file not found
couldn't get address for '$NAMESERVER': not found
The problematic character is the closing bracket to the right of "REBOOT", and the obvious solution is to escape the character:
BOUND|RENEW|REBIND|REBOOT\) nsupdate -k /var/lib/dhcp/nsupdate.key << EOX
This backslash character however ends up in the final cloudconfig variable, which in turn breaks the output:
Little-Net:orchestration minfrin$ bash /tmp/test.sh
if host $NAMESERVER 1>/dev/null 2>&1; then
case $reason in
BOUND|RENEW|REBIND|REBOOT\) nsupdate -k /var/lib/dhcp/nsupdate.key << EOX
server $NAMESERVER
update delete $HOST_NAME A
update add $HOST_NAME $TTL A $HOST_ADDR
send
EOX
;;
esac
fi
This particular fragment above is part of a larger file that is being written that relies on variable interpolation, so quoting there heredoc with >>"EOF" is going to break the rest of our script.
How do I escape the ")" character without the escape character leaking through the heredoc?
Upvotes: 1
Views: 2228
Reputation: 19112
I solved it by escaping the parenths manually inside the HEREDOC
and then follow it with a sed
statement to remove the backslashes.
long_string=$(cat << 'HEREDOC'
This is a string with \( escaped \) parenthesis.
HEREDOC
)
long_string=$(echo $long_string | sed -e 's:\\(:(:g' -e 's:\\):):g')
Using bash 4+ would be better, but then I have to get everyone on my team using mac's to also get bash 4+.
Upvotes: 0
Reputation: 531205
Since you don't have any parameters you actually want to expand inside the here document, I would just quote the entire thing (which saves you a lot of explicit backslashes):
#!/bin/bash
cloudconfig=$(cat <<'EOF'
if host $NAMESERVER 1>/dev/null 2>&1; then
case $reason in
BOUND|RENEW|REBIND|REBOOT) nsupdate -k /var/lib/dhcp/nsupdate.key << EOX
server $NAMESERVER
update delete $HOST_NAME A
update add $HOST_NAME $TTL A $HOST_ADDR
send
EOX
;;
esac
fi
EOF
)
echo "${cloudconfig}"
Note that you cannot indent EOX
either, or else that here document will not be correctly terminated when you go to use it.
Even easier, though, is to not use a here document at all; just use embedded newlines in the parameter assignment.
#!/bin/bash
cloudconfig='
if host $NAMESERVER 1>/dev/null 2>&1; then
case $reason in
BOUND|RENEW|REBIND|REBOOT) nsupdate -k /var/lib/dhcp/nsupdate.key << EOX
server $NAMESERVER
update delete $HOST_NAME A
update add $HOST_NAME $TTL A $HOST_ADDR
send
EOX
;;
esac
fi'
echo "${cloudconfig}"
If you need to allow some parameter expansion, you can still use the multiline string with some modifications. Whenever you need an interpolation, close the single quote and immediately open the double quote. After the expansion is complete, close the double and reopen the single.
cloudconfig='
if [[ $SOMEVARIABLE =='"$value"' ]]; then
...
'
Upvotes: 2
Reputation: 80931
As this seems to be a bash 3.x parsing issue (as it works in bash 4.x as can be seen here) you would need to avoid the parser getting confused. This seems to work for me:
#!/bin/bash
rp=")"
cloudconfig=$(cat <<EOF
if host \$NAMESERVER 1>/dev/null 2>&1; then
case \$reason in
BOUND|RENEW|REBIND|REBOOT${rp} nsupdate -k /var/lib/dhcp/nsupdate.key << EOX
server \$NAMESERVER
update delete \$HOST_NAME A
update add \$HOST_NAME \$TTL A \$HOST_ADDR
send
EOX
;;
esac
fi
EOF
)
echo "${cloudconfig}"
Upvotes: 4