在Linux/Unix的世界里,用expect就可以很容易得就可以实现人机交互的功能,尤其网上好多examples实现比如password, ftp, telnet之类的程序都很简单。但是如何很好的实现和终端程序交互,尤其是这个终端是用ncurses/curses实现,有着丰富的颜色和复杂的结构,这样的现有的expect就有点力不从心了。现在,笔者参与一个OpenSolaris的项目测试,这个测试要求能够自动和终端交互,来实现复杂的自动人机交互的功能。但是这个项目使用ncurses + python bridge写成的。为了追求好的用户体验,开发人员使用了很多的屏幕刷新,这就要求expect能购追踪每一次刷新并且从用户的最终角度来得到屏幕显示的是什么。
比如:原来屏幕上显示的是"Yes or no....", 但是在下一屏刷新的时候,可能之改变了几个字符,显示就变成了"Yesterday", 如下所示:
|
Y e s o r n o ...
|
Y e s t e r d a y ...
|
但是在expect_out(buffer)里面,显示的就是"...Yes or no\r\n...^[[3Gterday", 这样如果搜寻"Yesterday"这串字符就显然不可能能够搜到的。
有人会问"^[[3G"是什么?这个就是termcap/terminfo 用来使terminal具有丰富显示功能的约定俗成方法。为了方便使用这些功能,人们就把他们组成了各种方法,封装在curses/ncurses库中。具体约定的方法请参见附录1
在expect的创始人Don.Libes的论文〔附录2〕中, 他提出了一种方法能够解决上述terminal问题的自动人机交互,而且作为expect的FAQ给出。但是,他实现的方法是用tcl/tk来模拟一个后台终端(tk terminal),把显示的内容用控制的方法来实现前台终端和后台终端同步显示,在他封装的脚本里,用term_expect〔附录3〕来对后端terminal进行监控从而间接的对用户程序显示的前台进行监控。
但是他的方法里面有一个限制,就是前端运行的terminal程序必须能在后端的tk Terminal上正常显示,但是,据我所知,不同的terminal控制转义字符是不一样的,而tk terminal的转移字符远远不能够覆盖所有其他的terminal类型,比如xterm. 而且有很多程序只能在诸如xterm这样的终端上才能够正常运行。这就造成了他给出的程序在实际应用中受限。更何况很多情况下根本就不允许tk存在(在没有X的情况下)。
Don.Libes 的例子中也给出了virterm〔附录4〕作为非tk terminal的解决方案。但这同样有问题,首先这个程序本身就是一个学生写的,用于从他所在大学的图书馆里自动检索,而且他模拟的后台终端是tt终端现以不多见。所以在把virterm直接用于本例也不能够实现。
但是这里的核心思想却很有用,在后端模仿一个terminal的buffer,来把有用的东西写到buffer里面。寻找一个定位符(cursor),根据expect_out(buffer)的内容不断的调整cursor的位置,不断进行刷新。这样在buffer里面就能够保证和前端terminal显示一致。
首先要根据控制序列的所在终端调整完全转义序列的内容,使之在expect_out(buffer)一个个的得到处理,重点是根据内容来调整cursor.在我的prototype的实验中,如下内容可以用于xterm.
expect {
-re "^\r" {
# (cr,) Go to to beginning of line
set cur_col 0
term_cursor_changed
} "^\n" {
# (ind,do) Move cursor down one line
term_down
term_cursor_changed
} "^\b" {
# Backspace nondestructively
incr cur_col -1
term_cursor_changed
} "^\a" {
# Bell, pass back to user
send_user "\a"
} "^\t" {
# Tab, shouldn't happen
send_error "got a tab!?"
} eof {
term_exit
} "^\x1b\\\[K" {
# (ind,do) Move cursor down one line
term_down
term_cursor_changed
} "^\x1b=" {
#???
} "^\x1b(B" {
#
} "^\x1b(0" {
#
} "^\x1b\\\[A" {
# (cuu1,up) Move cursor up one line
incr cur_row -1
term_cursor_changed
} "^\x1b\\\[C" {
# (cuf1,nd) Nondestructive space
incr cur_col
term_cursor_changed
} -re "^\x1b\\\[((\[0-9]{1,})(;*)(\[0-9]{1,}))*r" {
set cur_row [expr $expect_out(2,string)+1]
set cur_col $expect_out(4,string)
term_cursor_changed
} -re "^\x1b\\\[(\[0-9]{1,2})*G" {
# (cuu1,up) Move cursor to the line
set cur_col $expect_out(0,string)
term_cursor_changed
} -re "^\x1b\\\[(\[0-9]{1,2})*m" { # unsupported
} -re "^\x1b\\\[((\[0-9]{1,2})(;*)(\[0-9]{1,2}))*m" { # unsupported
} -re "^\x1b\\\[((\[0-9]{1,2})(;*)(\[0-9]{1,2})(;*)(\[0-9]{1,2}))*m" { # unsupported
# (rmso,se) End standout mode
set term_standout 0
} -re "^\x1b\\\[(\[0-9]{1,})*X" {
} -re "^\x1b\\\[(\[0-9]{1,})*H" {
# (cuu1,up) Move cursor to the line
set cur_row $expect_out(0,string)
term_cursor_changed
} -re "^\x1b\\\[((\[0-9]*);(\[0-9]*))*H" {
# (cup,cm) Move to row y col x
set cur_row [expr $expect_out(2,string)+1]
set cur_col $expect_out(3,string)
term_cursor_changed
} -re "^\x1b\\\[(\[0-9]*)*l" {
} -re "^\x1b\\\[(\[0-9]*)*J" {
# (clear,cl) Clear screen
term_init
term_cursor_changed
} -re "^\x1b\\\[\\\?(\[0-9]{1,})*h" {
# (smkx,ks) start keyboard-transmit mode
# terminfo invokes these when going in/out of graphics mode
set cur_row 1
term_cursor_changed
} -re "^\x1b\\\[\\\?(\[0-9]{1,})*l" {
} -re "^\[\x20-\x7e]+" {
term_insert $expect_out(0,string)
term_cursor_changed
}
在项目的具体实现过程中,由于匹配效率的问题,上述方法被另外以 C库来写成。首先我们用expect调取程序来实现终端交互,并且把expect_out(buffer)的内容输出供C程序来调取进行实施分析。后端的buffer由C程序来进行实现。然后expect在根据这个buffer里面的内容能够来对前端程序进行匹配和其他操作。
整体的结构图如下:
对了,在测试这个框架的时候,OpenSolaris的项目terminal程序还没有好,我自己用ncurses写了一个stub module,用于前期的可用调研和测试代码的集成测试。如附件。
|
文件: | test1.tar |
大小: | 27KB |
下载: | 下载 |
|
附录:
1.
控制终端代码 - Linux 控制终端转义和控制序列 2. Automation and Testing of Character-Graphic Programs
3. term_expect:
4. virterm: