expect 是一个自动对话命令,可以编程实现与交互式命令的对话交互。
交互式命令
想要学习 expect 命令,掌握它的典型应用场景,我们需要先弄清楚:什么是交互式命令?
Linux 系统中,绝大部分命令都是非交互式的常规命令。非交互式的命令一旦提交执行,它便默默地干活,执行完毕后输出结果。举个例子,我们执行 uname 命令,回车按下后不用再做任何输入:
uname 命令用于查看操作系统信息,例子输出表明这是一个 Linux 系统。
交互式命令,顾名思义在命令执行期间,可能需要跟用户进行交互。举个例子,我们执行 apt 命令安装软件包,该命令询问是否继续,用户要输入 Y 之后它才会继续安装:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
$ sudo apt install g++
Reading package lists... Done
Building dependency tree
Reading state information... Done
The following additional packages will be installed:
cpp-9 g++-9 gcc-9 gcc-9-base libasan5 libgcc-9-dev libstdc++-9-dev
Suggested packages:
gcc-9-locales g++-multilib g++-9-multilib gcc-9-doc gcc-9-multilib libstdc++-9-doc
The following NEW packages will be installed:
g++ g++-9 libstdc++-9-dev
The following packages will be upgraded:
cpp-9 gcc-9 gcc-9-base libasan5 libgcc-9-dev
5 upgraded, 3 newly installed, 0 to remove and 320 not upgraded.
Need to get 31.0 MB of archives.
After this operation, 60.5 MB of additional disk space will be used.
Do you want to continue? [Y/n]
|
自动对话
如果在脚本里面执行交互式命令,执行过程中需要用户手工交互,那就太麻烦了。那么,有办法让脚本自动输入 Y 吗?当然有啦!这就是 expect 干的活——自动跟交互式命令对话!
那么,如何让 expect 执行 apt 命令,并自动给它输入 Y 呢?我们可以这样写:
1
2
3
4
5
6
7
8
9
10
11
|
expect << EOF
spawn apt install g++
expect {
"Y/n]" { send "Y\n" }
}
expect eof
wait
EOF
|
spawn 表示创建一个新进程,来执行 apt 命令; expect 顾名思义是期待 apt 命令输出 Y/n] 字眼,这时向它输入 Y 和回车( \n );expect eof 表示期待 apt 命令结束输出(退出时会关闭标准输出);wait 则等待 apt 进程退出。
将这个命令拷贝贴到 shell 中执行,这时我们就不用手工输入 Y 了。
apt 命令有一个 -y 选项,加上后就不用询问,默认 Yes 。本文为了演示 expect 用法,故意不用这个选项。
expect脚本
上面例子,expect 命令从标准输入读取用户指令,比如 spawn 、 expect 、send 和 wait 等等。我们也可以将指令写在一个文件里,比如 install-g++.exp :
1
2
3
4
5
6
7
8
|
spawn apt install g++
expect {
"Y/n]" { send "Y\n" }
}
expect eof
wait
|
然后再调用 expect 命令执行该文件:
我们还可以给 expect 脚本写一个注释头,这样加上执行权限后就可以直接运行了:
1
2
3
4
5
6
7
8
9
10
|
#!/usr/bin/expect
spawn apt install g++
expect {
"Y/n]" { send "Y\n" }
}
expect eof
wait
|
执行以下命令给脚本加上运行权限并执行:
1
2
3
4
5
|
# 为脚本加上执行权限
chmod +x install-g++.exp
# 执行脚本
./install-g++.exp
|
执行脚本时,shell 会分析注释头行,由此找到脚本解析器,并最终执行:
1
|
/usr/bin/expect install-g++.exp
|
高级用法
循环
expect 指令也支持执行循环,但语法比 shell 循环要弱一些,但我们可以用 shell 语法来驱动 expect 。举个例子,将当前目录中的文件通过 sftp 上传:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
sftp_ip="127.0.0.1"
sftp_port="10022"
sftp_user="foo"
sftp_password="pass"
local_dir="/Users/fasion/data"
remote_dir="./upload"
for file in `ls ${local_dir}`; do
/usr/bin/expect << EOF
spawn sftp -oPort=${sftp_port} ${sftp_user}@${sftp_ip}
expect {
"*password:" { send "${sftp_password}\n" }
}
expect "sftp>"
send "lcd ${local_dir}\n"
expect "sftp>"
send "cd ${remote_dir}\n"
expect "sftp>"
send "put abc.txt\r"
expect "sftp>"
send "bye \n"
EOF
done
|
这个例子利用 for-in 遍历当前目录内的每个文件,执行 expect 命令进行提交,代码非常清晰。美中也有不足,每上传一个文件都要重新执行 sftp 命令,然后登录。
那么,有办法只执行一次 sftp 命令,实现多文件上传吗?
我们注意到,上传一个文件需要两条 expect 指令:
1
2
|
expect "sftp>"
send "put abc.txt\r"
|
那么,我们可以用 shell 先为每个文件都生成两条这样的指令,这不难办:
1
|
ls ${local_dir} | awk '{ print "expect \"sftp>\""; print "send \"put " $0 "\n\"" }'
|
这个行命令利用 awk ,为每个文件输出两行指令。假设目录中有 abc.txt 和 cba.txt 两个文件,执行效果如下:
1
2
3
4
|
expect "sftp>"
send "put abc.txt\n"
expect "sftp>"
send "put cba.txt\n"
|
最后,将这个命令的输出,嵌入到 expect 指令中即可:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
|
sftp_ip="127.0.0.1"
sftp_port="10022"
sftp_user="foo"
sftp_password="pass"
local_dir="/Users/fasion/data"
remote_dir="./upload"
puts=`ls ${local_dir} | awk '{ print "expect \"sftp>\""; print "send \"put " $0 "\n\"" }'`
/usr/bin/expect << EOF
spawn sftp -oPort=${sftp_port} ${sftp_user}@${sftp_ip}
expect {
"*password:" { send "${sftp_password}\n" }
}
expect "sftp>"
send "lcd ${local_dir}\n"
expect "sftp>"
send "cd ${remote_dir}\n"
expect "sftp>"
send "ls\n"
${puts}
expect "sftp>"
send "bye \n"
EOF
if [ $? -eq 0 ]
then
echo "文件传输成功"
else
echo "文件传输失败!"
fi
#退出sh脚本
exit 0
|
- 第 8 行,为每个文件生成上传指令,保存到 puts 变量中;
- 第 26 行,嵌入生成的文件上传指令;
订阅更新,获取更多学习资料,请关注我们的公众号: