What is the cleanest way to ssh and run multiple commands in Bash?
Asked Answered
P

14

464

I already have an ssh agent set up, and I can run commands on an external server in Bash script doing stuff like:

ssh blah_server "ls; pwd;"

Now, what I'd really like to do is run a lot of long commands on an external server. Enclosing all of these in between quotation marks would be quite ugly, and I'd really rather avoid ssh'ing multiple times just to avoid this.

So, is there a way I can do this in one go enclosed in parentheses or something? I'm looking for something along the lines of:

ssh blah_server (
   ls some_folder;
   ./someaction.sh;
   pwd;
)

Basically, I'll be happy with any solution as long as it's clean.

Edit

To clarify, I'm talking about this being part of a larger bash script. Other people might need to deal with the script down the line, so I'd like to keep it clean. I don't want to have a bash script with one line that looks like:

ssh blah_server "ls some_folder; ./someaction.sh 'some params'; pwd; ./some_other_action 'other params';"

because it is extremely ugly and difficult to read.

Pipette answered 10/12, 2010 at 18:50 Comment(6)
Hmm, how about putting all that into a script on the server and just calling it with one ssh invocation?Veiling
@Nikolai if the commands depends on the client side, they can be written into a shell script, then scp, ssh, and run. This will the cleanest way, I think.Vocalise
This is part of a bigger bash script, so I'd rather not split it up with half living on my personal computer and the other half living on the server and run through ssh. If at all possible, I'd really like to just keep it as one script run from my personal computer. Is there really no clean way to encase a bunch of commands in an ssh?Pipette
Best way is not to use bash but Perl, Python, Ruby, etc.Overwhelm
Why do you want to avoid putting the remote commands in quotes? You can have newlines inside the quotes, as many as you like; and using a string instead of standard input means standard input is available for e.g. reading input to the remote script. (Though on Unix, single quotes are usually to be preferred over double quotes, unless you specifically need the local shell to evaluate some parts of the string.)Stucco
@Vocalise See the other answer how to run a script on the client side on the server. e.g. 'ssh <remote-user>@<remote-host> "bash -s" <./remote-commands.sh -- arg1 arg2'. (not the answer to this question which asks how to do it with commands inline)Barnwell
C
593

How about a Bash Here Document:

ssh otherhost << EOF
  ls some_folder; 
  ./someaction.sh 'some params'
  pwd
  ./some_other_action 'other params'
EOF

