通过编写和自动化一个简单的游戏来学习 Expect

用 Expect 编写一个“猜数字”游戏。然后,通过一个单独的脚本来自动化玩游戏,学习 Expect 的真正威力。
3 位读者喜欢这个。
An introduction to GNU Screen

Opensource.com

在尝试自动化我的工作流程时,我遇到了一个配置实用程序,它拒绝有意义的自动化。这是一个 Java 进程,它不支持静默安装程序或 stdin,并且有一组不一致的提示。Ansible 的 expect 模块不足以完成此任务。但我发现 expect 命令正是完成这项工作的工具。

我学习 Expect 的旅程意味着学习一点 Tcl。现在我已经有了创建简单程序的背景,我可以更好地学习用 Expect 编程。我认为写一篇演示这个历史悠久的实用程序的酷功能的文章会很有趣。

本文超越了典型的简单游戏形式。我计划使用 Expect 的一部分来创建游戏本身。然后,我用一个单独的脚本来演示 Expect 的真正威力,以自动化玩游戏。

这个编程练习展示了几个经典的编程示例:变量、输入、输出、条件评估和循环。

安装 Expect

对于基于 Linux 的系统,请使用

$ sudo dnf install expect
$ which expect
/bin/expect

我发现我的 Expect 版本包含在 macOS 的基本操作系统中

$ which expect
/usr/bin/expect

在 macOS 上,您也可以使用 brew 加载稍微新一点的版本

$ brew install expect
$ which expect
/usr/local/bin/expect

用 Expect 猜数字

使用 Expect 的猜数字游戏与我在之前的文章中使用的基本 Tcl 没有太大区别。

Tcl 中的所有事物都是字符串,包括变量值。代码行最好用花括号括起来(而不是尝试使用行继续符)。方括号用于命令替换。命令替换对于从其他函数派生值很有用。它可以直接用作需要的输入。您可以在后续脚本中看到所有这些。

创建一个新的游戏文件 numgame.exp,将其设置为可执行文件,然后输入以下脚本

#!/usr/bin/expect

proc used_time {start} {
	return [expr [clock seconds] - $start]
}

set num [expr round(rand()*100)]
set starttime [clock seconds]
set guess -1
set count 0

send "Guess a number between 1 and 100\n"

while { $guess != $num } {
	incr count
	send "==> "

	expect {
		-re "^(\[0-9]+)\n" {
			send "Read in: $expect_out(1,string)\n"
			set guess $expect_out(1,string)
		}

		-re "^(.*)\n" {
			send "Invalid entry: $expect_out(1,string) "
		}
	}

	if { $guess < $num } {
		send "Too small, try again\n"
	} elseif { $guess > $num } {
		send "Too large, try again\n"
	} else {
		send "That's right!\n"
	}
}

set used [used_time $starttime]

send "You guessed value $num after $count tries and $used elapsed seconds\n"

使用 proc 设置一个函数(或过程)定义。这包括函数名称,后跟包含参数(1 个参数 {start})的列表,然后是函数体。return 语句显示了嵌套 Tcl 命令替换的一个很好的例子。set 语句定义变量。前两个使用命令替换来存储随机数和当前系统时间(以秒为单位)。

while 循环和 if-elseif-else 逻辑应该很熟悉。再次注意花括号的特殊位置,以帮助将多个命令字符串组合在一起,而无需行继续符。

您在这里看到的与之前的 Tcl 程序的最大区别是使用了函数 expectsend,而不是使用 putsgets。使用 expectsend 构成了 Expect 程序自动化的核心。在这种情况下,您使用这些函数来自动化终端上的用户。稍后您可以自动化一个真正的程序。在这种上下文中,使用 send 命令与在屏幕上打印信息没有太大区别。expect 命令稍微复杂一些。

expect 命令可以采用几种不同的形式,具体取决于您的处理需求的复杂性。典型的用法由一个或多个模式-动作对组成,例如

expect "pattern1" {action1} "pattern2" {action2}

更复杂的需求可以将多个模式-动作对放在花括号内,可以选择以更改处理逻辑的选项为前缀。我上面使用的形式封装了多个模式-动作对。它使用选项 -re 将正则表达式处理(而不是 glob 处理)应用于模式。它后跟花括号,花括号封装了一个或多个要执行的语句。我在上面定义了两个模式。第一个旨在匹配一个或多个数字的字符串

"^(\[0-9]+)\n"

第二个模式旨在匹配任何不是数字字符串的内容

"^(.*)\n"

请注意,expect 的这种用法是从 while 语句中重复执行的。这是读取多个条目的完全有效的方法。在自动化中,我展示了 Expect 的一种稍微不同的变体,它可以为您完成迭代。

最后,$expect_out 变量是 expect 使用的数组,用于保存其处理结果。在这种情况下,变量 $expect_out(1,string) 保存正则表达式的第一个捕获模式。

