expect学习笔记及实例详解

时间:2023-03-08 20:03:01

因为最近正在学习expect脚本,但是发现网上好多文章都是转载的,觉得这篇文章还不错,所以简单修改之后拿过来和大家分享一下~

1. expect是基于tcl演变而来的,所以很多语法和tcl类似,基本的语法如下所示:
1.1 首行加上/usr/bin/expect
1.2 spawn: 后面加上需要执行的shell命令,比如说spawn sudo touch testfile
1.3 expect: 只有spawn执行的命令结果才会被expect捕捉到,因为spawn会启动一个进程,只有这个进程的相关信息才会被捕捉到,主要包括:标准输入的提示信息,eof和timeout。
1.4 send和send_user:send会将expect脚本中需要的信息发送给spawn启动的那个进程,而send_user只是回显用户发出的信息,类似于shell中的echo而已。

2. 一个小例子,用于Linux下账户的建立:
filename: account.sh,可以使用./account.sh newaccout来执行;

1 #!/usr/bin/expect
  2
  3 set passwd "mypasswd"【这个是你设置的密码】
  4 set timeout 60
  5
  6 if {$argc != 1} {
  7     send "usage ./account.sh \$newaccount\n"
  8     exit
  9 }
 10
 11 set user [lindex $argv [expr $argc-1]]
 12
 13 spawn sudo useradd -s /bin/bash -g mygroup -m $user
 14
 15 expect {
 16     "assword" {
 17         send_user "sudo now\n"
 18         send "$passwd\n"
 19         exp_continue
 20     }
 21     eof
 22     {
 23         send_user "eof\n"
 24     }
 25 }
 26
 27 spawn sudo passwd $user
 28 expect {
 29     "assword" {
 30         send "$passwd\n"
 31         exp_continue
 32     }
 33     eof
 34     {
 35         send_user "eof"
 36     }
 37 }
 38
 39 spawn sudo smbpasswd -a $user
 40 expect {
 41     "assword" {
 42         send "$passwd\n"
 43         exp_continue
 44     }
 45     eof
 46     {
 47         send_user "eof"
 48     }
 49 }

3. 注意点:
第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下一个命令,但是不要依赖这种行为,很有可能今天还可以,明天就不能用了;

4. 其他
下面这个例子比较特殊,在整个过程中就不能expect eof了:
 1  #!/usr/bin/expect
 2
 3  set timeout 30
 4  spawn ssh 10.192.224.224
 5  expect "password:"
 6  send "mypassword\n"
 7  expect "*$"
 8  send "mkdir tmpdir\n"
 9  expect "*$"
这个例子实际上是通过ssh去登录远程机器,并且在远程机器上创佳一个目录,我们看到在我们输入密码后并没有去expect eof,这是因为ssh这个spawn并没有结束,而且手动操作时ssh实际上也不会自己结束除非你exit;所以你只能expect bash的提示符,当然也可以是机器名等,这样才可以在远程创建一个目录。

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

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

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

6.实例:下面这个脚本是完成对单个服务器scp任务。

 1: #!/usr/bin/expect
 2: 
 3: set timeout 10
 4: set host [lindex $argv 0]
 5: set username [lindex $argv 1]
 6: set password [lindex $argv 2]
 7: set src_file [lindex $argv 3]
 8: set dest_file [lindex $argv 4]
 9: 
 10: spawn scp $src_file $username@$host:$dest_file
 11: expect {
 12:     "(yes/no)?"
 13:         {
 14:             send "yes\n"
 15:             expect "*assword:" { send "$password\n"}
 16:         }
 17:     "*assword:"
 18:         {
 19:             send "$password\n"
 20:         }
 21:     }
 22: expect "100%"
 23: expect eof

注意代码刚开始的第一行,指定了expect的路径,与shell脚本相同,这一句指定了程序在执行时到哪里去寻找相应的启动程序。代码刚开始还设定了timeout的时间为10秒,如果在执行scp任务时遇到了代码中没有指定的异常,则在等待10秒后该脚本的执行会自动终止。