To avoid the problems mentioned by @Globalz in the comments, you may be able to (depending what you're doing on the remote site) get away with replacing the first line with

ssh otherhost /bin/bash << EOF

Note that you can do variable substitution in the Here document, but you may have to deal with quoting issues. For instance, if you quote the "limit string" (ie. EOF in the above), then you can't do variable substitutions. But without quoting the limit string, variables are substituted. For example, if you have defined $NAME above in your shell script, you could do

ssh otherhost /bin/bash << EOF
touch "/tmp/${NAME}"
EOF

and it would create a file on the destination otherhost with the name of whatever you'd assigned to $NAME. Other rules about shell script quoting also apply, but are too complicated to go into here.

Carlson answered 10/12, 2010 at 19:3 Comment(23)
This looks like exactly what I want! How's it work? Would you happen to have a link to a page that explains it?Pipette
+1 Was just thinking that myself -- here's one place to read about it: tldp.org/LDP/abs/html/here-docs.htmlInterpreter
When I use a heredoc like this the "Message of the Day" login is outputted to my local. Is there a way to silence this from displaying?Newsome
I also get this output to my local: Pseudo-terminal will not be allocated because stdin is not a terminal.Newsome
It may be important for you to quote the word (i.e. first 'EOF') in order to prevent expansion of the command lines. See: man bash | less +/'Here Documents'Gaige
I wanted this tabbed in a script, by using <<- EOF it will consume the tabs, but note the last EOF must be on the same line.Bassoon
I seemed to have to quote it all so I could pipe the output somewhere like this [code] ssh piback "/bin/bash << EOF lvcreate -L500M -s -n mailbackup /dev/cruz/mail >/dev/null mkdir -p /mnt/mail-backup mount -o ro,noload /dev/cruz/mailbackup /mnt/mail-backup tar -czp /mnt/mail-backup umount /mnt/mail-backup lvremove -f /dev/cruz/mailbackup > /dev/null EOF" | cat - > pibak-mail.tar.gzHayden
comment system wouldn't let me format previous comment, so its a messHayden
@Hayden looks a lot like my remote backup script except I use rsync with --link-dest to keep multiple versions.Carlson
@PaulTomblin it is just like your script except to make it work I had to quote (with ") all the way from /bin/bash right through to the EOF at the endHayden
For some reason "here document" didn't work for me. It was always existing after a particular command (may be that command is returning non zero exit code). However this worked - ssh $HOST 'ls ; pwd ; cmd3 ; cmd4'Inhabitancy
It even worked when I used ~ for the home folder. It took the remote server users' home folder, just like I wanted.Dowable
When I use a 'sudo -S' command inside the EOF-brackets I'm not asked for my password (like I am when running a single SSH command in a script). How do we do this?Implead
@Implead don't use sudo -S in a script. You should never put your password in a script, especially not if you have sudo privileges.Carlson
I don't put the password in the script. I type it in the terminal. But it doesn't work in heredocsImplead
@Implead because you use -S which says to take the password from stdin rather than from the terminal.Carlson
You can also use something like /bin/sh -e instead of /bin/bash to exit immediately if any command fails.Flowerage
Paul, I'm wondering if you might add a note about quoting here? Since heredocs don't know they're being used to control another shell, variables containing spaces or quotes could get dangerous. In really recent versions of bash it would be easiest to do e.g. ssh <host> <<-EOF mv ${source@Q} ${dest@Q} EOF. For older versions, variations on printf '%q' "$myVariable" should suffice.Coitus
@Coitus the original question didn't include anything that needed special quoting. Let's not overcomplicate a seven-year-old question with detail that's interesting but not necessarily relevant.Carlson
Paul, I only suggest it because with nearly 200k views on this question, it looks like a lot of people are coming here when scripting with ssh. It's common to need to inject values when scripting. If not for the noise in the other questions and comments I would just make one and be on my way, but it's unlikely to be seen at this stage. A one-line footnote might save people some major headaches.Coitus
Please consider taking the "/bin/bash" to your standard answer and not offer it as optional as it does not work correctly without it. Thanx for the answer anyway.Opal
I added the /bin/bash and variables work just fine, e.g. echo $NAME;Antimalarial
Is there a way to get the stdout and stdin of the remote server to your local terminal using this << EOF method?Peipus
I
154

Edit your script locally, then pipe it into ssh, e.g.

cat commands-to-execute-remotely.sh | ssh blah_server

where commands-to-execute-remotely.sh looks like your list above:

ls some_folder
./someaction.sh
pwd;
Interpreter answered 10/12, 2010 at 19:0 Comment(6)
This has the great advantage that you know exactly what is being executed by the remote script - no problems with quoting. If you need dynamic commands, you can use a shell script with a subshell, still piping into the ssh, i.e. ( echo $mycmd $myvar ; ...) | ssh myhost - as with the cat usage, you know exactly what is going into the ssh command stream. And of course the subshell in the script can be multi-line for readability - see linuxjournal.com/content/bash-sub-shellsWeatherby
Can you do this with arguments in the commands-to-execute-remotely.sh?Airing
I think this is far more useful than paultomblin solution. Since the script can be redirected to any ssh server without having to open the file. Also it's far more readable.Quintal
yes, but using echo or a here document (see top answer) : use: $localvar to interpret a locally-defined variable, \$remotevar to interpret remotely a remotely-defined variable, \$(something with optionnal args) to get the output of something executed on the remote server. An exemple that you can ssh through 1 (or, like shown here, multiple ssh commands) : echo " for remotedir in /*/${localprefix}* ; do cd \"\$remotedir\" && echo \"I am now in \$(pwd) on the remote server \$(hostname) \" ; done " | ssh user1@hop1 ssh user2@hop2 ssh user@finalserver bash Renzo
Hmm...how is this different from using input redirection, i.e. ssh blah_server < commands-to-execute-remotely.sh?Midpoint
Different approaches but in practice they do the exact same thing.Ablate
S
53

To match your sample code, you can wrap your commands inside single or double qoutes. For example

ssh blah_server "
  ls
  pwd
"
Suspiration answered 27/6, 2013 at 8:17 Comment(5)
I like this format, however it sadly is not useful for storing std data into a variable.Velites
Signus, what do you mean by "storing std data into a variable"?Suspiration
@Signus It is perfectly possible to do what you describe, although you will probably want to use single quotes instead of double quotes around the remote commands (or escape the operators which need to be escaped inside double quotes to prevent your local shell from intercepting and interpolating them).Stucco
I am not able to put inside double quotes code a command like this fileToRemove=$(find . -type f -name 'xxx.war'). fileToRemove should have a filename inside but instead it has an empty string. Does something need to be escaped?Typology
@Typology yes, the $.Jambeau
D
46

I see two ways:

First you make a control socket like this:

 ssh -oControlMaster=yes -oControlPath=~/.ssh/ssh-%r-%h-%p <yourip>

and run your commands

 ssh -oControlMaster=no -oControlPath=~/.ssh/ssh-%r-%h-%p <yourip> -t <yourcommand>

This way you can write an ssh command without actually reconnecting to the server.

The second would be to dynamically generate the script, scping it and running.

Dirk answered 10/12, 2010 at 18:56 Comment(0)
W
28

This can also be done as follows. Put your commands in a script, let's name it commands-inc.sh

#!/bin/bash
ls some_folder
./someaction.sh
pwd

Save the file

Now run it on the remote server.

ssh user@remote 'bash -s' < /path/to/commands-inc.sh

Never failed for me.

Wood answered 26/8, 2014 at 23:26 Comment(8)
Similar to what I was thinking originally! But why is bash -s needed?Midpoint
Also, is #!/bin/bash really used?Midpoint
The -s is there for compatibility. From man bash -s If the -s option is present, or if no arguments remain after option processing, then commands are read from the standard input. This option allows the positional parameters to be set when invoking an interactive shell.Wood
But you are right, the #!/bin/bash is not really needed. This was an example of running an existing local script on a remote server.Wood
R J, thanks! With my first comment, it looks like we just do ssh user@remote < /path/to/commands-inc.sh (it seems to work for me). Well, I guess your version ensures we use the bash shell, and not some other shell - that's the purpose, isn't it?Midpoint
Thanks. Yep, some of us have our notes, and habits from older versions of bash and other shells. :-)Wood
Not bad, but if you need variables/flags from the original script used within your ssh commands this is not useful unfortunately. It is clean though, if you dont need that.Zest
@RJ not sure how its different to my answer :). Anyway CheersSelway
S
16

