什么是在Bash中进行ssh并运行多个命令的最干净的方法?

451

我已经设置了ssh代理,并且可以在Bash脚本中运行一些命令来完成在外部服务器上执行操作的任务,例如:

ssh blah_server "ls; pwd;"

现在,我真正想做的是在一个外部服务器上运行许多长命令。把所有这些命令用引号括起来会非常丑陋,我真的宁愿避免多次 ssh 连接以避免这种情况。

那么,有没有一种方法可以在一步中使用括号或类似的东西完成这个任务?我正在寻找以下内容:

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

基本上,只要解决方案简洁,我就会满意。

编辑

为了澄清,我是在谈论这是一个更大的bash脚本的一部分。其他人可能需要在未来处理该脚本,因此我希望保持它的简洁性。我不想让bash脚本看起来像这样一行:

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

因为它非常丑陋而且难以阅读。


2
嗯,把所有的东西都放到服务器上的一个脚本里,然后只用一个 ssh 调用它怎么样? - Nikolai Fetissov
1
@Nikolai 如果命令依赖于客户端,可以将其编写成一个shell脚本,然后通过scpssh进行传输和执行。我认为这是最清晰的方式。 - khachik
7
这是一个更大的bash脚本的一部分,所以我不想将其分成两半,一半放在我的个人电脑上,另一半放在服务器上并通过ssh运行。如果可能的话,我真的很想只保留一个脚本从我的个人电脑运行。是否真的没有干净的方法来将一堆命令封装在ssh中? - Eli
最好的方法不是使用bash,而是使用Perl、Python、Ruby等。 - salva
2
为什么要避免在引号中放置远程命令?您可以在引号内拥有多行文本,且使用字符串而不是标准输入意味着标准输入可用于读取远程脚本的输入。 (尽管在Unix上,通常建议使用单引号而不是双引号,除非您特别需要本地Shell评估字符串的某些部分。) - tripleee
@khachik 请参考另一个答案,了解如何在服务器上运行客户端脚本。例如:'ssh <remote-user>@<remote-host> "bash -s" <./remote-commands.sh -- arg1 arg2'。(这不是回答此问题的答案,该问题询问如何使用内联命令完成它) - gaoithe
14个回答

579

可以考虑使用Bash的Here Document

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

为了避免@Globalz在评论中提到的问题,你可以根据远程网站上的具体情况尝试用以下代码替换第一行:

ssh otherhost /bin/bash << EOF

注意,您可以在此处文档中执行变量替换,但可能需要处理引号问题。例如,如果将“限制字符串”(即上面的EOF)加上引号,则无法进行变量替换。但是,不用引号括起限制字符串,则会进行变量替换。例如,如果您在shell脚本中定义了$NAME,则可以执行以下操作:

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

它将在目标otherhost上创建一个文件,该文件名与您分配给$NAME的名称相同。其他关于Shell脚本引号的规则也适用,但这里太复杂了。


6
看起来这正是我想要的!它怎么工作?你有没有一个解释它的页面链接? - Eli
9
我刚想到这个,这里有一个阅读相关内容的地方:http://tldp.org/LDP/abs/html/here-docs.html。 - bosmacs
18
我也在我的本地看到了这个输出:伪终端将不会被分配,因为标准输入不是终端。 - Globalz
17
为了防止命令行的扩展,引用“word”(即第一个“EOF”)可能很重要。请参见: man bash | less +/'Here Documents' - assem
7
保罗,我之所以建议这样做,是因为这个问题已经有将近20万的浏览量,看起来有很多人在使用ssh进行脚本编写。在脚本编写过程中通常需要注入值。如果不考虑其他问题和评论中的噪音,我只需要写一个并离开,但是现在很难被人看到。一行脚注可能会帮助人们避免一些麻烦。 - koyae
显示剩余18条评论

151

在本地编辑脚本,然后通过ssh管道输送它,例如:

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

其中commands-to-execute-remotely.sh看起来像你上面列出的列表:

ls some_folder
./someaction.sh
pwd;

8
这样做的巨大优势在于,您可以确切地知道远程脚本执行的内容 - 不会出现引号问题。如果需要动态命令,则可以使用带有子shell的shell脚本,仍然将其管道传递到ssh,即 ( echo $mycmd $myvar ; ...) | ssh myhost - 与cat用法一样,您知道正在进入ssh命令流的内容。当然,脚本中的子shell可以为了可读性而多行 - 参见http://www.linuxjournal.com/content/bash-sub-shells - RichVel
8
你能否将这个参数加入到commands-to-execute-remotely.sh脚本中?请确保翻译过后的内容通俗易懂,但不改变原文意思,并且不要添加解释。 - elaRosca
1
我认为这比paultomblin的解决方案更有用。因为脚本可以被重定向到任何ssh服务器,而无需打开文件。此外,它更易读。 - Patrick Bassut
4
是的,但使用回声或文档(请参见顶部答案):使用$localvar解释本地定义的变量,使用\$remotevar在远程解释远程定义的变量,使用\$(something with optionnal args)获取在远程服务器上执行的某些内容的输出。以下是一个可以通过1个ssh(或者如此示例所示,多个ssh命令)进行ssh的示例: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
7
这与使用输入重定向有什么不同呢?也就是说,ssh blah_server < commands-to-execute-remotely.sh - flow2k
不同的方法,但在实践中它们做相同的事情。 - parttimeturtle

53

为了匹配您的示例代码,您可以将命令放在单引号或双引号中。例如:

ssh blah_server "
  ls
  pwd
"

