384

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.

Community
  • 1
  • 1
Eli
  • 31,424
  • 32
  • 127
  • 194
  • 2
    Hmm, how about putting all that into a script on the server and just calling it with one `ssh` invocation? – Nikolai Fetissov Dec 10 '10 at 18:53
  • @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. – khachik Dec 10 '10 at 18:57
  • 7
    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? – Eli Dec 10 '10 at 19:02
  • Best way is not to use bash but Perl, Python, Ruby, etc. – salva May 05 '14 at 09:49
  • 1
    *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.) – tripleee Dec 29 '15 at 11:36
  • @khachik See the other answer how to run a script on the client side on the server. e.g. 'ssh @ "bash -s" <. answer="" arg1="" arg2="" asks="" commands="" do="" how="" inline="" it="" question="" the="" this="" to="" which="" with=""> – gaoithe Mar 10 '16 at 17:23

13 Answers13

495

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.

Paul Tomblin
  • 167,274
  • 56
  • 305
  • 392
  • 6
    This looks like exactly what I want! How's it work? Would you happen to have a link to a page that explains it? – Eli Dec 10 '10 at 19:06
  • 9
    +1 Was just thinking that myself -- here's one place to read about it: http://tldp.org/LDP/abs/html/here-docs.html – bosmacs Dec 10 '10 at 19:07
  • 1
    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? – Globalz Nov 05 '12 at 22:43
  • 16
    I also get this output to my local: Pseudo-terminal will not be allocated because stdin is not a terminal. – Globalz Nov 05 '12 at 22:53
  • 16
    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' – assem May 07 '13 at 09:27
  • I wanted this tabbed in a script, by using < – JStrahl May 14 '13 at 12:53
  • 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.gz – akc42 Feb 12 '14 at 07:52
  • 1
    comment system wouldn't let me format previous comment, so its a mess – akc42 Feb 12 '14 at 07:59
  • @akc42 looks a lot like my remote backup script except I use `rsync with --link-dest` to keep multiple versions. – Paul Tomblin Feb 12 '14 at 13:03
  • @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 end – akc42 Feb 13 '14 at 20:07
  • 1
    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' – Rishi Mar 28 '14 at 21:29
  • It even worked when I used ~ for the home folder. It took the remote server users' home folder, just like I wanted. – s3v1 Nov 25 '14 at 12:21
  • 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? – Richard May 10 '16 at 09:42
  • @Richard don't use `sudo -S` in a script. You should never put your password in a script, especially not if you have sudo privileges. – Paul Tomblin May 10 '16 at 10:59
  • I don't put the password in the script. I type it in the terminal. But it doesn't work in heredocs – Richard May 10 '16 at 13:12
  • @Richard because you use `-S` which says to take the password from stdin rather than from the terminal. – Paul Tomblin May 10 '16 at 13:26
  • You can also use something like `/bin/sh -e` instead of `/bin/bash` to exit immediately if any command fails. – exic Aug 15 '17 at 10:25
  • 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 < – koyae Nov 13 '17 at 23:06
  • @koyae 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. – Paul Tomblin Nov 13 '17 at 23:46
  • 6
    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. – koyae Nov 15 '17 at 00:28
  • 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. – DimiDak Nov 09 '18 at 17:57
  • I added the /bin/bash and variables work just fine, e.g. echo $NAME; – Maciej Krawczyk May 16 '19 at 08:37
