Shell-通过expect工具脚本的自动交互及实例

 安装expect工具

expect是建立在tcl基础上的一个自动化交互套件, 在一些需要交互输入指令的场景下, 可通过脚本设置自动进行交互通信. 其交互流程是:

spawn启动指定进程 -> expect获取指定关键字 -> send想指定进程发送指定指令 -> 执行完成, 退出.

由于expect是基于tcl的, 所以需要确保系统中安装了tcl:

# 检查是否安装了tcl:
[root@localhost ~]# whereis tcl
tcl: /usr/lib64/tcl8.5 /usr/include/tcl.h /usr/share/tcl8.5
 
# 如果没有安装, 使用yum安装tcl和expect:
[root@localhost ~]# yum install -y tcl
[root@localhost ~]# yum install -y expect
 
# 查看expect的安装路径:
[root@localhost ~]# command -v expect
/usr/bin/expect

  

expect的常用命令

命 令	说 明
spawn	启动新的交互进程, 后面跟命令或者指定程序
expect	从进程中接收信息, 如果匹配成功, 就执行expect后的动作
send	向进程发送字符串
send exp_send	用于发送指定的字符串信息
exp_continue	在expect中多次匹配就需要用到
send_user	用来打印输出 相当于shell中的echo
interact	允许用户交互
exit	退出expect脚本
eof	expect执行结束, 退出
set	定义变量
puts	输出变量
set timeout	设置超时时间
  • Expect中最关键的四个命令是send,expect,spawn,interact。

简单的用法,稍作修改就可以变成很实用都脚本。

#!/usr/bin/expect -f
#用expect实现自动交互
#期望的字符串必须出现,而且必须按顺序出现,send记得带上换行符

#指定要启动的命令,可以带参数
spawn ./hello.sh v1 v2
#可以设置超时,默认的超时就是10秒
set timeout 11 

#一问一答的节奏,expct期望一个字符串,然后send回应一个字符串
expect "(yes/no)"
send "yes\r"
expect "password:"
send "no-password\n"
#期望的字符串是正则匹配的,发送要记得换行符,\r和\n都行
expect "*num"
send "1\n"
#等着命令结束后退出
expect eof

例1:远程登录并创建文件后退出

 

#!/usr/bin/expect           ##注意路径,使用 [whereis expect] 查看
set user "hadoop"           ##设定参数,注意",'的区别
set pwd "yangkun"
set host "48.93.36.144"

set timeout -1              ##;号可有可无
spawn ssh -p 2020 $user@$host
expect {                    ##expect后有空格
    "*yes/no" {send "yes\r";exp_continue}
    "*password:" {send "$pwd\r"}
}
expect "]*"                 ## 通配符,使用 ]* 有效, 使用  *# 无效
send "touch /home/hadoop/aa.txt\r"
expect "]*"
send "echo hello world >> /home/hadoop/aa.txt\r"
expect "]*"
[interact]                  ##人为交互
send "exit\r"               ##退出

  

例2:配置免密登录并安装JDK

#!/bin/bash
#!/usr/bin/expect
SERVERS="114.114.114.114"		##数组以空格分隔,可以为目标ip 或者hostName
PASSWORD="yangkun"

## 实现免密登录配置的函数
auto_ssh_copy_id() {
	expect -c "set timeout -1;
		spawn ssh-copy-id \"-p 2020 $1\";       ## 这里要注意,使用'或\'不可行
		expect {
		   	*(yes/no)* {send -- yes\r;exp_continue;}
			*password:* {send -- $2\r;exp_continue;}
			eof {exit 0;}
		}";
}

## 循环执行,配置主机到从节点所有免密
ssh_copy_id_to_all() {
	for SERVER in $SERVERS				## 取值需要加$
	do
		auto_ssh_copy_id $SERVER $PASSWORD
	done	
}

## 调用循环配置函数
ssh_copy_id_to_all


## 批量部署
for SERVER in $SERVERS
do
    scp install.sh root@$SERVER:/root
    ssh root@$SERVER /root/install.sh
donel取文件中的host配置

读取文件中的host配置

让脚本自动读取slaves文件中的机器名来批量安装
cat slaves | while read host
do
echo $host
expect -c "set timeout -f
spawn ssh-copy-id $host"
done

例3:批量配置JDK,install.sh

以root用户执行

#!/bin/bash
BASE_SERVER=master
BASE_PATH=/home/hadoop/soft
TARGET_PATH=/usr/local
JAVA_PATH=$TARGET_PATH/java
## 1.判断是否存在文件夹,不存在则创建soft文件夹
#if [ ! -d "$BASE_PATH" ]; then
#	mkdir "$BASE_PATH"
#fi

## 2.从指定host拷贝jdk到目标机器上(已经拷贝文件夹)
scp -r $BASE_SERVER:$BASE_PATH $BASE_PATH

## 2.解压jdk到指定目录
if [ ! -d "$JAVA_PATH" ]; then
	sudo -S mkdir -p "$JAVA_PATH"
fi

## 赋予权限
sudo -S chmod -R hadoop:hadoop $JAVA_PATH

tar -zxvf $BASE_PATH/jdk1.8.0_121.tar.gz -C $JAVA_PATH

## 3.配置环境变量
sudo -S cat>>/etc/profile<<EOF
export JAVA_HOME=$JAVA_PATH/jdk1.8.0_121
export PATH=\$PATH:\$JAVA_HOME/bin
EOF

代码保存为一个脚本,例如 a.sh,加上执行执行权限后 ./a.sh 即可运行。

下面接受作用原理简介

简单的示例脚本

这里以ssh远程登录某台服务器的脚本为例进行说明, 假设此脚本名称为remote_login.sh:

#!/usr/bin/expect
 
set timeout 30
spawn ssh -l root 172.16.22.131
expect "password*"
send "123456\r"
interact

脚本功能解读

(1) #!/usr/bin/expect

上述内容必须位于脚本文件的第一行, 用来告诉操作系统, 此脚本需要使用系统的哪个脚本解析引擎来执行.

具体路径可通过command -v expect命令查看.

注意:

这里的expect和Linux的bash、Windows的cmd等程序一样, 都是一种脚本执行引擎.

脚本需要有可执行权限(chmod +x remote_login.sh, 或chmod 755 auto_login.sh), 然后通过命令./remote_login.sh运行即可; 不行的话尝试:expect remote_login.sh 方式运行。

如果输入sh remote_login.sh, 意义就不一样了: 明确调用sh引擎去执行此脚本, 此时首行的#!/usr/bin/expect就失效了,所以也可以直接输入 expect remote_login.sh 运行。

  

(2) set timeout 30

设置连接的超时时间为30秒.

(3) spawn ssh -l root 172.16.22.131

spawn是expect的语句,执行命令前都要加这句,简单地理解就是,在你原来要执行的shell命令前面多加一个 spawn 关键字;

spawn、send等命令是expect工具中的内部命令, 如果没有安装expect工具, 就会出现"spawn not found"等错误.

不要用which spawn之类的命令去找spawn, 因为并没有这样的程序.

(4) expect "password*"

这个命令用来判断上次命令执行的输出结果里是否包含"password*"的字符串;只有spawn执行的命令结果才会被expect捕捉到,因为spawn会启动一个进程,只有这个进程的相关信息才会被捕捉到,主要包括:标准输入的提示信息,eof和timeout;如果捕获到则立即返回, 否则就等待一段时间后返回. 这里的等待时长就是前面设置的timeout, 也就是30秒.

(5) send "123456\r"

这里就是执行交互动作, 作用等同于手工输入密码。send会将expect脚本中需要的信息发送给spawn启动的那个进程,而send_user只是回显用户发出的信息,类似于shell中的echo而已

提示: 命令字符串结尾加上\r, 这样的话, 如果出现异常等待的状态就能够停留下来, 作进一步的核查.

(6) interact

interact代表执行完留在远程控制台,不加这句执行完后返回本地控制台 。expect执行完成后保持用户的交互状态, 这个时候用户就可以手工操作了.

如果没有这一句, expect执行完成后就会退出脚本刚刚远程登录过去的终端, 用户也就不能继续操作了.