Put all the commands on to a script and it can be run like

ssh <remote-user>@<remote-host> "bash -s" <./remote-commands.sh
Selway answered 5/5, 2014 at 4:50 Comment(6)
Somewhat odd but it works well. And args can be passed to the script (either before or after the redirect). e.g. 'ssh <remote-user>@<remote-host> "bash -s" arg1 <./remote-commands.sh arg2' e.g. 'ssh <remote-user>@<remote-host> "bash -s" -- -arg1 <./remote-commands.sh arg2'Barnwell
I had a similar question with another answer above, but what is the purpose of including bash -s?Midpoint
@Midpoint -s is to indicate the bash to read the commands from standard input. Here is the description from the man pages If the -s option is present, or if no arguments remain after option processing, then commands are read from the standard input. This option allows the positional parameters to be set when invoking an interactive shell. Selway
@JaiPrakash Thanks for this, but I was actually thinking if we can just do away with the bash command altogether, i.e. ssh remote-user@remote-host <./remote-commands.sh. I tried my way and it seemed to work, though I'm not sure if it's deficient in some way.Midpoint
@Midpoint from my understanding of the man pages there wont be any deficiency without that option. Its required if you are trying to pass on any arguments. -- thanksSelway
You cannot use flag variables & constants from your original script inside your remote-commands.sh ... :)Zest
O
12

