我有一个脚本,我需要启动一个命令,然后将一些额外的命令作为命令传递给该命令。我试过了
su
echo I should be root now:
who am I
exit
echo done.
...但它不起作用:su
成功,但命令提示符只是盯着我看。如果我在提示符下键入exit
,echo
和who 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
我该如何解决这个问题?
我正在寻找能够以一般方式解决这个问题的答案,而这些答案并非特定于
su
或ssh
。这个问题的目的是成为这个特定模式的canonical。
答案 0 :(得分:17)
shell脚本是一系列命令。 shell将读取脚本文件,并依次执行这些命令。
在通常的情况下,这里没有意外;但是一个常见的初学者错误是假设某些命令将从shell接管,并开始在脚本文件中执行以下命令,而不是当前运行此脚本的shell。但那不是它的工作方式。
基本上,脚本像交互式命令一样完全,但是完全的工作方式需要正确理解。交互式地,shell读取命令(来自标准输入),运行该命令(使用来自标准输入的输入),并且当它完成时,它读取另一个命令(来自标准输入)。
现在,在执行脚本时,标准输入仍然是终端(除非您使用了重定向),但是命令是从脚本文件中读取的,而不是从标准输入中读取的。 (相反的情况确实非常麻烦 - 任何read
都会占用脚本的下一行,cat
会淹没脚本的所有其余部分,并且无法与它进行交互!)脚本文件 only 包含执行它的shell实例的命令(尽管你当然可以使用here文档等将输入作为命令参数嵌入)。
换句话说,这些"误解了"命令(su
,ssh
,sh
,sudo
,bash
等)单独运行(不带参数)将启动交互式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
对于接受单个命令参数的命令,该命令可以是sh
或bash
,具有多个命令:
sudo sh -c 'uname -a; who am i; uptime'
顺便说一句,你通常不需要一个明确的exit
,因为该命令在执行你传入执行的脚本(命令序列)时会终止。
答案 1 :(得分:15)
重要的是要记住,格式化为另一个shell的here-document的脚本部分是在具有自己环境的不同shell中执行的(甚至可能在不同的机器上)。
如果您的脚本块包含参数扩展,命令替换和/或算术扩展,那么您必须使用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
然后,此处文档的分隔符必须引用。
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
然后,此处文档的分隔符必须不带引号,并且您必须转义必须在子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
答案 2 :(得分:8)
如果您想要一个适用于任何类型程序的通用解决方案,您可以使用expect
命令。
摘自手册页:
Expect
是一个&#34;会谈&#34;根据脚本到其他交互式程序。 在脚本之后,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
之类的程序,任何可能从stdin获取信息的函数/程序调用都会使下一个命令失败:
ssh use@host <<'____HERE'
echo "Enter your name:"
read name
echo "ok."
____HERE
--> The `echo "ok."` string will be passed to the "read" command