Greg
Greg

Reputation: 6759

Can a bash heredoc put its result directly into a variable?

I have some code like this:

CMD=$(cat <<EOC
docker run 
   -p $MY_IP:$LOCAL_PORT:$LOCAL_PORT -p $MY_IP:$PEER_PORT:$PEER_PORT 
   -v $CERT_DIR:/cert 
   $ETCD_IMAGE 
   --name $MACHINE.$DOMAIN 
   --peer-cert-file=/cert/server-cert.pem
   --peer-key-file=/cert/server-key.pem --peer-ca-file=/cert/ca.pem 
   --peer-addr=$MY_IP:$PEER_PORT 
   --peers=$OIPPC
EOC
)

is there a way to get a here doc to assign result directly to variable in bash without intervening process (cat)? This code works, it just feels like too much work.

Upvotes: 11

Views: 7023

Answers (1)

gniourf_gniourf
gniourf_gniourf

Reputation: 46813

How to put a here-string into a variable in Bash:

In Bash use read with the -d delimiter set to null:

IFS= read -r -d '' cmd <<EOC
    ...blah blah...
EOC

Make sure you really use IFS= like shown, in front of read, otherwise any leading and trailing spaces will be trimmed. Make sure you use -r otherwise some backslashes would be understood as escape backslashes.

Some would argue that it's simpler to just use a plain assignment as:

cmd='
    ...blah blah...
'

But sometimes you have lots of quotes to the point that it becomes simpler and nicer to use this.

Subtle note. With this, read returns a failure return code (1) since the null-byte delimiter is not read before EOF. While this is alright most of the times, it can be a problem if you're using set -e (but you really shouldn't use set -e anyway). If you want to be sure, add:

IFS= read -r -d '' cmd <<EOC || true
    ...blah blah...
EOC

Now, seriously, about your problem.

Below is a serious note that you really should take into account: don't put code into strings! it's broken!. Instead, use a function or (still bad, but not broken) an array. Here's how you would use an array:

mycommand=(
    docker run 
        -p "$MY_IP:$LOCAL_PORT:$LOCAL_PORT"
        -p "$MY_IP:$PEER_PORT:$PEER_PORT"
        -v "$CERT_DIR":/cert
        "$ETCD_IMAGE"
        --name "$MACHINE.$DOMAIN"
        --peer-cert-file=/cert/server-cert.pem
        --peer-key-file=/cert/server-key.pem
        --peer-ca-file=/cert/ca.pem 
        --peer-addr="$MY_IP:$PEER_PORT"
        --peers="$OIPPC"
)

(observe the quotes that I took time to type, with love). Then you can safely run it (by safely I mean it's all right if you're having glob characters or quotes or spaces in your arguments) as:

"${mycommand[@]}"

(observe the healthy quotes, again). If you want to print the command, use this:

printf '%s\n' "${mycommand[*]}"

Unfortunately, the line breaks won't be preserved here. But really, that shouldn't be a problem at all. If really needed, you should pass this command through a formatter of some sort (well, very likely it doesn't exist so you'll have to code it yourself). But put the things in the right order: you want to define a command, to execute it (and, optionally format it, for user display), not the other way round, have a string that's nice to the user's eyes that you then have to parse (dangerously) to transform into code.

Upvotes: 28

Related Questions