128

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;
bosmacs
  • 7,069
  • 4
  • 28
  • 31
  • 7
    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 http://www.linuxjournal.com/content/bash-sub-shells – RichVel Jul 17 '13 at 06:07
  • 7
    Can you do this with arguments in the `commands-to-execute-remotely.sh`? – elaRosca Feb 21 '14 at 08:18
  • 1
    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. – Patrick Bassut Mar 26 '14 at 21:59
  • 4
    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 ` – Olivier Dulac Mar 08 '16 at 14:18
  • 4
    Hmm...how is this different from using input redirection, i.e. `ssh blah_server < commands-to-execute-remotely.sh`? – flow2k Feb 27 '18 at 19:26
  • Different approaches but in practice they do the exact same thing. – parttimeturtle Feb 01 '21 at 03:44
44

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

ssh blah_server "
  ls
  pwd
"
Andrei B
  • 2,647
  • 13
  • 12
  • I like this format, however it sadly is not useful for storing std data into a variable. – Signus Oct 04 '13 at 18:49
  • 1
    Signus, what do you mean by "storing std data into a variable"? – Andrei B Nov 16 '13 at 12:40
  • 1
    @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). – tripleee Dec 29 '15 at 11:25
  • 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? – jkonst Feb 05 '19 at 09:10
40

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.

terminus
  • 12,035
  • 8
  • 30
  • 36
23

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.

R J
  • 1,576
  • 13
  • 14
  • Similar to what I was thinking originally! But why is `bash -s` needed? – flow2k Feb 27 '18 at 19:32
  • Also, is `#!/bin/bash` really used? – flow2k Feb 27 '18 at 19:36
  • 4
    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. – R J Feb 28 '18 at 01:45
  • 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. – R J Feb 28 '18 at 01:49
  • 1
    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? – flow2k Feb 28 '18 at 05:04
  • 2
    Thanks. Yep, some of us have our notes, and habits from older versions of bash and other shells. :-) – R J Feb 28 '18 at 08:30
  • 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. – trainoasis Aug 16 '19 at 06:55
  • 1
    @RJ not sure how its different to my answer :). Anyway Cheers – Jai Prakash Mar 26 '21 at 01:32
14

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

ssh <remote-user>@<remote-host> "bash -s" <./remote-commands.sh
Jai Prakash
  • 1,676
  • 21
  • 26
  • Somewhat odd but it works well. And args can be passed to the script (either before or after the redirect). e.g. 'ssh @ "bash -s" arg1 <. arg2="" e.g.="">@ "bash -s" -- -arg1 <. arg2=""> – gaoithe Mar 10 '16 at 17:17
  • I had a similar question with another answer above, but what is the purpose of including `bash -s`? – flow2k Feb 27 '18 at 19:37
  • 1
    @flow2k -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. ``` – Jai Prakash Feb 27 '18 at 23:52
  • @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 <. and="" deficient="" i="" if="" in="" it="" my="" not="" seemed="" some="" sure="" though="" to="" tried="" way="" way.="" work=""> – flow2k Feb 28 '18 at 00:20
  • @flow2k 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. -- thanks – Jai Prakash Mar 02 '18 at 00:28
  • You cannot use flag variables & constants from your original script inside your remote-commands.sh ... :) – trainoasis Aug 16 '19 at 06:56
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
Eric Leschinski
  • 123,728
  • 82
  • 382
  • 321
arnab
  • 89
  • 1
  • 1
  • 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. – Eric Leschinski May 07 '15 at 01:20
  • 1
    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..."* – jww Oct 25 '16 at 08:15
8

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 ..
sjas
  • 15,508
  • 11
  • 75
  • 80
  • 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). – jsears Mar 25 '16 at 16:06
  • 1
    [Paul Tomblin provided the Bash Here Document](http://stackoverflow.com/a/4412338/608639) answer 6 years earlier. What value is added by this answer? – jww Oct 25 '16 at 08:21
  • `-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. – sjas Oct 25 '16 at 09:44
8

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

ssh user@host "cmd1; cmd2; cmd3"
DimiDak
  • 2,990
  • 1
  • 16
  • 23
6

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.)

Aconcagua
  • 19,952
  • 4
  • 31
  • 51
  • 1
    What's even better you can use && and || this way, too - echo test \&\& ls – Miro Kropacek May 14 '19 at 17:51
  • @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... – Aconcagua May 14 '19 at 19:24
  • @MiroKropacek, I didn't have to escape the && when putting double quotes around the commands: `ssh host "echo test && ls"` – not2savvy May 08 '20 at 15:50
6

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"
  • 3
    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. – Charles Addis Oct 31 '17 at 16:24
4

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
    ControlMaster auto
    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.

Jonathan
  • 3,807
  • 3
  • 27
  • 34
0

For simple commands you can use:

ssh <ssh_args> command1 '&&' command2

or

ssh <ssh_args> command1 \&\& command2
Soufiane Sakhi
  • 591
  • 6
  • 11