在C和C++中使用Expect库:全面指南
1. 概述
通常,我们会在面向命令的Tcl环境中使用Expect。不过,即使不借助Tcl,也能够运用Expect。接下来,将介绍如何通过Expect库在C语言里调用Expect函数。这个库同样兼容C++,并且大部分示例在C和C++环境下是一致的。要是你想在其他语言里使用,就得自行探索了。但如果你清楚如何在自己喜欢的语言里调用C例程,那在这些语言里使用Expect应该也没问题。
Expect库中的很多函数和Expect程序里的对应函数工作方式类似。所以,在使用这个库之前,要是有一些使用Expect的经验,会非常有帮助。像生成进程、通配符模式与正则表达式这类概念,这里就不详细解释了。
需要说明的是,这里并非鼓励大家使用C或C++。特别是对于类似Expect的编程任务,在Tcl环境下操作会比C或C++容易得多,毕竟C和C++有编辑 - 编译 - 调试这样的常规循环。和典型的编译程序不同,调试Expect程序时,大部分工作不是让编译器接受程序,而是确保对话逻辑正确,而在Tcl里做这件事要快得多。
要是你清楚C、C++和Tcl之间的权衡,并且有充分理由使用这个库,那就大胆去做。但要是你不了解Tcl,只是不想再学一门新语言,我建议你重新审视自己的决定,给Tcl一个机会,它的功能远超你的想象。
对于很多任务来说,Tcl环境比C和C++更合适。不过,要是你已经有大量使用其他机制来控制符号表和程序流程的软件,那可能就不得不使用C和C++了。
2. 调用Expect函数
从C和C++调用Expect很简单。由于大部分示例和解释在两种环境下一样,后面就不再特别提及C++了。Expect以库的形式存在,你可以把它和其他目标文件链接起来。这个库包含三种类型的函数:
- 操作伪终端(pty)和进程
- 使用模式等待输入
- 断开控制终端的连接
这些函数不一定要一起使用。比如,你可以让Expect生成一个进程,然后直接从这个进程读取数据(不使用模式)。此外,你还能使用Tcl库中的函数(像正则表达式模式匹配器)或者Expect扩展库。
不过,Expect程序里的一些功能在库中没有对应的实现。换句话说,Expect库并没有为所有的Expect命令提供替代方案。例如,没有
send
函数,因为你可以用
fprintf
或
write
来实现相同的功能。
下面是一个创建并与进程交互的简单示例:
FILE *fp = exp-popen("chess");
exp_fexpectl(fp,exp_glob,"Chess\r\n",0,exp_end);
fprintf(fp, "first\r");
第一行代码运行了
chess
程序,返回一个
FILE
指针,这样你就能和这个进程进行交互。
exp_fexpectl
函数声明了一个通配符风格的模式,程序会等待这个模式出现。最后一行代码向进程发送了一个字符串。
3. 链接程序
要把程序和Expect库链接起来,只需要指定合适的库名就行。Expect库名为
libexpect.a
,在链接程序时必须提供这个库。假设这个库已经安装在你的系统中,链接器能够找到它,在命令行中通常用
-lexpect
来引用这个库。
例如,如果你的程序由
foo.o
和
bar.o
这两个目标文件组成,可以这样链接:
cc foo.c bar.c -lexpect -ltcl
这里也列出了Tcl库。Expect库“借用”了Tcl的正则表达式模式匹配器,但没有借用其他部分。为了避免再提供一个模式匹配器的副本,直接链接Tcl库会更方便。当然,如果你提供一个正则表达式模式匹配器的替代品,也可以完全不使用Tcl库。
当这个库和其他语言一起使用时,可能需要用不同的模式匹配器来替换通配符或正则表达式模式匹配器。比如,其他语言定义的正则表达式可能和Tcl不同,把Tcl的正则表达式和其他语言的混用会让人困惑。可惜的是,模式匹配器没有标准接口。一个简单的解决办法是,用遵循Expect库使用的任何接口的新调用来替换对模式匹配器的调用。由于这些接口可能会改变,所以没有正式的文档说明。不过,当前的接口相对简单,熟悉流行UNIX模式匹配器的人应该很容易理解。
4. 包含文件
任何引用Expect库的文件都必须包含以下语句,并且这个语句要在调用任何Expect函数之前出现:
#include "expect.h"
这个语句对C和C++程序都适用。
Expect库的一些函数会和C标准I/O包一起工作。如果你使用了库的这些部分,还必须包含相应的头文件:
#include <stdio.h>
如果编译器需要知道Expect包含文件的位置,要在编译命令中添加合适的参数。例如,编译
foo.c
时,可能需要这样写:
cc -I/usr/local/include foo.c
具体的文件名取决于包含文件的安装位置。和前面一样,Tcl的包含文件也应该能被找到。通常,Expect和Tcl的包含文件在同一个目录下,所以一个
-I
标志就够了。
5. 伪终端和进程
Expect库提供了三个函数来启动新的交互式进程,每个函数都会创建一个新进程,这样当前进程就可以读写新进程的标准输入、标准输出和标准错误。
-
exp_spawnl
:当编译时就知道参数数量时,这个函数很有用。
-
exp_spawnv
:当编译时不知道参数数量时,用这个函数更合适。
-
exp-popen
:稍后会详细介绍。
在这两种情况下,参数都是直接传递的,不会进行shell模式匹配,也不会发生重定向,也就是说不会涉及shell。有时候会把这些函数统称为生成函数。
exp_spawnl
和
exp_spawnv
分别与UNIX函数
execlp
和
execvp
类似,调用序列如下:
int exp_spawnl(file, arg0 [, arg1, ... , argn] (char *)0);
char *file;
char *arg0, *arg1, ... *argn;
int exp_spawnv(file,argv);
char *file, *argv[];
在这两个函数中,
file
参数是一个相对或绝对的文件规范,不会进行特殊字符处理(比如
-
或
*
的扩展)。
exp_spawnl
和
exp_spawnv
会像shell一样,从与
PATH
环境变量关联的目录列表中搜索可执行文件。
exp_spawnv
中的
argv
参数会作为新进程
main
函数中的
argv
参数使用。
exp_spawnl
会收集其余的参数,然后进行处理,让它们也作为
main
函数中的
argv
参数。在这两种情况下,参数都会被复制,所以你之后改变指针或指针指向的内容,不会影响生成的进程。
例如,下面的命令会启动一个到
uunet.uu.net
的SMTP端口的
telnet
进程:
fd = exp_spawnl ( "telnet ", "telnet","uunet.uu.net","smtp", (char *)0);
要注意,
arg0
参数和
file
参数是一样的。要记住,
file
参数不是新
main
函数中
argv
数组的一部分,新进程中
argv[0]
来自当前进程的
arg0
。
在
exp_spawnl
和
exp_spawnv
中,参数列表必须以
(char *) 0
结尾。忘记加这个结尾是常见的错误,通常会导致核心转储。
如果函数调用成功,会返回一个文件描述符,这个文件描述符对应新进程的标准输入、标准输出和标准错误。你可以用
write
系统调用向标准输入写入数据:
write (fd, "foo\r", 4);
要从标准输出或标准错误读取数据,可以使用
read
系统调用:
read(fd,buffer,BUFSIZ);
可以用
fdopen
将一个流与文件描述符关联起来。在几乎所有情况下,你都想立即取消新流的缓冲:
fp = fdopen(fd,"r+");
setbuf(fp, (char *)0);
如果
exp_spawnl
或
exp_spawnv
过程中出现错误,会返回
-1
,并适当地设置
errno
。生成函数分叉后出现的错误(比如尝试生成一个不存在的程序)会被写入生成进程的标准错误,并在第一次读取时被读取到。
C库中的
popen
函数接受一个shell命令行,运行它,并返回一个与之关联的流。但可惜的是,你只能选择从进程读取数据或者向进程写入数据,不能同时进行。
Expect库定义了
exp-popen
,它的风格和
popen
类似。
exp-popen
接受一个Bourne shell命令行,并返回一个对应新进程的标准输入、标准输出和标准错误的流。命令行上会进行重定向和shell模式匹配。和
popen
不同,
exp-popen
不需要类型标志。
popen
使用的是只支持单向通信的管道,而
exp-popen
使用的是支持双向通信的伪终端。比较一下
popen
和
exp-popen
的声明:
FILE *popen(command, type)
char * command , *type;
FILE *exp-popen(command)
char * command;
下面的语句会生成一个
telnet
进程,列出了一些文件参数,并对标准错误进行了重定向:
FILE *fp;
fp = exp-popen("telnet host smtp *.c 2> /dev/null");
因为
exp-popen
返回的是一个流,所以你可以使用任何标准I/O函数来访问它。例如,可以用
fwrite
、
fprintf
、
fputc
等函数向流中写入数据,下面是一个使用
fprintf
的例子:
char *my_name = "Don";
fprintf (fp, "My name is %s\r" ,my_name);
可以用
fread
、
fscanf
、
fgetc
等函数从流中读取数据,下面是一个使用
fgets
的例子:
char buffer[100];
fgets(buffer,100,fp);
exp-popen
的实际实现是基于
exp_spawnl
的,如下所示:
FILE *
exp-popen(program)
char *program;
{
FILE *fp;
int ec;
ec = exp_spawnl("sh","sh","-c",program, (char *)0);
if (0 > ec) return(0);
fp = fdopen(ec,"r+");
if (fp) setbuf(fp, (char *)0);
return fp;
}
包含
expect.h
后会有几个可用的变量,这些变量不需要定义或声明,但可以读写。其中两个变量是生成函数的副作用,分别是:
extern int exp-pid;
extern char *exp-pty_slave_name;
exp-pid
变量包含生成函数创建的进程的进程ID。每次调用生成函数时,
exp-pid
变量都会被重写,所以通常应该立即把它保存到另一个变量中。
生成函数使用伪终端与进程进行通信,
exp-pty_slave_name
变量是与每个生成进程关联的伪终端从设备的名称。换句话说,
exp-pty_slave_name
是生成进程用于标准输入、标准输出和标准错误的终端设备名称。
下面是一个生成
cat
程序并打印出生成进程的进程ID和终端设备名称的程序:
#include <stdio.h>
#include "expect.h"
main () {
FILE *fp = exp-popen("cat");
printf ("pid = %d\n", exp-pid);
printf ("pty name = %s\n", exp-pty_slave_name);
}
在我的系统上运行这个程序时,输出如下:
pid = 18804
pty name = /dev/ttyp3
还有几个变量可以控制生成函数的某些方面,分别是:
extern int exp_console;
extern int exp_ttyinit;
extern int exp_ttycopy;
extern char *exp_stty_init;
默认情况下,如果环境中有控制终端,伪终端会以和用户终端相同的方式初始化。只有当
exp_ttycopy
变量不为零时,才会进行这种初始化,而这个变量默认是不为零的。
如果
exp_ttyinit
变量不为零,伪终端会进一步初始化为系统范围的默认设置,这个默认设置通常和
stty sane
类似,
exp_ttyinit
默认也是不为零的。
可以通过设置
exp_stty_init
变量来进一步修改终端设置,这个变量的解释方式和
stty
参数类似。例如,下面的语句会重复默认的初始化操作。如果
exp_stty_init
设置为0,则不会进行额外的初始化。
这三种初始化操作看起来可能有些多余,但它们解决了很多问题。
exp_console
变量尝试将新的伪终端与控制台关联起来。如果关联成功,发送到控制台的任何消息都会被发送到伪终端,并可以作为进程的输出读取。
如果你的系统支持
environ
变量,可以用它来控制生成进程的环境,应该这样声明:
extern char **environ;
environ
变量是一个字符指针数组,指向表示环境变量的字符串。当生成一个新进程时,
environ
数组会被复制到新进程中,成为新进程的环境。你可以在生成进程之前修改
environ
表,这样生成的进程就会得到修改后的环境。
进程的大多数其他属性会根据
exec
系列函数的“常规”规则继承,包括用户ID、组ID、当前工作目录等。被捕获的信号会重置为默认操作。最后,进程会被放入一个新的进程组,并拥有一个新的会话ID。
可以通过定义
exp_child_exec-prelude
来在新程序获得控制权(通过
exec
函数)之前,在子进程的上下文中更改属性。例如,你可能希望子进程忽略
SIGHUP
和
SIGTERM
,可以这样做:
void exp_child_exec-prelude()
{
signal (SIGHUP, SIG_IGN);
signal (SIGTERM, SIG_IGN);
}
6. 分配自己的伪终端
默认情况下,每次生成进程时都会自动分配一个伪终端。你也可以通过其他机制(自己实现的)来分配伪终端。理论上,你还可以使用一对FIFO或类似的东西,尽管它们可能无法完全模拟终端的功能。
有两个变量可以控制伪终端的分配:
extern int exp_autoallocpty;
extern int exp-pty[2];
exp_autoallocpty
变量默认设置为1。如果你把它设置为0,生成函数就不会自动分配伪终端。相反,
exp-pty[0]
的值会作为主伪终端文件描述符,
exp-pty[1]
的值会作为从伪终端文件描述符。
下面是一个使用
pipe
系统调用进行伪终端分配的示例:
exp_autoallocpty = 0;
pipe (exp-pty);
exp-popen("cat");
当你自己分配伪终端时,还必须对其进行初始化。生成函数不会进行通常的伪终端初始化操作(例如,不会使用
exp_stty_init
)。
新进程创建后,当前进程会关闭从伪终端文件描述符,生成的进程会关闭主伪终端文件描述符。在当前进程的上下文中,所有后续通信都通过主伪终端文件描述符(即
exp-pty[0]
)进行。
无论你是否自己分配伪终端,新进程可能都需要关闭文件描述符。默认情况下,生成函数创建的进程的所有文件描述符都被标记为在执行时关闭,这确保了生成进程中主伪终端文件描述符的关闭行为。其他与生成无关的文件描述符也应该标记为在执行时关闭,以便它们可以自动关闭。或者,你可以将函数指针
exp_close_in_child
设置为一个关闭额外文件描述符的函数,默认情况下,
exp_close_in_child
为0。
void (*exp_close_in_child) ();
使用Tcl(无论是否使用Expect)时,可以使用
exp_close_tcl_files
函数关闭所有高于标准输入、标准输出和标准错误的文件,直到Tcl知道的最高描述符,这正是Expect所做的。可以通过以下语句启用此行为:
// 启用关闭Tcl文件的行为
这种行为比较简单,但通常足够了。更复杂的解决方案需要深入研究Tcl内部机制,这超出了本文的范围。
7. 关闭与生成进程的连接
Expect库没有提供专门的函数来关闭与生成进程的连接。通常,调用
close
就足够了。如果你已经将文件描述符转换为流(或者使用了返回流的
exp-popen
),则调用
fclose
。
进程退出后,应该等待它结束,以释放进程槽。方便的时候,你可以使用任何
wait
系列的调用来等待进程。你也可以在等待之前捕获
SIGCHLD
信号,或者完全忽略
SIGCHLD
信号。关于这方面的更多讨论可以在任何UNIX系统编程的书籍中找到。
有些进程在标准输入关闭时不会自动终止,你可能需要向它们发送显式的退出命令,或者直接杀死它们。前面提到的
exp-pid
变量提供了最近生成的进程的进程ID。
与
popen
和
pclose
不同,
exp-popen
没有对应的
exp-pclose
函数。关闭连接只需要两个操作(
fclose
然后等待进程ID),但这两个操作通常会间隔很长时间,所以为此提供一个新函数没有太大价值。只需使用
fclose
关闭流并等待进程结束即可。
8. Expect命令
库中提供了可用于读取文件或流的函数。和Expect程序的
expect
命令一样,这些库函数会等待模式出现或特殊事件发生。
有四个函数可以进行类似
expect
的处理,其中两个用于处理文件描述符,另外两个用于处理流。每个类型中,一个函数接受类似
exp_spawnl
的参数列表,另一个接受类似
exp_spawnv
的单个可变参数描述符。有时会将这些函数统称为“
expect
函数”,如下表所示:
| | 文件描述符 | 流 |
| — | — | — |
| 参数列表 | expectl | fexpectl |
| 单个可变参数描述符 | expectv | fexpectv |
实际使用时,每个函数都要加上
exp_
前缀。这些名称很容易记忆,和
exp_spawnl
与
exp_spawnv
类似,以
l
结尾的
expect
函数接受参数列表,以
v
结尾的函数接受单个可变描述符。
f
表示该函数读取流,否则读取文件描述符。
例如,
exp_expectl
函数接受一个参数列表并从文件描述符读取数据,一个简单的例子如下:
exp_expect1(fd, exp_glob, "prompt*", 1, exp_end);
这个调用会等待从文件描述符
fd
收到一个以
prompt
开头的提示信息。
exp_glob
是模式类型,表示
prompt*
是一个通配符模式。当匹配到该模式时,
exp_expectl
返回值
1
。
可以通过向参数列表中添加更多模式来同时等待多个模式,示例如下:
exp_expect1(fd, exp_glob, "prompt*", 1,
exp_glob, "another pattern", 2,
exp_glob, "and another", 3,
exp_end);
参数列表的结尾总是用
exp_end
标记。
exp_end
和
exp_glob
是类型为
enum exp_type
的预定义常量,包含
expect.h
时会自动定义这个枚举类型,其公共定义如下:
enum exp_type {
exp_end, // 占位符 - 无更多情况
exp_glob, // 通配符风格
exp_exact, // 精确字符串
exp_regexp, // 正则表达式风格,未编译
exp_compiled, // 正则表达式风格,已编译
exp_null // 匹配二进制0
};
exp_expectl
返回的值是匹配模式后面的数字,在上面的例子中,
1
、
2
和
3
的选择是任意的,你可以为多个模式关联相同的数字。在实际代码中,除非这些数字有内在含义,否则最好使用预处理器定义来隐藏这些数值。
下面是一个在
telnet
对话中查找成功登录信息的例子:
switch (exp_expect1(
exp_glob, "connected", CONN,
exp_glob, "busy", BUSY,
exp_glob, "failed", ABORT,
exp_glob, "invalid password", ABORT,
exp_end)) {
case CONN:
// 成功登录
break;
case BUSY:
// 此时无法登录
break;
case ABORT:
// 任何时候都无法登录!
break;
case EXP_TIMEOUT:
break;
}
如果
expect
函数超时,会返回
EXP_TIMEOUT
,注意这个值不会出现在模式列表中。和Expect程序不同,这里不需要专门请求处理超时情况,
expect
函数不会自动执行操作,只是描述发生了什么,所以即使你没有请求处理超时,也不会错过它。
超时的秒数由整数变量
exp_timeout
决定。如果
exp_timeout
为
-1
,则超时时间实际上是无限的,永远不会超时。超时时间为
0
用于轮询,后面会详细介绍。
还有三个特殊值可能会被返回:
-
EXP_EOF
:在文件结束时返回。
-
-1
:如果系统调用失败或发生其他严重错误,会返回
-1
。例如,如果内部内存分配失败,会返回
-1
并将
errno
设置为
ENOMEM
。只要返回
-1
,
errno
就会被设置。
-
EXP_FULLBUFFER
:如果整数变量
exp_full_buffer
不为零,当
expect
函数的缓冲区满时会返回
EXP_FULLBUFFER
。如果
exp_full_buffer
为零且缓冲区满了,会丢弃缓冲区的前半部分,将后半部分复制下来,然后
expect
函数继续执行。
所有这些特殊值都是小的负整数,所以最好将模式与正整数关联起来,尽管代码中并没有强制要求这样做。
9. 正则表达式模式
可以使用
exp_regexp
来识别正则表达式模式。将前面的例子重写为使用正则表达式模式如下:
exp_expectl(fd, exp_regexp, "prompt.*", 1, exp_end);
所有模式的类型都需要显式标识,这样不同类型的模式就可以混合使用而不会产生混淆。下面是一个同时包含通配符和正则表达式模式的
expect
调用示例:
exp_expectl(fd, exp_regexp, "prompt.*", 1,
exp_glob, "another pattern", 2,
exp_regexp, "and another", 3,
exp_end);
10. 缓存正则表达式
Expect使用的正则表达式实现会将模式转换为一种内部形式,这样可以非常快速地测试字符串。这个转换过程称为编译。编译本身可能比后续模式匹配节省的时间还要长,但如果模式会被多次使用,编译最终可以节省大量时间。
在前面的例子中,
expect
函数会在内部编译正则表达式。由于输入通常来得比较慢,模式会被多次评估,编译过程会带来好处并节省时间。然而,每次
expect
函数结束时,编译后的形式都会被丢弃。如果函数处于一个紧密的循环中,这可能会造成浪费。
你可以使用
exp_compiled
而不是
exp_regexp
将编译后的形式传递给
expect
函数。假设编译后的形式存储在
fastprompt
中,前面的例子可以重写如下:
exp_expectl(fd, exp_compiled, "prompt.*", fastprompt, 1, exp_end);
这里仍然传递了字符串形式的模式,但它仅用于记录和调试,实际的模式匹配使用的是编译后的形式。
使用
TclRegComp
函数来编译模式,它接受一个模式并返回类型为
regexp
指针(一个
typedef
)的编译后形式。下面是在循环中使用它的示例:
#define PROMPTED 17
char *pat = "prompt.*";
regexp *fastprompt = TclRegComp(pat);
while (1) {
switch (exp_expectl(fd,
exp_compiled, pat, fastprompt, PROMPTED,
exp_end)) {
case PROMPTED:
// 响应提示
break;
// 其他情况
}
}
当不再需要编译后的形式时,使用
free
释放其占用的内存:
free((char *)fastprompt);
格式错误的模式无法编译。如果
TclRegComp
返回
0
,则编译失败。变量
tclRegexpError
包含描述问题的错误消息,用C语言表示如下:
fastprompt = TclRegComp(pat);
if (fastprompt == NULL) {
fprintf(stderr, "regular expresion %s is bad: %s", pat, tclRegexpError);
}
11. 精确匹配
模式类型
exp_exact
用于标识必须在输入中精确匹配的模式。需要遵守通常的C转义规则,并且没有字符会被解释为通配符或锚点,精确模式是无锚点的。例如:
exp_expectl(fd, exp_exact, "#*!$", 1, exp_end);
只有当字符流中包含连续的字符序列
#
、
*
、
!
和
$
时,上述示例才会返回
1
。
12. 匹配空字符
默认情况下,生成进程的输出中会自动去除空字符(值为零的字节)。可以通过将整数变量
exp_remove_nulls
设置为
0
来禁用此功能,设置为
1
则重新启用。
禁用空字符去除功能后,可以使用
exp_null
来匹配空字符。仍然需要传递字符串形式的模式,但它仅用于记录和调试。例如:
exp_expectl (fd,
exp_null, "zero byte", 1,
exp_end);
13. 匹配的字符
当
expect
函数返回时,变量
exp_buffer
指向正在考虑进行匹配的字符缓冲区,
exp_buffer_end
指向缓冲区末尾的下一个字符,缓冲区以空字符结尾。如果模式匹配成功,变量
exp_match
会指向同一缓冲区中模式首次匹配的位置,
exp_match_end
指向最后一个匹配字符的下一个位置。这些变量都是字符指针:
char *exp_buffer;
char *exp_buffer_end;
char *exp_match;
char *exp_match_end;
下面的代码片段展示了如何打印出所有匹配信息。在循环中,临时将子匹配项以空字符结尾,以便可以打印出来。为了避免破坏字符串,会临时保存要写入空字符的位置的字符,然后再恢复:
exp_expectl(fd,
exp_compiled, pattern, regexp, 1,
exp_end);
for (i = 0; i < NSUBEXP; i++) {
char save;
if (regexp->startp[i] == 0) continue;
// 临时将匹配项以空字符结尾
save = regexp->endp[i];
regexp->endp[i] = '\0';
printf("match [%d] = %s\n", i, regexp->startp[i]);
// 恢复原来的字符
regexp->endp[i] = save;
}
expect
函数会根据需要自动为
exp_buffer
分配空间。变量
exp_match_max
是一个整数,描述了保证能匹配的字符串的最大长度,默认情况下,
exp_match_max
为
2000
。
14. 模式数量未知时的处理
当模式列表在事先已知,并且至少知道模式的数量时,
exp_expectl
函数是合适的。当模式数量可能变化时,
exp_fexpectv
函数更合适。这个函数只接受两个参数,第一个是文件描述符,第二个是模式描述符数组,其原型如下:
int exp_expectv(int fd, struct exp_case *pats);
struct exp_case
的定义如下:
struct exp_case {
char *pattern;
regexp *re;
enum exp_type type;
int value;
};
exp_case
结构中的信息与
exp_expectl
中直接传递的参数信息完全相同。模式存储在
pattern
中,可选的编译后的正则表达式存储在
re
中。
type
元素描述模式的类型,是
exp_type
枚举常量。和之前一样,最后一个模式类型必须是
exp_end
。最后,
value
是关联模式匹配时返回的整数。
当模式类型为
exp_regexp
时,
exp_expectv
的工作方式与
exp_expectl
略有不同。在这种情况下,
exp_expectv
会编译每个模式,并将编译后的形式存储在
re
中。如果再次使用相同的模式调用
exp_expectv
,编译后的形式会保留在
exp_case
结构中供你使用或重用。如果类型是
exp_regexp
,
exp_expectv
会在编译模式之前检查
re
是否已初始化,只有当
re
未初始化时才会编译模式。
当你使用完正则表达式后,必须释放每个包含
regexp
的
exp_case
中的
re
,无论它是你自己编译的还是
exp_expectv
编译的。
15. 从流中进行期望匹配
exp_expectl
和
exp_expectv
都有对应的处理流的类似函数,这些流函数的名称与它们处理文件描述符的对应函数相同,只是流函数的名称中包含一个
f
,这类似于
write
和
fwrite
的区别。这两个流版本的函数与它们处理文件描述符的对应函数相同,只是第一个参数是流而不是文件描述符。
exp_fexpectl
是
exp_expectl
的流版本,一个简单的例子如下:
FILE *fp = exp-popen("telnet");
exp_fexpectl(fp, exp_glob, "prompt*", 1, exp_end);
在某些系统上,
expect
函数的流版本比文件描述符版本慢得多,因为没有一种可移植的方法可以在不超时的情况下读取未知数量的字节,因此字符是逐个读取的。虽然交互式程序的自动化版本通常不需要很高的速度,但文件描述符函数在所有系统上可能更高效。
你可以同时获得两者的优点,使用通常的流函数(如
fprintf
)进行写入,使用
expect
函数的文件描述符版本进行读取,只要你不尝试混合使用其他流输入函数(如
fgetc
)。为此,将
fileno (stream)
作为文件描述符传递给
exp_expectl
或
exp_expectv
。幸运的是,从交互式程序读取时,除了
expect
函数,几乎没有理由使用其他函数。
16. 后台运行
有时候需要在进程开始运行后将其移到后台,典型的应用场景是读取密码,然后在后台休眠,之后再使用这些密码进行实际工作。
将进程移到后台比较复杂,而且不同系统的实现方式也不同。不过,Expect在生成例程中集成了这个功能。由于对于使用Expect库的程序来说,将进程移到后台是一项常见任务,所以它通过一个单独的接口提供。
要将进程移到后台,可以通过以下步骤实现:
switch (fork()) {
case 0: /* 子进程 */
exp_disconnect();
break;
case -1: /* 错误 */
perror ( " fork" );
default: /* 父进程 */
exit (0);
}
调用
exp_disconnect
会使进程与控制终端分离。如果你想以不同的方式将进程移到后台,必须将整数变量
exp_disconnected
设置为
1
(初始值为
0
),这样可以确保从这一点之后生成的进程能够正确启动。
exp_disconnect
函数会将
exp_disconnected
设置为
1
。
exp_disconnected
变量也会与Expect程序共享。如果你调用Expect的
disconnect
命令,它也会将
exp_disconnected
设置为
1
。
17. 处理多个输入和更多关于超时的内容
在某些情况下,你可能不想在
expect
函数内部等待,而是在
select
、
poll
或窗口系统提供的事件管理器中等待。这时,将生成进程对应的文件描述符交给事件循环。当事件循环检测到可以从文件描述符读取输入时,会以某种方式回调,之后你可以调用
expect
函数来测试模式。
可以通过将
exp_timeout
设置为
0
来确保
expect
函数立即返回。如果没有任何模式匹配,将返回
EXP_TIMEOUT
。
下面是一个使用
select
系统调用的例子:
#define WAIT_FOREVER (struct timeval *)0
FD_SET(fd,&rdrs);
select (fd_max, &rdrs, ... , WAIT_FOREVER);
exp_timeout = 0;
exp_expectl(fd, ... , exp_end);
如果
exp_timeout
不为零,
expect
函数在从单个文件描述符读取数据时可能会在
read
系统调用中阻塞。内部会使用
ALARM
信号来中断读取。如果你定义了信号处理程序,可以选择重启或中止读取。整数变量
exp_reading
只有在读取被中断时为
1
,否则为
0
。以下语句可以中止读取:
longjmp(exp_readenv, EXP_ABORT);
以下语句可以重启读取:
longjmp(exp_readenv, EXP_RESTART);
18. 输出和调试杂项
库中存在一些输出和调试控制选项,但它们的种类和灵活性不是很大,因为在这个领域没有太多的开发需求。例如,交互调试通常使用Tcl而不是C进行。
这些控制选项与Expect程序和扩展中的命令相对应,通过以下变量进行操作,所有变量默认值为
0
:
int exp_loguser;
int exp_logfile_all;
FILE *exp_logfile;
int exp_is_debugging;
FILE *exp_debugfile;
-
如果
exp_loguser不为零,expect函数会将生成进程的任何输出发送到标准输出。由于交互式程序通常会回显输入,这通常足以显示对话的双方。 -
如果
exp_logfile也不为零,相同的输出会被写入exp_logfile定义的流中。 -
如果
exp_logfile_all不为零,无论exp_loguser的值如何,都会将输出写入exp_logfile。 -
当
exp_is_debugging不为零时,Expect内部的调试信息会发送到标准错误。调试信息包括接收到的每个字符以及每次将当前输入与模式进行匹配的尝试。此外,不可打印字符会转换为可打印形式,例如,控制 - C会显示为脱字符后跟C。如果exp_logfile不为零,此信息也会写入exp_logfile。 -
如果
exp_debugfile不为零并设置为流指针,所有正常和调试信息都会写入该流,而不管exp_is_debugging的值如何。
所有这些变量直接控制Expect程序和扩展中的对应项。例如,Expect命令
log_user 1
会将
exp_loguser
的值设置为
1
。
19. 伪终端捕获
某些系统(特别是HP系统)要求捕获伪终端,以便通过
select
或
poll
检测到文件结束(EOF)。启用捕获后,生成进程在伪终端上执行的所有
ioctl
操作都必须得到确认。当Expect处于其
expect
函数之一时,通常会自动执行此确认操作。但有时,你可能需要显式处理捕获。例如,你可能想在启动从伪终端后更改其模式。
捕获和确认协议在你的系统文档中有描述,这里不再赘述,因为可以避免使用它们。幸运的是,这不是因为它们复杂,而是因为在你进行其他操作时(例如在
ioctl
调用中间)无法执行它们。解决方案是暂时禁用捕获。
可以使用
exp_slave_control
控制捕获,第一个参数是与生成进程对应的文件描述符,第二个参数为
0
表示禁用捕获,为
1
表示启用捕获:
/* 禁用捕获 */
exp_slave_control(fd, 0);
/* 调整伪终端模式 */
/* 启用捕获 */
exp_slave_control(fd, 1);
在不使用捕获的系统上,
exp_trap_control
会变成一个空操作。因此,如果你担心程序在需要捕获的系统上的可移植性,建议使用捕获控制函数。
20. 练习
为了更好地掌握在C和C++中使用Expect库的技巧,以下提供了几个练习供你实践:
1.
对比编程体验
:
- 首先,使用Expect库编写一个程序。
- 然后,使用Expect程序重写这个程序。
- 比较编写(和调试)这两个程序所花费的时间。
- 比较两个程序源代码的大小。
- 比较生成的可执行文件的大小。
- 从这些比较中,你能得出什么结论?可以尝试在一个更大规模的示例上重复这个练习,进一步验证你的结论。
2.
创建FTP优化库
:创建一个专门为FTP优化的库,该库应包含启动和停止FTP、发送和期望FTP请求的函数。与原始的
expect
函数相比,能实现多少简化?
3.
创建终端模拟器小部件
:为Tk创建一个终端模拟器小部件。这种小部件与之前介绍的方法相比,有哪些优点和缺点?
通过完成这些练习,你可以更深入地理解和掌握在C和C++中使用Expect库的方法,同时也能更好地体会不同编程方式的优缺点。希望你能从这些练习中获得宝贵的经验,提升自己的编程能力。
在C和C++中使用Expect库:全面指南
21. 总结与回顾
在前面的内容中,我们详细探讨了在C和C++中使用Expect库的各个方面。从基本的概述,我们了解到虽然通常在Tcl环境中使用Expect,但不借助Tcl也能使用它。并且明确了使用该库前有使用Expect的经验会更有帮助,同时也分析了C、C++与Tcl在编程上的优劣。
在调用Expect函数部分,我们知道了Expect库包含操作伪终端和进程、使用模式等待输入、断开控制终端连接这三种类型的函数,还通过简单示例展示了如何创建并与进程交互。
链接程序时,我们需要指定合适的库名,如
-lexpect
和
-ltcl
,并且了解了在与其他语言一起使用时处理模式匹配器的方法。
包含文件方面,要在调用Expect函数前包含
#include "expect.h"
和可能需要的
#include <stdio.h>
,并根据情况设置编译命令中的参数。
伪终端和进程部分,介绍了
exp_spawnl
、
exp_spawnv
和
exp-popen
这三个启动新交互式进程的函数,以及它们的调用方式、参数传递、错误处理等内容,还说明了相关变量对进程属性和伪终端初始化的控制。
分配自己的伪终端时,通过控制
exp_autoallocpty
和
exp-pty
变量可以实现,同时要注意自己分配时的初始化和文件描述符关闭问题。
关闭与生成进程的连接,通常使用
close
或
fclose
,并在进程退出后进行等待操作。
Expect命令部分,详细介绍了四个进行类似
expect
处理的函数,包括它们的使用方式、模式匹配类型、特殊返回值等,还讲解了正则表达式模式的使用、缓存以及精确匹配、匹配空字符等内容。
从流中进行期望匹配时,流版本的函数与文件描述符版本类似,但在某些系统上速度可能较慢,我们可以通过合理的方式同时利用两者的优点。
后台运行进程可以通过
fork
、
exp_disconnect
等操作实现,并且
exp_disconnected
变量在其中起到了关键的控制作用。
处理多个输入和超时时,我们可以通过
select
等系统调用结合设置
exp_timeout
来实现灵活的等待和处理机制。
输出和调试杂项部分,介绍了通过几个变量来控制输出和调试信息的显示和记录。
伪终端捕获在某些系统上需要注意,我们可以使用
exp_slave_control
来控制捕获的启用和禁用,以提高程序的可移植性。
22. 实际应用场景分析
22.1 自动化网络设备配置
在自动化网络设备配置的场景中,我们可以使用Expect库来实现与网络设备的交互。例如,通过
exp-popen
启动
telnet
或
ssh
进程连接到网络设备,然后使用
expect
函数等待设备的提示信息,根据不同的提示信息发送相应的配置命令。
#include <stdio.h>
#include "expect.h"
#define CONNECTED 1
#define PROMPT 2
int main() {
FILE *fp = exp-popen("telnet 192.168.1.1");
switch (exp_fexpectl(fp, exp_glob, "Connected to", CONNECTED,
exp_glob, "#", PROMPT,
exp_end)) {
case CONNECTED:
fprintf(fp, "username\r");
exp_fexpectl(fp, exp_glob, "Password:", 0, exp_end);
fprintf(fp, "password\r");
break;
case PROMPT:
// 已经登录,直接发送配置命令
fprintf(fp, "configure terminal\r");
break;
}
// 后续可以继续根据提示信息进行更多配置操作
fclose(fp);
return 0;
}
这个示例展示了如何使用Expect库自动登录到网络设备并开始配置过程。通过
expect
函数的模式匹配,我们可以根据不同的提示信息做出相应的响应。
22.2 批量文件传输自动化
在批量文件传输的场景中,我们可以结合FTP协议使用Expect库来实现自动化。例如,创建一个专门为FTP优化的库(如练习中提到的),包含启动和停止FTP、发送和期望FTP请求的函数。
#include <stdio.h>
#include "expect.h"
// 假设已经创建了一个FTP优化库,包含以下函数
extern int ftp_start(const char *server, const char *username, const char *password);
extern int ftp_upload(const char *local_file, const char *remote_file);
extern int ftp_stop();
int main() {
if (ftp_start("ftp.example.com", "user", "pass")) {
if (ftp_upload("local_file.txt", "remote_file.txt")) {
printf("File uploaded successfully.\n");
} else {
printf("File upload failed.\n");
}
ftp_stop();
} else {
printf("FTP connection failed.\n");
}
return 0;
}
这个示例展示了如何使用自定义的FTP优化库来实现文件上传的自动化。通过封装
expect
函数,我们可以更方便地处理FTP连接和文件传输。
22.3 交互式程序自动化测试
在对交互式程序进行自动化测试时,Expect库可以帮助我们模拟用户输入和验证输出。例如,对一个命令行游戏进行测试,我们可以使用
exp_popen
启动游戏进程,然后使用
expect
函数等待游戏的提示信息,根据测试用例发送相应的输入。
#include <stdio.h>
#include "expect.h"
#define START_GAME 1
#define TURN_PROMPT 2
int main() {
FILE *fp = exp-popen("./game");
switch (exp_fexpectl(fp, exp_glob, "Welcome to the game", START_GAME,
exp_glob, "Your turn:", TURN_PROMPT,
exp_end)) {
case START_GAME:
fprintf(fp, "start\r");
break;
case TURN_PROMPT:
// 根据测试用例发送相应的输入
fprintf(fp, "move 1 2\r");
break;
}
// 继续根据游戏的输出进行验证和输入
fclose(fp);
return 0;
}
这个示例展示了如何使用Expect库对交互式程序进行自动化测试。通过模拟用户的输入和验证程序的输出,我们可以快速发现程序中的问题。
23. 性能优化建议
23.1 正则表达式编译优化
在前面提到,正则表达式的编译过程可能会比较耗时,但如果模式会被多次使用,编译最终可以节省大量时间。因此,对于经常使用的正则表达式模式,应该尽量进行编译并缓存。例如:
#define PROMPTED 17
char *pat = "prompt.*";
regexp *fastprompt = TclRegComp(pat);
if (fastprompt == NULL) {
fprintf(stderr, "regular expresion %s is bad: %s", pat, tclRegexpError);
return -1;
}
while (1) {
switch (exp_expectl(fd,
exp_compiled, pat, fastprompt, PROMPTED,
exp_end)) {
case PROMPTED:
// 响应提示
break;
// 其他情况
}
}
free((char *)fastprompt);
通过将正则表达式模式编译并缓存,避免了每次匹配时的重复编译,提高了匹配效率。
23.2 减少不必要的I/O操作
在使用
expect
函数时,尽量减少不必要的I/O操作。例如,避免频繁地从流中读取数据,可以通过合理设置缓冲区和超时时间来优化。同时,尽量使用文件描述符版本的
expect
函数,因为在某些系统上,流版本可能会较慢。
23.3 并行处理多个进程
如果需要同时处理多个进程,可以考虑使用多线程或多进程技术。例如,使用
fork
创建多个子进程,每个子进程处理一个独立的任务。这样可以充分利用系统资源,提高整体性能。
#include <stdio.h>
#include <unistd.h>
#include "expect.h"
void handle_process() {
FILE *fp = exp-popen("some_process");
// 处理进程的逻辑
fclose(fp);
}
int main() {
pid_t pid1 = fork();
if (pid1 == 0) {
handle_process();
return 0;
}
pid_t pid2 = fork();
if (pid2 == 0) {
handle_process();
return 0;
}
// 父进程等待子进程结束
wait(NULL);
wait(NULL);
return 0;
}
这个示例展示了如何使用
fork
创建多个子进程来并行处理任务。
24. 常见问题与解决方案
24.1 模式匹配失败
如果
expect
函数的模式匹配失败,可能有以下原因:
-
模式错误
:检查模式是否正确,特别是正则表达式模式,要确保语法正确。可以使用调试信息(通过设置
exp_is_debugging
)来查看匹配过程中的详细信息。
-
输入数据格式问题
:确保输入数据的格式与模式匹配的要求一致。例如,注意换行符、空格等字符的影响。
-
缓冲区问题
:如果缓冲区满了,可能会导致部分数据丢失。可以通过设置
exp_full_buffer
来控制缓冲区满时的处理方式。
24.2 进程无法正常启动
如果使用
exp_spawnl
、
exp_spawnv
或
exp-popen
启动进程失败,返回
-1
,可以检查以下方面:
-
文件路径问题
:确保要启动的程序的路径正确,并且该程序具有可执行权限。
-
环境变量问题
:检查
environ
变量是否正确设置,确保进程可以在正确的环境中启动。
-
系统资源问题
:如果系统资源不足,可能会导致进程无法启动。可以检查系统的内存、文件描述符等资源使用情况。
24.3 超时问题
如果
expect
函数超时,可能是由于以下原因:
-
网络延迟
:如果是与远程设备进行交互,网络延迟可能会导致数据传输缓慢,从而引起超时。可以适当增加
exp_timeout
的值。
-
程序响应慢
:被交互的程序可能响应较慢,导致
expect
函数等待时间过长。可以检查程序的性能,或者优化交互逻辑。
25. 未来发展趋势
随着计算机技术的不断发展,Expect库在自动化领域的应用可能会更加广泛。以下是一些可能的发展趋势:
-
与新兴技术的结合
:随着人工智能、机器学习等技术的发展,Expect库可能会与这些技术结合,实现更智能的自动化任务。例如,通过机器学习算法预测交互式程序的输出,从而更准确地进行模式匹配和响应。
-
跨平台兼容性的提升
:未来,Expect库可能会进一步提升跨平台兼容性,支持更多的操作系统和硬件平台。这将使得开发者可以在不同的环境中更方便地使用Expect库进行自动化开发。
-
功能的扩展和优化
:可能会增加更多的功能,如更强大的模式匹配算法、更灵活的调试工具等。同时,对现有功能进行优化,提高性能和稳定性。
26. 总结
通过本文的介绍,我们全面了解了在C和C++中使用Expect库的方法和技巧。从基本的使用到实际应用场景,再到性能优化和常见问题解决,我们对Expect库有了更深入的认识。希望读者能够通过实践,掌握这些知识,并在实际项目中灵活运用Expect库,实现高效的自动化任务。同时,也期待Expect库在未来能够不断发展和完善,为开发者提供更多的便利。
最后,不要忘记完成前面提到的练习,通过实践来巩固所学知识,提升自己的编程能力。相信在不断的实践和探索中,你会发现Expect库的更多潜力和价值。
超级会员免费看
768

被折叠的 条评论
为什么被折叠?