其他脚本使用示例

4.1 直接通过expect执行多条命令

注意首行内容, 这种情况下就只能通过./script.sh(或expect  script.sh)来执行这类脚本了:

#!/usr/bin/expect -f
 
set timeout 10
# 切换到root用户, 然后执行ls和df命令:
spawn su - root
expect "Password*"
send "123456\r"
expect "]*"         # 通配符
send "ls\r"
expect "#*"         # 通配符的另一种形式
send "df -Th\r"
send "exit\r"       # 退出spawn开启的进程
 
expect eof          # 退出此expect交互程序

  

4.2 通过shell调用expect执行多条命令

注意首行内容, 这种情况下可通过sh script.shbash script.sh 或./script.sh, 都可以执行这类脚本:

注:简单插入一下重定向的基本用法:

命令符号格式 作用
命令 < 文件 将指定文件作为命令的输入设备
命令 << 分界符 表示从标准输入设备(键盘)中读入,直到遇到分界符才停止(读入的数据不包括分界符),这里的分界符其实就是自定义的字符串
命令 < 文件 1 > 文件 2 将文件 1 作为命令的输入设备,该命令的执行结果输出到文件 2 中。

例:
command <<END
  document
END

  

command是 Shell 命令,<<END是开始标志,END是结束标志,document是输入的文档(也就是一行一行的字符串)。

这种写法告诉 Shell 把 document 部分作为命令需要处理的数据,直到遇见终止符END为止(终止符END不会被读取)。

注意,终止符END必须独占一行,并且要定顶格写。

分界符(终止符)可以是任意的字符串,由用户自己定义,比如 END、MARKER 等。分界符可以出现在正常的数据流中,只要它不是顶格写的独立的一行,就不会被作为结束标志。

Shell中通常将EOF与 << 结合使用,表示后续的输入作为子命令或子Shell的输入,直到遇到EOF为止,再返回到主调Shell。可以把EOF替换成其他东西,意思是把内容当作标准输入传给程序。

回顾一下< <的用法。当shell看到< <的时候,它就会知道下一个词是一个分界符。在该分界符以后的内容都被当作输入,直到shell又看到该分界符(位于单独的一行)。这个分界符可以是你所定义的任何字符串。

#!/bin/bash
 
ip="172.16.22.131"
username="root"
password="123456"
 
# 指定执行引擎
/usr/bin/expect <<EOF
    set time 30
    spawn ssh $username@$ip df -Th
    expect {
        "*yes/no" { send "yes\r"; exp_continue }
        "*password:" { send "$password\r" }
    }
    expect eof
EOF

  带有命令参数的shell脚本 :

# 脚本文件
#!/bin/bash 
ip=$1  
user=$2 
password=$3 
 
expect <<EOF  
    set timeout 10 
    spawn ssh $user@$ip 
    expect { 
        "yes/no" { send "yes\n";exp_continue } 
        "password" { send "$password\n" }
    } 
    expect "]#" { send "useradd hehe\n" } 
    expect "]#" { send "touch /tmp/test.txt\n" } 
    expect "]#" { send "exit\n" } expect eof 
 EOF  
 
 
 # 运行命令
 #./ssh5.sh 192.168.1.10 root 123456

  

五、常见问题及解决方法

5.1 spawn not found 的解决

出现这个错误的基本上都是初学者: Linux 执行shell脚本有两种方式:

一种是将脚本作为sh的命令行参数, 如sh remote_login.sh, 或sh /data/remote_login.sh;

一种是将脚本作为具有执行权限的可执行脚本, 如./remote_login.sh, 或/data/remote_login.sh.

5.2 expect: spawn id exp6 not open while executing "expect eof"的解决
一个小例子,用于linux下账户的建立:

filename: account.sh,可以使用./account.sh newaccout来执行;

#!/usr/bin/expect
 
set passwd "mypasswd"
set timeout 60
 
if {$argc != 1} {
    send "usage ./account.sh \$newaccount\n"
    exit
}
 
set user [lindex $argv [expr $argc-1]]
 