spawn代表在本地终端执行的语句,在该语句开始执行后,expect开始捕获终端的输出信息,然后做出对应的操作。expect代码中的捕获的(yes/no)内容用于完成第一次访问目标主机时保存密钥的操作。有了这一句,scp的任务减少了中断的情况。代码结尾的expect eof与spawn对应,表示捕获终端输出信息的终止。

有了这段expect的代码,还只能完成对单个远程主机的scp任务。如果需要实现批量scp的任务,则需要再写一个shell脚本来调用这个expect脚本。

1: #!/bin/sh

 2: 
 3: list_file=$1
 4: src_file=$2
 5: dest_file=$3
 6: 
 7: cat $list_file | while read line
 8: do
 9:     host_ip=`echo $line | awk '{print $1}'`
 10:     username=`echo $line | awk '{print $2}'`
 11:     password=`echo $line | awk '{print $3}'`
 12:     echo "$host_ip"
 13:     ./expect_scp $host_ip $username $password $src_file $dest_file
 15: done

很简单的代码,指定了3个参数:列表文件的位置、本地源文件路径、远程主机目标文件路径。需要说明的是其中的列表文件指定了远程主机ip、用户名、密码,这些信息需要写成以下的格式:

IP username password

中间用空格或tab键来分隔,多台主机的信息需要写多行内容。

这样就指定了两台远程主机的信息。注意,如果远程主机密码中有“$”、“#”这类特殊字符的话,在编写列表文件时就需要在这些特殊字符前加上转义字符,否则expect在执行时会输入错误的密码。

对于这个shell脚本,保存为batch_scp.sh文件,与刚才保存的expect_scp文件和列表文件(就定义为hosts.list文件吧)放到同一目录下,执行时按照以下方式输入命令就可以了:

./batch_scp.sh ./hosts.list /root/src_file /root/destfile

===============================================================================

下面我们来看一些expect的一些内部参数:

exp_continue [-continue_timer]
             The command exp_continue allows expect itself to continue executing rather than returning as it  normally
             would.  By  default  exp_continue  resets the timeout timer. The -continue_timer flag prevents timer from
             being restarted.

exp_version [[-exit] version]
             is useful for assuring that the script is compatible with the current version of Expect.

With  no  arguments, the current version of Expect is returned.  This version may then be encoded in your
             script.  If you actually know that you are not using features of recent versions, you can specify an ear-
             lier version.

具体的用法还可以查看文档~

#!/bin/sh
# \
exec expect -- "$0" ${1+"$@"}
exp_version -exit 5.0
if {$argc!=2} {
    send_user "usage: remote-exec command password\n"
    send_user "Eg. remote-exec \"ssh user@host ls\; echo done\" password\n"
    send_user "or: remote-exec \"scp /local-file user@host:/remote-file\" password\n"
    send_user "or: remote-exec \"scp user@host:/remote-file local-file\" password\n"
    send_user "or: remote-exec \"rsync --rsh=ssh /local-file user@host:/remote-file\" password\n"
    send_user "Caution: command should be quoted.\n"
    exit
}
set cmd [lindex $argv 0]
set password [lindex $argv 1]
eval spawn $cmd
set timeout 600
while {1} {
    expect -re "Are you sure you want to continue connecting (yes/no)?" {
            # First connect, no public key in ~/.ssh/known_hosts
            send "yes\r"
        } -re "assword:" {
            # Already has public key in ~/.ssh/known_hosts
            send "$password\r"
        } -re "Permission denied, please try again." {
            # Password not correct
            exit
        } -re "kB/s|MB/s" {
            # User equivalence already established, no password is necessary
            set timeout -1
        } -re "file list ..." {
            # rsync started
            set timeout -1
        } -re "bind: Address already in use" {
            # For local or remote port forwarding
            set timeout -1
        } -re "Is a directory|No such file or directory" {
            exit
        } -re "Connection refused" {
            exit
        } timeout {
            exit
        } eof {
            exit
        }
}

注意用法:

Eg. remote-exec "ssh user@host ls; echo done" password
or: remote-exec "scp /local-file user@host:/remote-file" password
or: remote-exec "scp user@host:/remote-file local-file" password
or: remote-exec "rsync --rsh=ssh /local-file user@host:/remote-file" password
Caution: command should be quoted.