运行游戏

这里应该没有意外

$ ./numgame.exp 
Guess a number between 1 and 100
==> Too small, try again
==> 100
Read in: 100
Too large, try again
==> 50
Read in: 50
Too small, try again
==> 75
Read in: 75
Too small, try again
==> 85
Read in: 85
Too large, try again
==> 80
Read in: 80
Too small, try again
==> 82
Read in: 82
That's right!
You guessed value 82 after 8 tries and 43 elapsed seconds

您可能会注意到的一个区别是这个版本表现出的不耐烦。如果您犹豫足够长的时间,expect 会超时并显示无效条目。然后它会再次提示您。这与无限期等待的 gets 不同。expect 超时是一个可配置的功能。它有助于处理挂起的程序或意外输出期间的情况。

在 Expect 中自动化游戏

对于此示例,Expect 自动化脚本需要与您的 numgame.exp 脚本位于同一文件夹中。创建 automate.exp 文件,使其可执行,打开编辑器,然后输入以下内容

#!/usr/bin/expect

spawn ./numgame.exp

set guess [expr round(rand()*100)]
set min 0
set max 100

puts "I'm starting to guess using the number $guess"

expect {
     -re "==> " { 
        send "$guess\n"
        expect {
             "Too small" {
                 set min $guess
                 set guess [expr ($max+$min)/2]
             }
             "Too large" {
                 set max $guess
                 set guess [expr ($max+$min)/2]
             }
             -re "value (\[0-9]+) after (\[0-9]+) tries and (\[0-9]+)" {
                 set tries  $expect_out(2,string)
                 set secs   $expect_out(3,string)
            }
        }
        exp_continue
    }

    "elapsed seconds" 
}

puts "I finished your game in about $secs seconds using $tries tries"

spawn 函数执行您要自动化的程序。它将命令作为单独的字符串,后跟要传递给它的参数。我设置了要猜测的初始数字,真正的乐趣开始了。expect 语句相当复杂,并说明了这个实用程序的威力。请注意,这里没有循环语句来迭代提示。因为我的游戏有可预测的提示,所以我可以要求 expect 为我做更多处理。外部 expect 尝试匹配游戏输入提示 `==>`。看到它,它使用 send 进行猜测,然后使用额外的 expect 来找出猜测的结果。根据输出,调整和计算变量以设置下一个猜测。当提示 `==>` 被匹配时,调用 exp_continue 语句。这导致重新评估外部 expect。因此不再需要循环。

此输入处理依赖于 Expect 处理的另一种行为。Expect 缓冲终端输出,直到它匹配一个模式。此缓冲包括任何嵌入的行尾和其他不可打印字符。这与您在 Awk 和 Perl 中习惯的典型正则表达式行匹配不同。当模式匹配时,匹配后出现的任何内容都保留在缓冲区中。它可以用于下一次匹配尝试。我已经利用了这一点来干净地结束外部 expect 语句

-re "value (\[0-9]+) after (\[0-9]+) tries and (\[0-9]+)"

您可以看到内部模式匹配了正确的猜测,并且没有消耗游戏打印的所有字符。字符串的最后一部分(经过的秒数)在成功猜测后仍然被缓冲。在下一次评估外部 expect 时,此字符串从缓冲区中匹配以干净地结束(未提供任何操作)。现在开始有趣的部分,让我们运行完整的自动化

$ ./automate.exp 
spawn ./numgame.exp
I'm starting to guess with the number 99
Guess a number between 1 and 100
==> 99
Read in: 99
Too large, try again
==> 49
Read in: 49
Too small, try again
==> 74
Read in: 74
Too large, try again
==> 61
Read in: 61
Too small, try again
==> 67
Read in: 67
That's right!
You guessed value 67 after 5 tries and 0 elapsed seconds
I finished your game in about 0 seconds using 5 tries

哇!感谢自动化,我的猜数字效率显着提高!几次试运行的平均猜测次数在 5-8 次之间。它也总是在 1 秒内完成。既然这种麻烦、耗时的乐趣可以如此迅速地解决,我就没有理由拖延其他更重要的任务,比如进行家居装修项目 :P

永不止步地学习

本文篇幅较长,但非常值得付出努力。猜数字游戏为演示更有趣的 Expect 处理示例提供了良好的基础。我从练习中学到了很多,并且成功完成了我的工作自动化。我希望您觉得这个编程示例有趣,并且它有助于您进一步实现自动化目标。

标签
James Farrell
我是一位资深的 UNIX 系统管理员和开源倡导者。近年来,我的主要重点一直是 Linux 和 FreeBSD 系统管理、网络、电信和 SAN/存储管理。我喜欢构建基础设施、将系统连接在一起、创建流程以及将人们聚集在一起以支持他们的技术工作。

评论已关闭。

© . All rights reserved.