Not sure if the cleanest for long commands but certainly the easiest:

ssh user@host "cmd1; cmd2; cmd3"
Opal answered 9/11, 2018 at 17:42 Comment(2)
Best in simplicity!Disturbing
This breaks down as soon as you need additional quoting in the remote command. Any dollar signs or backticks you need to pass through to the remote shell will need to be backslashed, or you will need additional quoting. Single quotes instead of double are a good solution for simpler quoting conundrums, but in this case tend to only push the problem to a different corner, as the remote shell will eat up one level of escaping.Stucco
W
11

This works well for creating scripts, as you do not have to include other files:

#!/bin/bash
ssh <my_user>@<my_host> "bash -s" << EOF
    # here you just type all your commmands, as you can see, i.e.
    touch /tmp/test1;
    touch /tmp/test2;
    touch /tmp/test3;
EOF

# you can use '$(which bash) -s' instead of my "bash -s" as well
# but bash is usually being found in a standard location
# so for easier memorizing it i leave that out
# since i dont fat-finger my $PATH that bad so it cant even find /bin/bash ..
Wadai answered 16/10, 2015 at 7:55 Comment(3)
The "$(which bash) -s" part gives you the location of bash on the local machine, rather than the remote machine. I think you want '$(which bash) -s' instead (single-quotes to suppress local parameter substitution).Goddamned
Paul Tomblin provided the Bash Here Document answer 6 years earlier. What value is added by this answer?Walls
-s flag, I cite him you may be able to get away with replacing the first line with..., and I wasn't able to get away with just calling bash I dimly remember.Wadai
S
8

SSH and Run Multiple Commands in Bash.

Separate commands with semicolons within a string, passed to echo, all piped into the ssh command. For example:

echo "df -k;uname -a" | ssh 192.168.79.134

Pseudo-terminal will not be allocated because stdin is not a terminal.
Filesystem     1K-blocks    Used Available Use% Mounted on
/dev/sda2       18274628 2546476  14799848  15% /
tmpfs             183620      72    183548   1% /dev/shm
/dev/sda1         297485   39074    243051  14% /boot
Linux newserv 2.6.32-431.el6.x86_64 #1 SMP Sun Nov 10 22:19:54 EST 2013 x86_64 x86_64 x86_64 GNU/Linux
Souse answered 6/5, 2015 at 21:44 Comment(2)
arnab, in the future, please describe what your code does briefly. Then talk about how it works, then put in the code. Then put the code in a code block so it's easy to read.Swee
From the question, you offered exactly what the OP wants to avoid: "I don't want to have a bash script with one line that looks like..."Walls
A
7

For anyone stumbling over here like me - I had success with escaping the semicolon and the newline:

First step: the semicolon. This way, we do not break the ssh command:

ssh <host> echo test\;ls
                    ^ backslash!

Listed the remote hosts /home directory (logged in as root), whereas

ssh <host> echo test;ls
                    ^ NO backslash

listed the current working directory.

Next step: breaking up the line:

                      v another backslash!
ssh <host> echo test\;\
ls

This again listed the remote working directory - improved formatting:

ssh <host>\
  echo test\;\
  ls

If really nicer than here document or quotes around broken lines - well, not me to decide...

(Using bash, Ubuntu 14.04 LTS.)