spawn sudo useradd -s /bin/bash -g mygroup -m $user
 
expect {
    "assword" {
        send_user "sudo now\n"
        send "$passwd\n"
        exp_continue
    }
    eof
    {
        send_user "eof\n"
    }
}
 
spawn sudo passwd $user
expect {
    "assword" {
        send "$passwd\n"
        exp_continue
    }
    eof
    {
        send_user "eof"
    }
}
 
spawn sudo smbpasswd -a $user
expect {
    "assword" {
        send "$passwd\n"
        exp_continue
    }
    eof
    {
        send_user "eof"
    }
}

  

注意点:

第3行: 对变量赋值的方法;
第4行: 默认情况下,timeout是10秒;
第6行: 参数的数目可以用$argc得到;
第11行:参数存在$argv当中,比如取第一个参数就是[lindex $argv 0];并且如果需要计算的话必须用expr,如计算2-1,则必须用[expr 2-1];
第13行:用spawn来执行一条shell命令,shell命令根据具体情况可自行调整;有文章说sudo要加-S,经过实际测试,无需加-S亦可;
第15行:一般情况下,如果连续做两个expect,那么实际上是串行执行的,用例子中的结构则是并行执行的,主要是看匹配到了哪一个;在这个例子中,如果你写成串行的话,即

expect "assword"
send "$passwd\n"
expect eof
send_user "eof"

  

那么第一次将会正确运行,因为第一次sudo时需要密码;但是第二次运行时由于密码已经输过(默认情况下sudo密码再次输入时间为5分钟),则不会提示用户去输入,所以第一个expect将无法匹配到assword,而且必须注意的是如果是spawn命令出现交互式提问的但是expect匹配不上的话,那么程序会按照timeout的设置进行等待;可是如果spawn直接发出了eof也就是本例的情况,那么expect "assword"将不会等待,而直接去执行expect eof。 这时就会报expect: spawn id exp6 not open,因为没有spawn在执行,后面的expect脚本也将会因为这个原因而不再执行;所以对于类似sudo这种命令分支不定的情况,最好是使用并行的方式进行处理;
第17行:仅仅是一个用户提示而已,可以删除;
第18行:向spawn进程发送password;
第19行:使得spawn进程在匹配到一个后再去匹配接下来的交互提示;
第21行:eof是必须去匹配的,在spawn进程结束后会向expect发送eof;如果不去匹配,有时也能运行,比如sleep多少秒后再去spawn下一个命令,但是不要依赖这种行为,很有可能今天还可以,明天就不能用了;

下面这个例子比较特殊,在整个过程中就不能expect eof了:

#!/usr/bin/expect
 
set timeout 30
spawn ssh 10.192.224.224
expect "password:"
send "mypassword\n"
expect "*$"
send "mkdir tmpdir\n"
 
expect "*$"

  

timeout指expect等待命令至多等30seconds
这个例子实际上是通过ssh去登录远程机器,并且在远程机器上创建一个目录,我们看到在我们输入密码后并没有去expect eof,这是因为ssh这个spawn并没有结束,而且手动操作时ssh实际上也不会自己结束除非你exit;所以你只能expect bash的提示符,当然也可以是机器名等,这样才可以在远程创建一个目录。

注意,请不要用spawn mkdir tmpdir,这样会使得上一个spawn即ssh结束,那么你的tmpdir将在本机建立。

当然实际情况下可能会要你确认ssh key,可以通过并行的expect进行处理,不多赘述。

觉得bash很多情况下已经很强大,所以可能用expect只需要掌握这些就好了,其他的如果用到可以再去google了

参考资料:

Linux重定向(输入输出重定向)详解
Linux expect 介绍和用法

shell脚本之Expect免交互的实现

解决问题expect: spawn id exp6 not open while executing "expect eof"

Shell脚本交互之:自动输入密码

Linux中命令行中EOF的用法​​​​​​​
————————————————
原文链接:https://blog.csdn.net/fitaotao/article/details/123576889

 

posted on 2023-02-27 10:27  大兄弟666  阅读(485)  评论(0编辑  收藏  举报