Shell: expect – 自动交互脚本, linux 自动化, shell自动化, expect教程中文版:expect说明

样式匹配

*

expect "hi*"
send "$expect_out(0,string) $expect_out(buffer)"

输入philosophic,输出为hilosophic philosophic,hi*匹配的是hilosophic

如果是hi*hi,则匹配的是hilosophi

如果是*hi*,则匹配的是philosop hi c\n ,而不是p hi losophic\n,因为匹配是从左到右进行,且*尽可能匹配更多的字符,但是要符合样式,所以第一个*匹配的字符多一些

*开头的样式并不经常用到,像*hi*,它能把前面不匹配的数据保存在expect_out(0,string),但是expect_out(buffer)中也会保存,所以这点也没什么意义

*结尾的样式经常需要认真考虑,因为程序的输出不像人的标准输入一样是一行一行而是一堆一堆进行,这样可能匹配的最多数据还没发送完,程序已经返回了。因为*可以匹配任何东西,包括空字符串,可以用它做结尾匹配人的一些输入,并清理输入缓存。

  • 例子:
    expect "220*"      ;#在ftp的例子中会出错,可能程序没准备好,即已经返回了
    expect "220*ready" ;#可以在整个的输入完成后才返回
    expect "220*re"    ;#可以作为一个简写,但是"220*r"就太简单了,它的r就可能匹配的server

更多Glob样式

  • ?:匹配一个字符,a?d匹配abd但不匹配abcd
  • []:匹配它范围内的任意字符
    • [abcdef0123456789]能匹配任意十六进制数,也可以写成[a-f0-9]
    • 如果要匹配短横杠-,要将其写在最前或者最后,例如[-a-c]匹配-,a,b,c
    • 因为中括号[]也是特殊的expect的字符,它里面的式子要立刻求值,所以[]样式必须写成下面两种方式:
      • expect "\[a-f0-9]",推荐使用
      • expect {[a-f0-9]},会出现预想不到的情况,因为里面所有的值都不会进行运算
      • 也可以括号的第二部分加\,但这不是必须的
  • 斜杠:太复杂,举例:
    expect "\n"     ;#matches \n
    expect "\r"     ;#matches \r
    expect "\z"     ;#matches z
    expect "\{"     ;#matches {
    expect "*"      ;#matches * and ? and X and abc
    expect "\*"    ;#matches * but not ? or X or abc
    expect "?"      :#matches * and ? and X but not abc
    expect "\?"    ;#matches ?but not * or X or abc
    expect "n"      ;#matches n
    expect "\n"     ;#matches \n
    expect "\n"    ;#matches n
    expect "\\n"   ;#matches \n
    expect "\\n"  ;#matches sequence of \ and n
    expect "\\\n"      ;#matches sequence of \ and \n
    expect "\\\n"     ;#matches sequence of \ and n
    expect \\\\n      ;#matches sequence of \ and \n
    expect \\\\n      ;#matches sequence of \,and \, and n
    expect "*"           ;#matches anything
    expect "\*"          ;#matches anything
    expect "\*"         ;#matches *
    expect "\\*"        ;#matches *
    expect "\\*"       ;#matches \ followed by anything
    expect "\\\*"      ;#matches \ followed by anything
    expect "\\\*"     ;#matches \ followed by *
    expect "\\["        ;#matches literal [
     
    # a procedure named xy returns the string "n*w"
    expect "[xy]"         ;#matches n followed by anything
    expect "\[xy]"        ;#matches x or y
    expect "\[xy]"       ;#matches n followed by anything followed by w
    expect "\\[xy]"      ;#matches [xy]
    expect "\\[xy]"     ;#matches \ followed by n followed...
    expect "\\\[xy]"    ;#matches sequence of \ and x or y
     
    # 注意匹配的过程,tcl按照顺序匹配,字符的转义等,都发生在这个阶段,而pattern matcher在第二阶段不会有转义了,只是按照字符的实际匹配
    expect "\\ n"  #在经tcl处理后字符为\,'\n',所以第二阶段匹配的是'\n',就是第一阶段的结果来
    expect "\ n"   #在经tcl处理后字符为\ ,'n',所以第二阶段匹配的是'n'
    expect "\\*"   #在经过tcl处理后字符为'\*', 因为还有一个\存在,所以第二阶段匹配是严格的*,而不是anything

处理超时

expect "hi"
expect "hi" {}
expect {
       "hi"     {send "You said hi\n"}
       "hello"  {send "Hello yourself\n"}
       "bye"
}