Albinaalbinism answered 28/10, 2016 at 11:33 Comment(4)
What's even better you can use && and || this way, too - echo test \&\& lsHenry
@MiroKropacek That's nice, too. Better? Depends on what you want; run second command conditionally, then yes, run it unconditionally (no matter if first one succeeded or failed), then not...Albinaalbinism
@MiroKropacek, I didn't have to escape the && when putting double quotes around the commands: ssh host "echo test && ls"Disturbing
Either quoting or backslashing is necessary but obviously not both, here (though there are situations where you do need to quote shell metacharacters from both the local and the remote shell).Stucco
I
7

The posted answers using multiline strings and multiple bash scripts did not work for me.

  • Long multiline strings are hard to maintain.
  • Separate bash scripts do not maintain local variables.

Here is a functional way to ssh and run multiple commands while keeping local context.

LOCAL_VARIABLE=test

run_remote() {
    echo "$LOCAL_VARIABLE"
    ls some_folder; 
    ./someaction.sh 'some params'
    ./some_other_action 'other params'
}

ssh otherhost "$(set); run_remote"
Inartificial answered 4/7, 2017 at 4:11 Comment(2)
You're thinking about this as if the only reason someone would want do to this is if they're sitting at a command line. There are other common reasons for this question as well, such as executing commands across distributed environments with tools such as rundeck, jenkins, etc.Chanda
@KamilCuk's answer is a (much!) better version of this, but more complex and hard to read and understand.Stucco
D
7

For simple commands you can use:

ssh <ssh_args> command1 '&&' command2

or

ssh <ssh_args> command1 \&\& command2
Deibel answered 5/2, 2021 at 12:39 Comment(0)
L
6

The easiest way to configure your system to use single ssh sessions by default with multiplexing.

This can be done by creating a folder for the sockets:

mkdir ~/.ssh/controlmasters

And then adding the following to your .ssh configuration:

Host *
    ControlMaster auto
    ControlPath ~/.ssh/controlmasters/%r@%h:%p.socket
    ControlPersist 10m

Now, you do not need to modify any of your code. This allows multiple calls to ssh and scp without creating multiple sessions, which is useful when there needs to be more interaction between your local and remote machines.

Thanks to @terminus's answer, http://www.cyberciti.biz/faq/linux-unix-osx-bsd-ssh-multiplexing-to-speed-up-ssh-connections/ and https://en.wikibooks.org/wiki/OpenSSH/Cookbook/Multiplexing.

Luetic answered 23/9, 2016 at 8:7 Comment(0)
E
6

What is the cleanest way to ssh and run multiple commands in Bash?

I recommend using this escaping function. The function takes one argument - a function to escape. Then sshqfunc outputs declare -f of the function and then outputs a string that will call the function with "$@" arguments properly quoted. Then the whole is "%q" quoted and bash -c is added. In case the remote does not have bash, you could change bash to sh.

sshqfunc() { echo "bash -c $(printf "%q" "$(declare -f "$@"); $1 \"\$@\"")"; };

Then define a function with the work you want to do on the remote. The function is defined normally, so it will be properly "clean". You can test such function locally. After defining, properly escaped function is passed to the remote.

work() {
   ls
   pwd
   echo "Some other command"
}

ssh host@something "$(sshqfunc work)"

Passing You can also pass arguments, and they will be passed to your function as positional arguments. The right next argument after the function will be assigned to $0 - usually a placeholder like -- or _ is used to separate arguments from call.

work() {
   file=$1
   num=$2
   ls "$file"
   echo "num is $num"
}

ssh host@something "$(sshqfunc work)" -- /this/file 5

But note that arguments should also be properly quoted if there are any magic characters:

ssh host@something "$(sshqfunc work)" -- "$(printf "%q" "$var1" "$var2")"
Ethyne answered 13/7, 2021 at 9:4 Comment(1)
Good recommendations overall, but definitely not understandable or maintainable for people who are not well versed in Bash.Stucco

© 2022 - 2024 — McMap. All rights reserved.