Graham Leggett
Graham Leggett

Reputation: 1148

Escaping the close-bracket character in a bash v3 heredoc

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

Answers (3)

phyatt
phyatt

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

chepner
chepner

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

Etan Reisner
Etan Reisner

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

Related Questions