使用 expect 脚本实现自动化操作
expect 简介
expect 是一个自动化交互套件,主要应用于执行命令和程序时,系统以交互形式要求输入指定字符串,实现交互通信。
在实际工作中,我们运行命令、脚本或程序时,这些命令、脚本或程序都需要从终端输入某些继续运行的指令,而这些输入都需要人为的手工进行。而利用 expect,则可以根据程序的提示,模拟标准输入提供给程序,从而实现自动化交互执行。这就是 expect 。
expect 自动交互流程:
- spawn 启动指定进程
- expect 获取指定预期输出
- send 发送指定字符串
- 执行完成退出
我在日常中使用主要是在 Windows 上使用 Cygwin Terminal, Cygwin Terminal 上通过安装 expect 实现在 Windows 上执行一些自动化运维操作. 用于作为平替 Xshell 的一种简单方案.

expect 命令
spawn- 命令用来启动新的进程,spawn后的send和expect命令都是和使用spawn打开的进程进行交互。expect- 获取匹配信息,匹配成功则执行expect后面的程序动作。exp_continue- 在expect中多次匹配就需要用到。
send- 命令接收一个字符串参数,并将该参数发送到进程。send exp_send- 用于发送指定的字符串信息。
interact- 开启人工交互, 比如使用 expect 自动登陆后你想要执行一些手动操作可以设置 exceptsend_user- 用来打印输出, 但不会向进程中输入 相当于 shell 中的 echo[lindex $argv 0]获取expect脚本的第1个参数set- 定义变量。set timeout- 设置超时时间。
puts- 输出变量。exit- 退出 expect 脚本eof- expect 执行结束,退出。set send_human- 在一些场景下,比如通过堡垒机登录时, 如果直接 send 字符会导致不可用, 此时可以设置 send_human 变量是使得 send 自动输入的字符行为更加符合人手工输入的行为. 通过 send -h 可以实现类人
expect 多分支语法
expect 关键字作为 expect 脚本中的关键命令可以实现等待指定字符串后并执行相关程序动作, 通过 expect 可以灵活组合实现不同的效果.
expect "aaa" {send "AAA\r"}
expect "bbb" {send "BBB\r"}
expect "ccc" {send "CCC\r"}
以上脚本是 expect 的基本使用, 首先脚本会等待 字符串'aaa' , 当接收到 aaa 后会发送 AAA; 然后脚本会等待字符串'bbb', 当接收到 bbb 后会发送 BBB; 最后脚本会等待字符串 ccc 并执行相关操作. 其中脚本等待 aaa, bbb ccc 的动作不会同时执行, 而是先等待 aaa 执行相关操作后再执行等待bbb, 最后是 ccc. 其中如果未等待到 aaa(即 timeout 超时后)才会继续等待 bbb操作.
等效于:
等待 aaa 出现时, 发送 AAA
等待 bbb 出现时, 发送 BBB
等待 ccc 出现时, 发送 CCC
其中等待 aaa,bbb,ccc 的操作时按照顺序执行的, 等待 aaa 期间不会同时等待 bbb.
我们再看一种 expect 的多分支语法结构:
expect {
"aaa" {send "AAA\r"}
"bbb" {send "BBB\r"}
"ccc" {send "CCC\r"}
}
如果将 expect 后面跟一个语句块后则会组成一个 expect 分支结构. 脚本再执行 expect 代码块时会等待输出字符串, 如果匹配到 aaa,bbb,ccc 其中之一的话则进入相关分支执行相关命令, 其他未被匹配到的命令不再被执行. 通常这种结构多用于一个命令的执行结果有多种不同的可能情况下.
等效于:
等待 aaa 或 bbb 或 ccc
当 aaa 出现时, 发送 AAA, 结束后跳出 expect
当 bbb 出现时, 发送 BBB, 结束后跳出 expect
当 ccc 出现时, 发送 CCC, 结束后跳出 expect
expect 后面跟语句块常常用于匹配一个命令的不同执行结果场景, 如下面这个脚本, 就是一个基本的 expect 分支结构的用法, 在ssh登录后等待字符串, 如果 匹配到 password 则输入密码登录, 如果匹配到 permission denied 则直接退出
spawn ssh -o StrictHostKeyChecking=no super@$host
expect "*password:" { send "$password\r" }
expect {
"*Permission denied*" { send_user "unauth or pass error. exit.." exit 1}
"*~]$*"
}
interact
关于 expect 语句块还有一种用法, 就是可以在后面跟 exp_continue 命令, 用于表示不跳出expect块而是继续等待新的匹配结果.
expect {
"aaa" { send "AAA\r"; exp_continue }
"bbb" { send "BBB\r"; exp_continue }
"ccc" { send "CCC\r" }
}
可以通过 exp_continue 脚本在 expect 代码块种实现循环等待. exp_continue 表示在执行完语句后不退出 expect 代码块, 而是继续等待监听字符串后再执行相关操作. 相当于 for(;;){ if match continue; else do sone thing } 结构中的 continue 操作.
等效于:
等待 aaa 或 bbb 或 ccc
当 aaa 出现时, 发送 AAA, 结束后继续重新等待匹配字符串
当 bbb 出现时, 发送 BBB, 结束后继续重新等待匹配字符串
当 ccc 出现时, 发送 CCC, 结束后跳出 expect
可以通过 exp_continue 结构实现两次输入密码的逻辑.
set password 123456
spawn ssh -o StrictHostKeyChecking=no root@$host
expect "*password:" { send "$password\r" }
expect {
"*Permission denied*" { send_user "unauth or pass error. exit.." exit 1}
"*~]$*"
}
expect "*\]#" { send "password super" }
expect {
"*password:" { send "$password" exp_continue }
"*token manipulation error" { }
"* tokens updated successfully." { }
}
interact
个性化脚本设置
设置 send_human 可以配置在向进程发送字符串时默认人工输入
当前我有这样一个脚本:
#!/usr/bin/expect -f
set timeout 10
set host [lindex $argv 0]
spawn ssh -o StrictHostKeyChecking=no admin@$host
expect "*password:" { send "123456\r" }
expect "\$" { send "su\r" }
interact {
\001 {send_user "The date is [clock format [clock seconds]]."}
\002 {send_user "you typed a control-B\n"; exit }
˜˜
}
我在使用 expect 中配置使用 sudo su 对节点进行提权时遇到了一些问题, 通过 su 进行提权时总是卡在输入密码的阶段, 抛出 permission denied. 这是因为在执行 su\r 时, 由于输入流过快, 系统误认为 \r 也是密码的一部分, 导致判断密码输入错误从而导致无法通过密码校验. 此时就可以通过设置 send -h 使得命令输入更加类似人类输入的效果从而修复该问题.
- 首选需要配置系统变量
send_human:
set send_human {.1 .3 1 .05 2}
- 然后在需要实现类人输入的位置指定
send -h选项实现该效果. 脚本如下:
#!/usr/bin/expect -f
set timeout 10
set send_human {.1 .3 1 .05 2}
set host [lindex $argv 0]
set password 123456
spawn ssh -o StrictHostKeyChecking=no admin@$host
expect "*password:" { send "$password\r" }
expect "\$" { send -h "su\r" }
expect "*]#" {}
interact {
\001 { send_user "The date is [clock format [clock seconds]]." }
\002 { send_user "auto pass exit\n"; exit }
˜˜
}
interact 实现人工交互
expect 可以通过关键字 interact 将终端的控制权交回给用户, 使得用户可以实现终端交互操作. 由于 expect 主要是实现自动化脚本. 因此需要进入人工交互场景实际并不多.
我们可以在 interact 中自定义按键的行为, 用于实现一些快捷操作, 比如我这里配置了如下快捷键:
- CtrlA(
\001) 实现查看当前时间 - CtrlB(
\002) 实现快速退出 - 输入
$name时, interact 会自动帮我们替换为配置的名字 ghimi.
interact {
\001 {send_user "The date is [clock format [clock seconds]]."}
\002 { exit }
\$name {send "ghimi"}
˜˜
}
下面脚本使用 expect 打开一个 bash进程, 并配置按键的自动化:
#!/usr/bin/expect -f
set timeout 10
spawn bash
interact {
\001 {send_user "The date is [clock format [clock seconds]].\n"}
\002 {send_user "auto bash exit\n"; exit }
\$name {send "ghimi"}
˜˜
}
bash 嵌入执行 expect
#!/bin/bash
hostname=$1
password=$2
set timeout 60
# 加载expect文件路径
/usr/bin/expect <<-EOF
spawn ssh root@${hostname}
expect {
"(yes/no)" { send "yes\r"; exp_continue }
"*password" { send "${password}\r" }
}
expect "*]#" { send "exit\r" }
expect eof
EOF
# expect结束标志,EOF前后不能有空格
expect 脚本示例
使用 expect 通过 ssh 自动登录节点
下面是 expect 脚本用于自动登录的功能, 并且实现自动提权至 root
#!/usr/bin/expect -f
set timeout 10
set host [lindex $argv 0]
spawn ssh -o StrictHostKeyChecking=no admin@$host
expect "Opt or ID>:" { send "$host\r" }
expect "ID>:" { send "0\r" }
expect "*opsadmin@*" { send -h "su\r" }
interact {
\001 {send_user "The date is [clock format [clock seconds]]."}
\002 {send_user "you typed a control-B\n"; exit }
˜˜
}
该脚本实现了使用 admin 用户登录远程主机, 然后通过 sudo su 实现提权至 root 账户下.
将上述脚本保存成 auto_ssh.sh 并赋予 chmod 700 权限, 使用 ./auto_ssh.sh 192.168.1.1 (其中 192.168.1.1 替换成你想要登录的远程节点 ip 地址).
使用 expect 通过 ssh 登录节点实现批量节点自动运维
#!/usr/bin/expect -f
set timeout 10
set ip_list {192.168.1.1 192.168.1.2 192.168.1.3}
foreach ip $ip_list {
spawn ssh -o SgrictHostKeyChecking=no root@$host
expect "Opt or ID>:" { send "$host\r" }
expect "ID>:" { send "0\r" }
expect "*opsadmin@*" { send -h "su\r" }
}
使用 expect 通过 ssh 登录节点实现批量节点自动运维
#!/usr/bin/expect -f
set timeout 10
set ip [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
spawn ssh "$user\@$ip" ifconfig
expect "password" { send "$password\r"}
interact
讲脚本保存为 remote.exp 后可以通过 expect remote.exp 192.168.1.1 root 123456 使用了.
使用 send_human 模拟人工输入
#!/usr/bin/expect -f
set timeout 10
set send_human {.1 .3 1 .05 2}
spawn ssh remote_node01
expect "*root]#" { send -h "ifconfig\r" }
expect "*root]#" { }
#!/usr/bin/expect -f
set timeout 10
set send_human {.1 .3 1 .05 2}
spawn bash
expect "ghimi*" { send -h "date\r" }
expect "ghimi*" { }
参考资料
本文来自博客园,作者:ghimi,转载请注明原文链接:https://www.cnblogs.com/ghimi/p/18807221

浙公网安备 33010602011771号