23

I'm trying to write a Bash script that will SSH into a machine and create a directory. The long-term goal is a bit more complicated, but for now I'm starting simple. However, as simple as it is, I can't quite seem to get it. Here's my code:

#!/bin/bash
ssh -T [email protected] <<EOI

# Fix "TERM environment variable undefined" error.
TERM=dumb
export TERM

# Store todays date.
NOW=$(date +"%F")
echo $NOW

# Store backup path.
BACKUP="/backup/$NOW"
[ ! -d $BACKUP ] && mkdir -p ${BACKUP}
echo $BACKUP

exit
EOI

It runs without any explicit errors. However, the echoed $NOW and $BACKUP variables appear empty, and the /backup directory is not created. How do I fix this?

0

6 Answers 6

30

The shell on the local host is doing variable substitution on $NOW and $BACKUP because the "EOI" isn't escaped. Replace

 ssh [email protected] <<EOI

with

 ssh [email protected] <<\EOI
Sign up to request clarification or add additional context in comments.

4 Comments

Thanks. This required the fewest changes, and works perfectly.
Do either of you have an example that allows variables passed into the ssh session? For instance, if I had the block @Cerin used earlier in a bash script and before the ssh ... I declare REMOTE_PATH=root/stuff/buildpath , can I use $REMOTE_PATH or the like inside my ssh / heredoc ?
@Brian, Using hbar's example below, if you just don't escape $REMOTE_PATH, then it should be inserted into the ssh script ran on the remote server.
In addition to \, we can also use single quotes to wrap the LimitString, in this case EOI to escape special characters in here documents, for example ssh [email protected] <<'EOI'.
21

The variables are being evaluated in the script on the local machine. You need to subsitute the dollar signs with escaped dollar signs.

#!/bin/bash
ssh -T [email protected] <<EOI

# Fix "TERM environment variable undefined" error.
TERM=dumb
export TERM

# Store todays date.
NOW=\$(date +"%F")
echo \$NOW

# Store backup path.
BACKUP="/backup/\$NOW"
[ ! -d \$BACKUP ] && mkdir -p \${BACKUP}
echo \$BACKUP

exit
EOI

1 Comment

Perfect :) :) Explanation ... I think this makes rest of the discussion pretty useless ..
6

Your script is doing substitution on the local host before being sent over.

Change your first line to:

ssh -T [email protected] <<'EOI'

This will cause the raw script to get sent over and interpreted on your remote host.

If you wanted a mix (so for example, if you wanted the date command executed on your local host, you should leave ssh line unchanged and quote the individual command):

ssh -T [email protected] <<EOI

# Execute the date command on the local machine.  The assignment still
# happens on the remote machine
NOW=$(date +"%F")

# Quote your $ so that the replacement happens on the remote machine
echo \$NOW

1 Comment

Your second example quotes the here-doc delimiter and the variable. I think you intended to only escape the variable. Otherwise +1
1

Double-escaping a bash script is really hard. Don't do it. Let bash do it. You can serialize a bash function to the remote and then call it.

#!/bin/bash
work() {
   # Fix "TERM environment variable undefined" error.
   TERM=dumb
   export TERM

   # Store todays date.
   NOW=$(date +"%F")
   echo $NOW

   # Store backup path.
   BACKUP="/backup/$NOW"
   [ ! -d $BACKUP ] && mkdir -p ${BACKUP}
   echo $BACKUP

   exit
}
exmaplevariable=value
ssh -T [email protected] "$(printf "%q " bash -c "
   $(declare -p examplevariable);$(declare -f work); work
")"

The printf "%q will take care of escaping the script for the remote shell. The declare -p will output variables in a source-able format and allows for transfering variables values. The declare -f will output the function in soure-able format and allows for transfering code.

That way you can check your script in shellcheck and easily find and fix mistakes and program normally, just the call to ssh is complicated.

It becomes also much easier with a helper wrapper function that passes arguments:

ssh_remote_function() {
    ssh "$1" "$(printf "%q " bash -c "$(declare -f $2); $2 \"\$@\"" "$2" "${@:3}")"
}
work() {
    examplevalue=$1
    .....
}
ssh_remote_function [email protected] work "$examplevalue"

Such a helper wrapper could be expanded with some additional options parsing. I added my function to my .bashrc in my git repo at https://gitlab.com/Kamcuk/kamilscripts/-/blob/master/etc/bash.d/ssh_remote_function.sh .

Comments

0

How to run a local script over SSH

Synopsis:

Script execution over SSH without copying script file. You need a simple SSH connexion and a local script.

Code:

#!/bin/sh
print_usage() {
        echo -e "`basename $0` ssh_connexion local_script"
        echo -e "Remote executes local_script on ssh server"
        echo -e "For convinient use, use ssh public key for remote connexion"
        exit 0
}

[ $# -eq "2" ] && [ $1 != "-h" ] && [ $1 != "--help" ] || print_usage

INTERPRETER=$(head -n 1 $2 | sed -e 's/#!//')

cat $2 | grep -v "#" | ssh -t $1 $INTERPRETER

Examples:

  • ssh-remote-exec root@server1 myLocalScript.sh #for Bash
  • ssh-remote-exec root@server1 myLocalScript.py #for Python
  • ssh-remote-exec root@server1 myLocalScript.pl #for Perl
  • ssh-remote-exec root@server1 myLocalScript.rb #for Ruby

Step by step explanations

This script performs this operations: 1° catches first line #! to get interpreter (i.e: Perl, Python, Ruby, Bash interpreter), 2° starts remote interpeter over SSH, 3° send all the script body over SSH.

Local Script:

Local script must start with #!/path/to/interpreter - #!/bin/sh for Bash script - #!/usr/bin/perl for Perl script - #!/usr/bin/python for Python script - #!/usr/bin/ruby for Ruby script

This script is not based on local script extension but on #! information.

2 Comments

What's with all the -es? POSIX doesn't require echo -e to do anything but print -e on output; in fact, doing anything else is explicitly noncompliant, and bash can be configured at either compile-time or runtime to comply with the letter of the standard, so behavior of echo -e is undefined and widely variable. (If your /bin/sh is provided by dash instead of bash, you'll get the POSIX-compliant behavior, so output will have a bunch of -e strings in it with this script written as it is).
Consider also running your code through shellcheck.net and fixing the quoting issues it identifies.
-3

Try:

NOW=`date +"%F"`

4 Comments

$() is the same as `` except the former easily allows nesting. This is something any POSIX-compliant sh can handle.
@GreenMatt, not just "newer bash versions", all shells compliant with the 1992 POSIX sh standard.
@GreenMatt, sure, but you were speaking about bash specifically, so we're not talking about Bourne here. And I'm not aware of any released version of bash that lacked $(...); it was certainly there in 2.0, and isn't listed as a change from 1.x either, making it presumptively something that wasn't new even then.
Anyhow, this isn't answering the question at all -- the question was how to run a script over ssh. date is incidental.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.