我喜欢这种格式,但遗憾的是它不能用于将std数据存储到变量中。 - Signus
1
Signus,你所说的“将std数据存储到变量中”是什么意思? - Andrei B
1
@Signus,你所描述的完全可以实现,不过你可能需要在远程命令周围使用单引号而不是双引号(或转义需要在双引号内转义的运算符,以防止本地 shell 拦截和插值)。 - tripleee
我无法将类似于fileToRemove=$(find . -type f -name 'xxx.war')的命令放入双引号中。fileToRemove应该包含一个文件名,但实际上它是一个空字符串。是否需要转义某些字符? - jkonst
@jkonst 是的,$ - Xbox One

46
我看到有两种方法:
第一种,你可以创建一个像这样的控制套接字:

```

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

并运行您的命令

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

您可以这样编写一个 SSH 命令,而无需重新连接到服务器。

第二种方法是动态生成脚本,使用scp并运行它。


28

这也可以按照以下方式完成。 将您的命令放入一个脚本中,让我们称之为commands-inc.sh

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

保存文件。

现在在远程服务器上运行它。

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

从来没让我失望过。


和我最初想的一样!但是为什么需要 bash -s - flow2k
4
"-s"选项是为了兼容性而存在。 来自bash的man文档:如果出现"-s"选项或在选项处理后没有剩余参数,则命令将从标准输入读取。该选项允许在调用交互式shell时设置位置参数。 - R J
1
R J,谢谢!根据我的第一条评论,看起来我们只需要执行 ssh user@remote < /path/to/commands-inc.sh (对我来说似乎可以工作)。好吧,我猜你的版本确保我们使用 bash shell,而不是其他 shell - 这就是目的,不是吗? - flow2k
2
谢谢。是的,我们中的一些人有我们的笔记和习惯来自旧版本的bash和其他shell。 :-) - R J
2
@RJ 不确定它与我的答案有何不同 :). 无论如何,干杯 - Jai Prakash
显示剩余3条评论

16
将所有命令放到一个脚本中,就可以像这样运行它:
ssh <remote-user>@<remote-host> "bash -s" <./remote-commands.sh

有点奇怪,但它运行得很好。并且参数可以传递到脚本中(在重定向之前或之后)。例如:'ssh <remote-user>@<remote-host> "bash -s" arg1 <./remote-commands.sh arg2' 例如:'ssh <remote-user>@<remote-host> "bash -s" -- -arg1 <./remote-commands.sh arg2' - gaoithe
1
我之前也有一个类似的问题,但是包含 bash -s 的目的是什么? - flow2k
1
@flow2k -s 是用来指示 bash 从标准输入读取命令的。以下是 man 页面中的描述: 如果存在 -s 选项,或者在选项处理后没有剩余参数,则命令将从标准输入读取。此选项允许在调用交互式 shell 时设置位置参数。 - Jai Prakash
@JaiPrakash 感谢您的回复,但我在想我们是否可以完全不使用 bash 命令,即 ssh remote-user@remote-host <./remote-commands.sh。我尝试了一下,似乎可以工作,但我不确定它是否存在某些缺陷。 - flow2k
根据我的理解,如果没有该选项,手册页中不会有任何缺陷。只有在尝试传递任何参数时才需要该选项。--谢谢 - Jai Prakash
你不能在remote-commands.sh脚本中使用来自原始脚本的标志变量和常量... :) - trainoasis

11

这对于创建脚本非常有效,因为您无需包含其他文件:

#!/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 ..

1
"$(which bash) -s" 部分提供了本地机器上bash的位置,而不是远程机器。 我认为你想要 '$(which bash) -s'(使用单引号来抑制本地参数替换). - jsears
1
Paul Tomblin在6年前提供了Bash Here Document的答案。这个答案添加了什么价值? - jww
--s标志,我引用他的话*你可能可以用替换第一行来解决问题...*,但我只是模糊地记得不能仅仅调用bash。 - sjas

11

不确定对于较长的命令是否最清晰,但肯定是最简单的方法:

ssh user@host "cmd1; cmd2; cmd3"

3
最好的是简单明了! - not2savvy
只要在远程命令中需要额外的引用,它就会崩溃。您需要传递到远程 shell 的任何美元符号或反引号都需要进行反斜杠转义,或者您需要进行额外的引用。单引号而不是双引号是更简单的引用难题的好解决方案,但在这种情况下,它往往只会将问题推向不同的角落,因为远程 shell 将吞掉一个转义级别。 - tripleee

8

SSH和Bash中运行多个命令。

在一个字符串中使用分号来分隔命令,并将其传递给echo,然后将所有命令连接到ssh命令。例如:

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

Arnab,将来请简要描述您的代码是做什么的。然后讲解它是如何工作的,再放入代码。最后将代码放入代码块中,以便易于阅读。 - Eric Leschinski
1
从问题中,您提供了确切地OP想要避免的内容:““我不想有一个看起来像...的一行bash脚本...” - jww

7

那些使用多行字符串和多个bash脚本的答案对我无效。

  • 长且多行的字符串难以维护。
  • 分离的bash脚本不能保持本地变量。

这里是一种功能性的方法,可以在保持本地上下文的同时ssh并运行多个命令。

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
你似乎认为只有在命令行下才会有人想要做这件事情。但实际上,还有其他常见的原因,比如使用类似rundeck、jenkins等工具在分布式环境中执行命令。请注意,在翻译过程中我尽量保持忠实于原文意思,并尽可能使语言通俗易懂。 - Charles D Pantoga
@KamilCuk的回答是一个(更好!)版本的这个,但更复杂和难以阅读和理解。 - tripleee

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接