用expect实现交互式命令自动对话

expect 是一个自动对话命令,可以编程实现与交互式命令的对话交互。

交互式命令

想要学习 expect 命令,掌握它的典型应用场景,我们需要先弄清楚:什么是交互式命令?

Linux 系统中,绝大部分命令都是非交互式的常规命令。非交互式的命令一旦提交执行,它便默默地干活,执行完毕后输出结果。举个例子,我们执行 uname 命令,回车按下后不用再做任何输入:

1
2
$ uname
Linux

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 命令从标准输入读取用户指令,比如 spawnexpectsendwait 等等。我们也可以将指令写在一个文件里,比如 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 命令执行该文件:

1
expect install-g++.exp

我们还可以给 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.txtcba.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 行,嵌入生成的文件上传指令;

订阅更新,获取更多学习资料,请关注我们的公众号:

【随笔】系列文章首发于公众号【小菜学编程】,敬请关注: