将命令作为输入传递给另一个命令(su、ssh、sh等)

28

我有一个脚本,需要启动一个命令,然后将一些额外的命令 作为命令 传递给该命令。我尝试过。

我有一个脚本,需要启动一个命令,然后将一些额外的命令 作为命令 传递给该命令。我尝试过。

su
echo I should be root now:
who am I
exit
echo done.

......但它不起作用: su 成功了,但是命令提示符只是盯着我。如果我在提示符处输入exitechowho am i等开始执行!而echo done.根本没有被执行。

同样,我需要让它在ssh上工作:

ssh remotehost
# this should run under my account on remotehost
su
## this should run as root on remotehost
whoami
exit
## back
exit
# back

我该如何解决这个问题?

我正在寻找一般性的答案,而不是特定于sussh的答案。目的是让这个问题成为这个特定模式的规范


2
相关:https://dev59.com/fWMm5IYBdhLWcg3wO9Ly - tripleee
为什么不用你想要以root身份运行的命令编写一个shell脚本呢?这样,你只需要运行sudo sh yourshellscript.sh即可。 - Jose Serodio
@JoseSerodio 这在 ssh 场景下是行不通的,例如(或者你必须先将脚本 scp 到远程主机),并且会使得一些简单的情况变得非常复杂(然后你必须管理两个脚本文件,并确保调用脚本知道另一个脚本文件的路径)。当然,它也不能轻松地扩展到某些命令是动态的情况下(例如,取决于调用脚本中执行的计算)。 - tripleee
3个回答

25

补充tripleee答案

需要记住,格式化为另一个shell的here-document脚本部分将在不同的shell中执行,并且具有自己的环境(甚至可能在不同的机器上执行)。

如果您的脚本块包含参数扩展、命令替换和/或算术扩展,则必须稍微不同地使用shell的here-document功能,具体取决于您想要执行这些扩展的位置。

1. 所有扩展都必须在父shell的范围内执行。

然后here-document的定界符必须不加引号

command <<DELIMITER
...
DELIMITER

例子:

#!/bin/bash

a=0
mylogin=$(whoami)
sudo sh <<END
    a=1
    mylogin=$(whoami)
    echo a=$a
    echo mylogin=$mylogin
END
echo a=$a
echo mylogin=$mylogin

输出:

a=0
mylogin=leon
a=0
mylogin=leon

2. 所有扩展都必须在子Shell的范围内执行。

然后,这里文档的定界符必须引用

command <<'DELIMITER'
...
DELIMITER

例子:

#!/bin/bash

a=0
mylogin=$(whoami)
sudo sh <<'END'
    a=1
    mylogin=$(whoami)
    echo a=$a
    echo mylogin=$mylogin
END
echo a=$a
echo mylogin=$mylogin

输出:

a=1
mylogin=root
a=0
mylogin=leon

3. 一些扩展必须在子shell中执行,一些必须在父shell中执行。

然后,这里文档的定界符必须是未引用的,并且您必须转义那些需要在子shell中执行的扩展表达式

例:

#!/bin/bash

a=0
mylogin=$(whoami)
sudo sh <<END
    a=1
    mylogin=\$(whoami)
    echo a=$a
    echo mylogin=\$mylogin
END
echo a=$a
echo mylogin=$mylogin

输出:

a=0
mylogin=root
a=0
mylogin=leon

这对于ssh特别麻烦,因为您必须考虑到远程和本地shell中的引用和转义。因此,例如,您需要使用 ssh remote 'echo "\\"'ssh remote "echo '\'"ssh remote echo \\\\来正确打印远程主机上的反斜杠。 - tripleee

22

一个 shell 脚本是一系列命令。shell 会读取脚本文件,并依次执行这些命令。

通常情况下,这里没有什么意外;但初学者常见的错误是认为有些命令会接管 shell,并开始执行脚本文件中的后续命令,而不是当前正在运行此脚本的 shell。但实际上并不是这样的。

基本上,脚本的工作方式与交互式命令完全相同,但需要正确理解它们的工作方式。在交互模式下,shell 会读取命令(从标准输入),运行该命令(使用标准输入的输入),当完成时,它会再次读取另一个命令(从标准输入)。

现在,在执行脚本时,标准输入仍然是终端(除非您使用重定向),但是命令是从脚本文件中读取的,而不是从标准输入中读取的。(反过来将非常麻烦——任何read都会消耗脚本的下一行,cat会读取剩余的整个脚本,而且无法与其交互!)脚本文件包含用于执行它的 shell 实例的命令(尽管您当然仍然可以使用 here document 等将输入嵌入为命令参数)。

换句话说,这些“误解”的命令(susshshsudobash等),在单独运行(没有参数)时,会启动一个交互式 shell,在交互会话中,这显然是可以的;但在从脚本中运行时,往往不是想要的结果。

所有这些命令都有接受命令的其他方式,而不是在交互式终端会话中。通常,每个命令支持一种将命令作为选项或参数传递的方式:

su root -c 'who am i'
ssh user@remote uname -a
sh -c 'who am i; echo success'
许多这些命令也可以接受标准输入上的命令:

许多这些命令也可以从标准输入接收命令:

printf 'uname -a; who am i; uptime' | su
printf 'uname -a; who am i; uptime' | ssh user@remote
printf 'uname -a; who am i; uptime' | sh

同时方便地允许您使用此类文件:

ssh user@remote <<'____HERE'
    uname -a
    who am i
    uptime
____HERE

sh <<'____HERE'
    uname -a
    who am i
    uptime
____HERE

对于只接受单个命令参数的命令,该命令可以是具有多个命令的shbash命令:

sudo sh -c 'uname -a; who am i; uptime'
作为旁注,通常不需要显式使用 exit 命令,因为当执行你传递给它的脚本(命令序列)后,命令会自动终止。

“a | ssh ..@...” 只是将标准输入传递到服务器端的命令,因此需要使用 “a | ssh ...@... 'sh'”。 - Andreas Louv
2
@andlrc 不需要。在没有显式命令的情况下,ssh会运行一个shell。 - tripleee
1
虽然那将是用户的登录 shell,但这在现今很少是 sh - tripleee
script 命令不支持将命令作为命令行参数进行传递,尽管 Linux 的 script 命令提供了 script -c 'your command here' 的方式。 - tripleee

9
如果您想使用通用解决方案来处理任何类型的程序,您可以使用expect命令。
从手册中摘录:
“Expect是一种根据脚本与其他交互式程序对话的程序。按照脚本,Expect知道可以从程序中期望什么以及正确的响应应该是什么。解释性语言提供了分支和高级控制结构,以指导对话。此外,用户可以在需要时直接控制并进行交互,然后将控件返回给脚本。”
以下是使用expect的工作示例:
set timeout 60

spawn sudo su -

expect "*?assword" { send "*secretpassword*\r" }
send_user "I should be root now:"

expect "#" { send "whoami\r" }
expect "#" { send "exit\r" }
send_user "Done.\n"
exit

脚本可以通过简单的命令启动:
$ expect -f custom.script

您可以在以下页面查看完整示例:http://www.journaldev.com/1405/expect-script-example-for-ssh-and-su-login-and-running-commands 注意:@tripleee提出的答案只适用于标准输入能够在命令开始时被读取一次,或者如果已经分配了tty,并且对于任何交互式程序都不起作用。
如果使用管道,将会出现错误示例。
echo "su whoami" |ssh remotehost
--> su: must be run from a terminal

echo "sudo whoami" |ssh remotehost
--> sudo: no tty present and no askpass program specified

在SSH中,当你使用多个-t参数强制分配TTY时,但是当sudo要求密码时,它会失败。
如果没有像expect这样的程序,任何从标准输入获取信息的函数/程序的调用都会使下一个命令失败。
ssh use@host <<'____HERE'
  echo "Enter your name:"
  read name
  echo "ok."
____HERE
--> The `echo "ok."` string will be passed to the "read" command

1
“expect”是一个有用的工具,与此话题有关,但作为简单shell语法问题的答案,它几乎肯定是过度杀伤力了。在我20多年的shell编程经验中,我很少需要使用它(而像Perl、Python、Ruby等好的通用脚本语言的可用性使得学习它变得不那么必要)。 - tripleee
2
你的问题很简单,但解决方案并不像你想象的那样通用。你提出的解决方案没有考虑到不能同时将所有内容推送到 <stdin> 的情况。但我同意你的观点,大多数情况下我们并不希望使用 expect - Adam

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