expect 知识与示例说明
2012/04/10 chenxin
2019/07/07 update Chenxin
参考
https://www.cnblogs.com/yinghao1991/p/6926125.html
https://www.jellythink.com/archives/373
https://www.cnblogs.com/chengjian-physique/p/8254381.html
https://core.tcl-lang.org/expect/index
概念与基础知识
目的
利用expect,则可以根据程序的提示,模拟标准输入提供给程序,从而实现自动化交互执行.
指令
命令 作用
send 用于向进程发送字符串.如果要发送Ctrl-C结束进程,可以通过send "\003" 实现。send \001(发送ctrl+a) 然后 send "d" (发送d)合起来相当于发送ctrl+a +d.
expect 从进程接收字符串(等待一个进程的反馈,捕获后匹配)
spawn 启动新的进程(spawn后的send和expect命令都是和使用spawn打开的进程进行交互).
interact 进入用户交互(一般情况下使用spawn、send和expect就可以很好的完成任务;在特殊场合下需使用interact,用于退出自动化,进入人工交互。如我们用spawn、send和expect完成了ftp登陆下载文件任务,但是我们希望在文件下载结束后,仍可以停留在ftp命令行状态,以便手动的执行后续命令,此时就需要使用interact).
timeout expect中等待命令的输出信息是有一个 timeout的设定的,默认是10秒。这个特性是防止那些执行死机的命令的。一旦到了这个timeout,还是没有屏幕输出的话,expect脚本中下面的代码就会执行 。或者我们在expect脚本中如果定义了timeout的响应代码的话,这些代码就会被执行。
示例说明
示例一
!/usr/tcl/bin/expect # 使用expect来解释该脚本;
set timeout 30 # 设置超时时间,单位为秒,默认情况下是10秒;
set host "101.200.241.109" # 设置变量;
set username "root"
set password "123456"
spawn ssh $username@$host # spawn是进入expect环境后才可以执行的expect内部命令.它主要的功能是给ssh运行进程加个壳,用来传递交互指令;
expect "password" {send "$password\r"} # 这里的expect也是expect的一个内部命令.判断上次输出结果里是否包含“password”的字符串,如果有则立即返回;否则就等待一段时间后返回(上面设置的30s);send "$password\r":当匹配到对应的输出结果时,就发送密码到打开的ssh进程,执行交互动作;这就是expect的 "模式-动作"
interact # 执行完成后保持交互状态,把控制权交给控制台,这个时候就可以手工操作了。如果没有这一句登录完成后会退出,而不是留在远程终端上。
示例二
set timeout -1
spawn ftp ftp.test.com #打开新的进程,该进程用户连接远程ftp服务器
expect "Name" #进程返回Name时
send "user\r" #向进程输入user\r
expect "Password:"
send "123456\r"
expect "ftp> "
send "binary\r"
expect "ftp> " #进程返回ftp>时
send "get test.tar.gz\r" #向进程输入get test.tar.gz\r
模式-动作 与 exp_continue循环式匹配
结合着expect "password" {send "$password\r"}这句代码来说说“模式-动作”。这是一个单一分支模式匹配.简单的说就是匹配到一个模式,就执行对应的动作;匹配到password字符串,就输入密码。
单一分支模式语法:
expect "hi" {send "You said hi"} # 匹配到hi后,会输出"you said hi"
多分支模式语法:
expect "hi" { send "You said hi\n" }"hello" { send "Hello yourself\n" }"bye" { send "That was unexpected\n" }
匹配到hi,hello,bye任意一个字符串时,执行相应的输出。等同于如下写法:
expect {
"hi" { send "You said hi\n"}
"hello" { send "Hello yourself\n"}
"bye" { send "That was unexpected\n"}
}
你可能也会看到这样的代码:
expect {
"password" {
send "$password\r";exp_continue # 其中exp_continue表示循环式匹配
}
eof # 一旦接收到标识子进程已经结束的eof字符,expect脚本也就退出结束
{
send "eof"
}
}
通常匹配之后都会退出语句,但如果有exp_continue则可以不断循环匹配,输入多条命令,简化写法。
关于 exp_continue 的说明
exp_continue 附加于某个 expect 判断项之后,可以使该项被匹配后,还能继续匹配该 expect 判断语句内的其他项。exp_continue 类似于控制语句中的 continue 语句。
例如:下例将判断交互输出中是否存在 yes/no 或 assword。如果匹配 yes/no 则输出 yes 并再次执行判断;如果匹配 assword 则输出 123abc 并结束该段 expect 语句。
expect {
"yes/no" {send "yes\r"; exp_continue;}
"*assword" {set timeout 300; send "123abc\r";}
}
注意:exp_continue允许expect继续执行自身而不是往下执行.默认情况下,exp_continue会重置timeout,如果不想重置timeout,使用-continue_timer选项。
expect eof 说明
expect {
"password" {
send "$password\r"
exp_continue # 其中exp_continue表示循环式匹配
}
eof # 一旦接收到标识子进程已经结束的eof字符,expect脚本也就退出结束
{
send "eof"
}
}
interact: 执行完成后保持交互状态,把控制权交给控制台(可以手工操作了)。若没有这句,登录完成后就会退出,而不是留在远程终端上。如果你只是登录过去执行一段命令就退出,可改为[expect eof]
expect eof: 传输一个大文件,脚本最后通过expect eof由于expect eof的超时时间很短,默认10秒,因此很可能导致文件传输不完整,解决方法是:将expect eof改成 expect -timeout -1 eof
expect puts 用法
puts输出到控制台,或输出到文件.
在shell中嵌套的时候,用puts的话,要注意变量被shell自动展开的问题.调用的时候应该采用以下方式:
!/bin/bash
...
puts "i is $i" # 要""括起来,shell中嵌套的时候,不应该直接puts $i
...
传参(命令行里的参数传递到expect脚本内部)
很多时候,我们需要传递参数到脚本中,现在通过下面这段代码来看看如何在expect中使用参数:
!/usr/tcl/bin/expect
if {$argc < 3} { # $argc表示参数个数
puts "Usage:cmd "
exit 1
}
set timeout -1
set host [lindex $argv 0] # 第一个参数
set username [lindex $argv 1]
set password [lindex $argv 2]
spawn ssh $username@$host
expect "password" {send "$password\r"}
interact
在expect中,$argc表示参数个数,而参数值存放在$argv中,比如取第一个参数就是[lindex $argv 0],以此类推。
expect内部变量<$expect_out()>
<$expect_out(buffer)>存储了所有对expect的输入(捕获到的所有内容).
<$expect_out(0,string)>存储了匹配到expect参数的输入(能够匹配到的内容).
比如如下程序:
expect "hi\n"
send "you typed <$expect_out(buffer)>"
send "but I only expected <$expect_out(0,string)>"
当在标准输入中输入
test
hi
时,运行结果如下
you typed: test
hi
I only expect: hi
shell中嵌套expect 以及 bash与expect 彼此间变量传递说明
2019/07/07 update Chenxin
cat test.sh
!/bin/bash
backup_aws="3.1.250.30";
passwd="cx88...adm!@#"
expect <<EOF
set timeout 5
spawn ssh admin@$backup_aws -p 4399
expect {
"refused" {exit 1}
"unreachable" {exit 2}
"No route" {exit 3}
"yes/no" {send "yes\n";exp_continue}
"password:" { send "$passwd\r";sleep 1 }
"]$" {exit 0}
"]#" {exit 0}
"@" {exit 0}
"timeout" {exit 4}
eof {exit 5}
}
expect "]$" {send "cd /home/admin/tmp\r";}
#expect "]$" {send "mkdir test_dir\r"; }
expect "]$" {send "cd ../\r";}
send "echo hello\r";send "echo OK\r"; # dont expect, send immediately
expect "]$" {send "tail -n 2 uwsgi8000.log\r";sleep 1} # The info of out maybe need some secends for display.
expect "]$*" {send "cd /opt;ls\r";sleep 1}
interact
EOF
注意,即使本地执行,有时候也需要多尝试sleep 1,等待1秒的情况,否则输出还没来得及看到,就结束了.
bash 与 expect 互相使用彼此变量
如果是在shell中启动了expect脚本,现在想在expect中使用shell里的变量,可以有两种方法:
1.首先在shell中进行变量export, 例如export a=1, 然后在expect中通过 $::env(a) 引用,例如set a_exp $::env(a)
2.也可以通过执行子shell调用,例如: set a [exec sh -c {echo $a}]
如果是在expect里面执行shell,并且想在shell里面使用expect的变量,需要在expect里面设置环境变量:
例如:set ::env(LAB) my_lab
那么在shell语句里面可以通过$LAB引用。
附一个完整的主机认证脚本
SSH主机自动认证expect
20121119 Chenxin
!/bin/bash
function renzheng () {
backup_aws="$1";
expect <<EOF
set timeout 5
spawn ssh -i /home/rsyncfiles/.ssh/id_dsa rsyncfiles@$backup_aws -p 4399 echo
expect {
"refused" {exit 1}
"unreachable" {exit 2}
"No route" {exit 3}
"yes/no" {send "yes\n";exp_continue}
"]$" {exit 0}
"]#" {exit 0}
"@" {exit 0}
"timeout" {exit 4}
eof {exit 5}
}
EOF
}
expect命令行指令使用方式
https://www.cnblogs.com/yinghao1991/p/6926125.html
expect命令语法
expect [选项] [ -c cmds] [ [ -[f|b] ] cmdfile] [ args]
可以参考本笔记后面的一些示例脚本的用法.
数字天空游戏服上一些脚本
redis 的启动与停止
在游戏服启动之前,需要先启动 redis服务,如果是关停,则先停止游戏服再停止 redis服;
redis的每次关停后,都会执行备份物理文件操作,放置到 /home/lzadmin/redis_data_bak/下,并清除45天前的备份文件;
cat /usr/local/bin/lzll01_redis_server_manage.exp
!/bin/bash
manage lzll redis server,include the gs,redis cmd,gs log...,except the db script;
By Chenxin 20120727
引入系统环境变量
if [ -f /etc/init.d/functions ];then
. /etc/init.d/functions;
fi
curr_date=date +%Y%m%d%H%M
export TERM=vt100
cd /home/sftproot/home/;
find /home/sftproot/home/lzupdate/ -maxdepth 2 -name redis_*_* |awk -F "/" '{print $7}'|grep -v ".sh">/home/lzadmin/redis_server_list.tmp
cd /home/lzadmin/;
start the redis server;
function start_redis_server () {
cd /home/lzadmin/;
for i in cat redis_server_list.tmp
do {
cd /home/sftproot/home/lzupdate/redis_servers/$i
sudo /home/sftproot/home/lzupdate/redis_servers/$i.sh /home/sftproot/home/lzupdate/redis_servers/$i/conf/redis.conf
} done
}
stop the redis server;
function stop_redis_server () {
cd /home/lzadmin/;
for i in cat redis_server_list.tmp
do {
cd /home/sftproot/home/;
redis_pass=cat /home/sftproot/home/lzupdate/redis_servers/$i/conf/redis.conf |grep requirepass|awk '{print $2}'
redis_port=cat /home/sftproot/home/lzupdate/redis_servers/$i/conf/redis.conf |grep port|awk '{print $2}'
expect -c "
set timeout 20
eval spawn screen -S $i
sleep 2
send "/usr/local/redis-2.4.10/src/redis-cli -p $redis_port\r"
expect ">";send "auth $redis_pass\r";
expect "OK";send "save\r";
expect "OK";send "shutdown\r";
expect ">";send "exit\r";
sleep 2
send "\004"
sleep 2
send "\004"
expect eof"
# 备份物理文件
mkdir -p /home/lzadmin/redis_data_bak # 每次停服都备份一次 redis物理数据文件到lzadmin目录下
cp -aprf /home/sftproot/home/lzupdate/redis_servers/$i/data/dump.rdb /home/lzadmin/redis_data_bak/dump_$curr_date.rdb
find /home/lzadmin/redis_data_bak/ -mtime +45 -name ".rdb" -exec rm -rf {} ;
} done
}
case $1 in
start)
start_redis_server;;
stop)
stop_redis_server;;
restart)
stop_redis_server;
start_redis_server;;
)
echo "(Arguments shuld be start|stop|restart,OK?!)" ;;
esac
游戏服务的启动与关停
cat /usr/local/bin/lzll02_game_server_manage.exp
!/bin/bash
...
cd /home/lzadmin/;
function start_game_and_log_server () {
cd /home/lzadmin/;
#start the game server;
for i in cat game_server_list.tmp
do {
expect -c "
set timeout 60
eval spawn screen -S $i
sleep 2
send "cd /home/sftproot/home/lzupdate/game_servers/$i/\r"
send "sudo /home/sftproot/home/lzupdate/game_servers/$i/GameServer.sh\r"
expect "cmd>"
sleep 2
send "\001"
send "d"
expect eof"
} done
#start the game server log to file;
for i in cat game_server_list.tmp
do {
expect -c "
set timeout 60
eval spawn screen -S ws_$i
sleep 2
send "cd /home/sftproot/home/lzupdate/game_servers/$i/\r"
send "sudo /home/sftproot/home/lzupdate/game_servers/$i/WriteLogServer.sh\r"
expect "cmd>"
sleep 2
send "\001"
send "d"
expect eof"
} done
}
function stop_game_and_log_server () {
cd /home/lzadmin/;
#stop the game server;
for i in cat game_server_list.tmp
do {
expect -c "
set timeout 60
eval spawn screen -r $i
send "stop\r"
expect "application will exit"
sleep 10
send "\004"
expect eof"
} done
#stop the game log service;
for i in cat game_server_list.tmp
do {
expect -c "
set timeout 60
eval spawn screen -r ws_$i
send "stop\r"
expect "TaskLog finished" # 捕获到该字符后 ,需要等待10s ,否则如果直接发送 ctrl+d的话,会造成故障;
sleep 10
send "\004"
expect eof"
} done
}
case $1 in
start)
start_game_and_log_server;;
stop)
stop_game_and_log_server;;
restart)
stop_game_and_log_server;
start_game_and_log_server;;
*)
echo "(Arguments shuld be start|stop|restart,OK?!)" ;;
esac
批量获取服务器序列号
2017/04/20 Chenxin
!/bin/bash
passwd_admin=yRtMHiNc0A5JNnz#
passwd_root=Ywsz098xylz!@#
for i in cat ip.txt
do {
expect << EOF
set timeout 1
spawn ssh -p 4399 admin@$i
expect {
"yes/no" { send "yes\r"; exp_continue }
"password:" { send $passwd_admin\r;sleep 1;exp_continue }
"密码" { send $passwd_admin\r;sleep 1;exp_continue }
"口令" { send $passwd_admin\r;sleep 1;exp_continue }
"admin@" {
send "su -\r";sleep 1;
expect {
"password:" { send $passwd_root\r;sleep 1;exp_continue }
"密码:" { send $passwd_root\r;sleep 1;exp_continue}
"口令" { send $passwd_root\r;sleep 1;exp_continue}
"root@" { send "echo -n $i && dmidecode -t 1|grep 'Serial Number'\r" }
}
}
}
expect eof
EOF
} done