SSH not only allows you to connect to remote servers, you can use it to send an ad hoc command or commands to a remote server. This post will cover three different methods to remotely execute multi-line commands with SSH.
Running Remote Commands with SSH
To run one command on a remote server with SSH:
ssh $HOST ls
To run two commands on a remote server with SSH:
ssh $HOST 'ls; pwd'
To run the third, fourth, fifth, etc. commands on a remote server with SSH, keep appending commands with a semicolon inside the single quotes.
But, what if you want to remotely run many more commands, if statements, while loops, etc., and make it all human readable?
#!/bin/bash
ssh $HOST '
ls
pwd
if true; then
echo "This is true"
else
echo "This is false"
fi
echo "Hello world"
'
The preceding shell script works but begins to break if local variables are added.
For example, the following shell script will run, but the local variable HELLO will not be parsed inside the remote if statement:
#!/bin/bash
HELLO="world"
ssh $HOST '
ls
pwd
if true; then
echo $HELLO
else
echo "This is false"
fi
echo "Hello world"
'
In order to parse the local variable HELLO so it is used in the remote if statement, continue onto the next section.
Using SSH with the bash Command
As mentioned above, in order to parse the local variable HELLO so it is used in the remote if statement, the bash
command is used:
#!/bin/bash
HELLO="world"
ssh $HOST bash -c "'
ls
pwd
if true; then
echo $HELLO
else
echo "This is false"
fi
echo "Hello world"
'"
Perhaps you want to use a remote sudo
command within the shell script:
#!/bin/bash
HELLO="world"
ssh $HOST bash -c "'
ls
pwd
if true; then
echo $HELLO
else
echo "This is false"
fi
echo "Hello world"
sudo ls /root
'"
When the preceding shell script is run, everything will work as intended until the remote sudo
command, which will throw the following error:
sudo: sorry, you must have a tty to run sudo
This error is thrown because the remote sudo
command is prompting for a password which needs an interactive tty/shell. To force a pseudo interactive tty/shell, add the -t
command line switch to the ssh
command:
#!/bin/bash
HELLO="world"
ssh -t $HOST bash -c "'
ls
pwd
if true; then
echo $HELLO
else
echo "This is false"
fi
echo "Hello world"
sudo ls /root
'"
With a pseudo interactive tty/shell available, the remote sudo
command’s password prompt will be displayed, the remote sudo password can then be entered, and the contents of the remote root’s home directory will be displayed.
I tried using the bash
command to run specific remote sed
commands with SSH. I wanted the first remote sed
command to find and delete one line and three subsequent lines in a file. I then wanted the second remote sed
command to find a line and insert another line with some text above it in a file.
#!/bin/bash
ssh $HOST bash -c "'
cat << EOFTEST1 > /tmp/test1
line one
line two
line three
line four
EOFTEST1
cat << EOFTEST2 > /tmp/test2
line two
EOFTEST2
sed -i -e '/line one/,+3 d' /tmp/test1
sed -i -e '/^line two$/i line one' /tmp/test2
'"
However, every time I ran the above shell script, I would get the following error:
sed: -e expression #1, char 5: unterminated address regex
But, the same commands worked when run individually:
ssh $HOST "sed -i -e '/line one/,+3 d' /tmp/test1"
ssh $HOST "sed -i -e '/^line two$/i line one' /tmp/test2"
I thought the problem might be because of single quotes within single quotes. The bash
command requires everything to be wrapped in single quotes. The sed
command requires the regular expression to be wrapped in single quotes as well. As stated in the BASH manual:
a single quote may not occur between single quotes, even when preceded by a backslash
However, I debunked this single quote theory being my problem because running a simple remote sed
search and replace command inside of the bash
command worked just fine:
#!/bin/bash
ssh $HOST bash -c "'
echo "Hello" >> /tmp/test3
sed -i -e 's/Hello/World/g' /tmp/test3
'"
I can only assume the problem with these specific remote sed
commands is syntax-related that I have not yet figured out. However, I eventually figured out that these specific remote sed
commands would work when using SSH with HERE documents.
Thanks to Edward Torbett’s comment, he has provided a solution to the problem I was encountering. What follows is a copy of that comment:
The single quotes were indeed the issue you were having, despite your debunking.
You can’t ever use single quotes within single quoted strings, even escaped, so what got passed to SSH was the following command:
sed -i -e /line one/,+3 d /tmp/test1
Note the lack of quotes - This means that the space between the +3 and d causes the d to be interpreted as another parameter rather than part of the expression. Your > debunk test didn’t include the space, so the expression was still parsed correctly.
However - there’s an even easier workaround!
While single quotes can’t be escaped inside a single quoted string, they can be escaped outside one. So all you need to do is end the current quote, use an escaped single > quote, then start a new quote. As long as there’s no spaces, they’ll all get combined into a single parameter.
This would mean that a working, single-quoted sed command would read as follows:
'sed -i -e '\''/line one/,+3 d'\'' /tmp/test1'
For clarity, here’s what it looks like when separated up for clarity:
'sed -i -e ' <-- A single-quoted string \' <-- Escaped single quote '/line one/,+3 d' <-- Another single-quoted string \' <-- The second escaped single quote ' /tmp/test1' <-- The final single-quoted string
Which, when passed across ssh to bash, would be interpreted how you want it:
sed -i -e '/line one/,+3 d' /tmp/test1
Hope this solves a mystery for you!
Using SSH with HERE Documents
As mentioned above, the specific remote sed
commands I wanted to run did work when using SSH with HERE documents:
ssh $HOST << EOF
cat << EOFTEST1 > /tmp/test1
line one
line two
line three
line four
EOFTEST1
cat << EOFTEST2 > /tmp/test2
line two
EOFTEST2
sed -i -e '/line one/,+3 d' /tmp/test1
sed -i -e '/^line two$/i line one' /tmp/test2
EOF
Despite the remote sed
commands working, the following warning message was thrown:
Pseudo-terminal will not be allocated because stdin is not a terminal.
To stop this warning message from appearing, add the -T
command line switch to the ssh
command to disable pseudo-tty allocation (a pseudo-terminal can never be allocated when using HERE documents because it is reading from standard input):
ssh -T $HOST << EOF
cat << EOFTEST1 > /tmp/test1
line one
line two
line three
line four
EOFTEST1
cat << EOFTEST2 > /tmp/test2
line two
EOFTEST2
sed -i -e '/line one/,+3 d' /tmp/test1
sed -i -e '/^line two$/i line one' /tmp/test2
EOF
With this working, I then discovered remote sudo
commands that require a password prompt will not work with HERE documents over SSH.
ssh $HOST << EOF
sudo ls /root
EOF
The above ssh
command will throw the following error if the remote user you are logging in to requires a password when using the remote sudo
command:
Pseudo-terminal will not be allocated because stdin is not a terminal.
user@host's password:
sudo: no tty present and no askpass program specified
However, the remote sudo
command will work if the remote user’s sudo settings allow that user to use sudo without a password by configuring the following in /etc/sudoers:
user ALL=(ALL) NOPASSWD: ALL