bashheredoc

how to display $ in bash script


I have a script. I can't print $ in a screen.

#!/bin/bash

SERVER_IP=""
PASSWORD=""
DOMAIN=""
CONFIG_CONTENT=$(cat << EOF

server {
    listen 80;
    listen [::]:80;

    root /home/webmas/$DOMAIN;

    index index.php index.html index.htm index.nginx-debian.html;

    server_name $DOMAIN;

    location / {
            try_files \$uri \$uri/ =404;
    }

    location ~ \.php$ {
            include snippets/fastcgi-php.conf;
            fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
    }

    location ~ /\.ht {
            deny all;
    }
}

EOF
)

ssh -tt -p 300 webmas@$SERVER_IP << EOF

mkdir $DOMAIN
cd /etc/nginx/sites-available
echo -n "$PASSWORD" | sudo -S sh -c "echo '$CONFIG_CONTENT' | tee /etc/nginx/sites-  available/$DOMAIN >/dev/null"

EOF

$uri is not displayed on the screen

I don't understand why \$ don't help. Single quotes didn't help either. I've run out of ideas.


Solution

  • Let's simplify the problem. Start with a small heredoc:

    var1=a
    var2=b
    doc=$( cat <<EOF
    $var1 \$var2
    EOF
    )
    echo "$doc" # prints: a $var2
    

    Okay, so far so good. The escaped variable wasn't expanded.

    ssh adds a layer of complexity to the problem, so let's skip it for a moment. Instead we'll use cat to inspect how the heredoc we want to pass to ssh actually looks after expansions.

    cat <<EOF
    sh -c "echo '$doc'"
    EOF
    # prints: sh -c "echo 'a $var2'"
    

    Oops, I see a problem. Even though $var2 is inside single quotes in the program that we want to pass to sh, that double quoted string has to be parsed first to create the program that sh receives and $var2 will be expanded at that point. If you copy sh -c "echo 'a $var2'" into a terminal and run it you'll see the output is a b.

    So, now that we know the problem, what are the solutions?

    You could manually add extra escaping to the original document:

    doc=$( cat <<EOF
    $var1 \\\$var2
    EOF
    )
    echo "$doc" # prints: a \$var2
    cat <<EOF
    sh -c "echo '$doc'"
    EOF
    # prints: sh -c "echo 'a \$var2'"
    sh -c "echo 'a \$var2'" # prints a $var2
    

    You could pass the document as an argument to sh instead of directly injecting it into the command string which would remove a layer of interpretation:

    doc=$( cat <<EOF
    $var1 \$var2
    EOF
    )
    echo "$doc" # prints: a $var2
    cat <<EOF
    sh -c 'echo "\$1"' _ '$doc'
    EOF
    # prints: sh -c 'echo "$1"' _ 'a $var2'
    sh -c 'echo "$1"' _ 'a $var2' # prints: a $var2
    

    You could also look at the "automatic requoting" feature documented in BashFAQ/096 or, as another user suggested, place the document in a file and scp it to the host instead of trying to work around the multiple levels of interpretation happening with your heredoc.