只有expect命令中最后一个动作可以省略

  • 增加错误检查
    spawn ftp $argv
    set timeout 10
    expect {
           "connection refused" exit
           "unknown host" exit
           "Name"
    }
    send "anonymous\r"
  • 区分超时和连接成功
    expect {
           timeout {puts "timed out",exit }
           "connection refused" exit
           "unknown host" exit
           "Name"
    }
  • 使用超时取代一切错误处理
    expect {
           timeout exit
           "Name"
    }

事实上,用来超时代替一切,会增加程序的反应时间,但要处理每种错误,就要在脚本上花费很多时间。通常用超时处理一些模糊的条件,而用一个样式处理大多数的错误,例如ftp的错误都是以4或者5开头,例子:

expect {
       timeout {unexpected...}
       "^\[45]" {error...}
       "ftp>"
}

在一些超时比较长的程序,可以先用ping判断主机是否存在

spawn ping $host
set timeout 2
expect "alive" {exit 0} timeout {exit 1}

可以用echo $?察看脚本的返回值,可以为timeout增加一些内容输出,因为ping有返回的话自会打印结果,如果超时,脚本会在ping返回结果以前将其终止并结束,所以脚本要增加输出内容:

spawn ping $host
set timeout 2
expect "alive" {exit 0} timeout{
       puts "no answer from $host"
       exit 1
}

从命令行设置超时时间,用法maxtime 20 prog

#!/usr/bin/expect -
set timeout [lindex $argv 0]
spawn [lindex $argv 1]
expect

文件结尾处理

在以上maxtime脚本中,如果程序先结束,则脚本随之结束,因为没有要匹配的样式,所以程序如果在超时以前没有结束,脚本也会返回。

#!/usr/bin/expect -
set timeout [lindex $argv 0]
eval spawn [lindex $argv 1 end]
expect {
       timeout {puts "took toomuch time"}
       eof {puts "finished in time"}
}

maxtime 2 sleep 5%maxtime 5 sleep 2返回的结果是不一样的,eof和timeout一样,也是一个特殊的匹配样式,表示程序结束。

  • spawn的注意

eval spawn [lrange $argv 1 end]来执行spawn命令,因为spawn会将它后面lrange取得的所有参数当作一个命令。

  • eof的注意
    spawn ping $host
    set timeout 2
    expect "alive" {exit 0} timeout {exit 1}

如果ping发现主机不存在,返回eof,则expect正常结束,它返回0,正常情况下脚本执行成功才返回0,但是这种情况下主机不存在也会返回0,所以要对eof处理,修订脚本如下:

spawn ping $host
set timeout 2
expect "alive" {exit 0} timeout {exit 1} eof {exit 1}

如果对超时和异常结束作相同的处理,可以default样式,它可以匹配所有可能的情况: expect "alive" {exit 0} default {exit 1}

close命令

eof不仅可以从process送往expect,也可以从expect送往process,使process退出,在expect脚本中用close显式结束一个进程,通常情况下,process和expect一个结束了,另一个也会随之结束。

忽略eof的程序

一类是运行在Raw模式下的程序,像telnet,这种模式下,不对输入的字符作特殊的翻译,^c,^d都不起特殊的作用,只是普通字符,eof也是,所以没有正常退出的话要手动杀死进程,用kill命令。

第二类是看到eof就忽略已经接收到的字符马上退出的进程,像ftp,vi,下面的脚本:

spawn ftp...
#assume username and password are accepted here
expect "ftp>" {send "get file1\r"}
expect "ftp>" {send "get file2\r"}
expect "ftp>" {send "get file3\r"}

上面的脚本只能接收到两个文件,因为在收到第三个文件的请求时也收到eof,因为脚本执行完毕,0所以退出,解决办法是在行末加一句epect命令,使脚本不能提前结束。

spawn vi file
send "ifoo3:wq\r"

错误,文件没有内容,正确的如下:

spawn vi file
send "ifoo3:wq\r"
expect

wait命令

wait 等待一个进程死亡,-noflag用来避免时延。

expect重要的命令

send命令

send "hello world"
send "hello world\n"
expect speak        #speak为一脚本

expect命令

expect "hi\n"
send "hello there!\n"
expect_out(0,string)  # 用来保存匹配的字符,匹配字符和它以前的字符保存在expect_out(buffer)
expect "hi\n"
send "you typed <$expect_out(buffer)>"
send "but I only expected <$expect_out(0,string)>"

定位

  • ^string:只匹配输入的开始部分,像^hi匹配hiccup但是不匹配sushi
  • string\(:只匹配输入的结束部分,像hi\)匹配sushi但是不匹配hiccup,而^hi$只匹配hi
  • expect的输入以自己接受到的字符为准,并不考虑行界,如果没有定位符,它会匹配最早遇到的合适字符
  • 不匹配的情况,例子说明:
    expect "hi"
    send "$expect(0,string) $expect_out(buffer)"

此时输入philosophic,输出为hi phi

如果再次执行这两句命令,输出为hi losophi

input buffer用来保存expect取得的输入字符串,第一匹配结束后,expect_out(0,string)hi,expect_out(buffer)phi,下一次的匹配从losophi开始,所以上面的两个值分别为hi,losophi,此时input buffer里为c\n,因为不能够匹配hi,所以如果进行第三次匹配,会超时,而上面的expect_out(0,string) expect_out(buffer)与第二次相同,所以第三次超时后会输出hi losophi

  • 超时值可以设置:set timeout INTERGER

INTERGER为一整数值,如果为-1表示永久等待,如果为0表示立即返回

  • 多种样式匹配
    expect "hi"    { send "You said hi\n" } \
           "hello" { send "Hello yourself\n" } \
           "bye"   { send "That was unexpected\n" }

    命令会对hi,hello,bye同时进行匹配 注意书写格式,expect "hi" send "You said hi\n"#错误,You said hi会被解释成一个命令

  •  可用的其它格式
    expect {
           "hi"    { send "You said hi\n"}
           "hello" { send "Hello yourself\n"}
           "bye"   { send "That was unexpected\n"}
    }
     
    expect "hi" {
           send "You said hi\n"
    } "hello" {
           send "Hello yourself\n"
    } "bye" {
           send "That was unexpected\n"
    }
     
    expect {
           "hi" {
                  send "You said hi\n"
           }
           "hello" {
                  send "Hello yourself\n"
           }
           "bye" {
                  send "That was unexpected\n"
           }
    }
    

    expect "exit" {exit 1} "quit" abort为写在一行的格式,匹配命令带参数的话一定要用大括号括起来。

    • 例子:Timed Reads

    timed-read 60作用是等待60,接收此段时间内用户的输入。

    expect {
    #!/usr/bin/expect -
    set timeout $argv
    expect "\n" {
         send [string trimright "$expect_out(buffer)" "\n"]
    }

spawn命令

spawn命令用于启动一个进程

例子ftp-rfc

#!/usr/local/bin/expect -
#retrieve an RFC (or the index) from uunet via anon ftp
if { [llength $argv]== 0} {
       puts "usage: ftp-rfc {-index|#}"
       exit 1
}
set timeout -1
spawn ftp ftp.uu.net
expect "Name"
send "anonymous\r"
expect "Password:"
send don@libes.com\r
expect "ftp> "
send "cd inet/rfc\r"
expect "ftp> "
send "binary\r"
expect "ftp> "
send "get rfc$argv.z\r"
expect "ftp> "
exec uncompress rfc$argv.z

例子中binary的命令在于让ftp传文件时不要作任何转换,注意结尾是\r,不是\n,视为手动输入和机器输入的差别?? 最后一句的作用是解压缩。

interact命令

interact命令用于脚本执行完简单的命令后人手动介入,只在所属的spawn进程空间有效

例子aftp

#!/usr/bin/expect -
spawn ftp $argv
expect "Name"
send "anonymous\r"
expect "Password:"
send don@libes.com\r
interact
可以用send "$env(USER)@libes.com\r"
proc domainname {} {
       set file [open /etc/resolv.conf r]
       while { [gets $file buf] !=-1} {
              if {[scan $buf "domain %s" name]==1} {
                     close $file
                     return $name
              }
       }
       close $file
       error "no domain declaration in /etc/resolv.conf"
}
send "$env(USER)@[domainname]\r"

其它

expect的几种格式

其中的第一种情况和情况二是两种完全不同的语义。

对于第一种情况来说,它是一个多选一的过程,对于特定任务的输出,只要其中的任何一个pattern能够匹配,那么这个expect就算是完成了一次匹配。

第二种情况则表示说进行若干次各自的匹配,所有的匹配被依次满足了之后才算整个语句完成。这一点就和前面说的tcl的命令解析方式有关了,因为第一种格式虽然占用的行数多,但是它是一个单独的expect命令,所以expect可以一次性将所有情况解析出来,然后组成一个大的并行分支;对于第二种,expect看到的只是多条单独的expect匹配,根据每个expect必须又一次匹配的原则,需要有多次匹配。总之简单的说,一个是并行关系,一个串行关系。

第三种是expect的一种特殊用法,虽然很少有人这么做,但是如果真的有人这么你也不能说错。它的意义同样是完成一次匹配,并且匹配之后不执行任何动作,同样是只要不匹配就继续执行。

 

本文:Shell: expect – 自动交互脚本, linux 自动化, shell自动化, expect教程中文版:expect说明