深入探索 Expect:与 Tcl 结合的使用指南
1. 引言
在编程领域,将不同的工具和扩展进行有效结合能够显著提升开发效率和程序的功能。本文将详细介绍如何将 Expect 作为 Tcl 的扩展来使用,以及如何在 Expect 中添加其他 Tcl 扩展,同时还会探讨相关的差异、操作步骤和一些实用的技巧。
2. 将 Expect 添加到基于 Tcl 的程序中
以 tclsh 程序为例,它是随 Tcl 附带的“Tcl shell”,本身没有其他扩展,但可作为创建带有其他扩展的基于 Tcl 程序的模板。以下是具体的操作步骤:
1.
复制模板文件
:在 Tcl 源目录中找到 tclApplni t. c 文件,将其复制到一个新目录。
2.
修改代码
:打开该文件,找到
Tcl_Applnit
函数,在
if (Tcl_Init(interp) == Tcl_ERROR)
这行代码之后,添加初始化 Expect 的代码:
if (Exp_Init(interp) == Tcl_ERROR)
return Tcl_ERROR;
你还可以添加其他扩展的初始化代码,一般来说,扩展初始化的顺序可以任意,但如果它们尝试使用相同的命令名,后添加的扩展会“胜出”。基本的 Tcl 命令实际上是在
Tcl_Init
之前创建的,但
Tcl_Init
仍必须首先出现。其他
xxx_Init
函数通常会为每个扩展定义自己的命令。
3.
包含头文件
:在文件顶部(包含“tcl. h”之后的任意位置)添加以下代码,以包含
Exp_Init
的声明和其他 Expect 定义:
#include "expect_tcl.h"
- 编译文件 :使用以下命令编译 tclApp1nit. c 文件:
cc -I/usr/local/include tclApplnit.c -L/usr/local/lib \
-lexpect -ltcl -lm
你可能需要根据自己的安装情况调整此命令。
-I
标志指定 Tcl、Expect 和其他扩展的头文件所在的目录,
-L
标志列出库文件所在的目录。你可以有多个
-I
和
-L
标志,命令末尾指定特定的库。你需要 Expect 库(
-lexpect
)、Tcl 库(
-ltcl
)以及其他扩展所需的库。大多数系统需要数学库(
-lm
),具体可能因系统而异。如果此命令不起作用,可以查看 Tcl 和 Expect 的 Makefiles 以了解它们在你的系统上使用的库。
5.
生成可执行文件
:如果 tclApp1nit. c 文件编译和链接成功,编译器会在当前目录下生成一个
a.out
文件。这是一个可执行文件,它可以理解 Tcl 命令、Expect 命令以及你定义的任何其他扩展。你可以将其重命名并移动到你想要的位置。
6.
处理 C++ 情况
:如果你使用的是 C++ 或者任何扩展使用了 C++,则需要额外的步骤。必须使用 C 编译 tclApp1nit. c 文件,使用 C++ 进行最终的命令链接,将所有内容组合成一个可执行文件。一些 C++ 编译器要求
main
函数也使用 C++ 编译。由于 Tcl 库提供了默认的
main
函数,你可能需要提取或重新创建它并使用 C++ 进行编译。
3. Expect 作为 Tcl 扩展的差异
当使用添加了
Exp_Init
的 tclsh 或其他程序时,可能会遇到它与 Expect 之间的一些差异,具体如下:
|差异类型|详细说明|
| ---- | ---- |
|命令行参数处理|Expect 定义了
-c
和
-d
等命令行标志的行为,其他程序不太可能支持这些标志。tclsh 仅支持
-f
标志。大多数 Tcl 程序和 Expect 一样,使用
argv
和
argc
变量使其他参数可用于脚本。|
|信号处理|其他扩展可能会尝试与 Expect 同时处理信号,但同一时间只能有一个扩展处理相同的信号。多个扩展可能声称要处理信号,但只会调用其中一个信号处理程序。信号定义应由用户控制,因此这通常不是问题。tclsh 不会自动建立 Expect 使用的默认信号处理程序(如
SIGINT
、
SIGTERM
)。|
|命令名称|与其他扩展中同名的 Expect 命令通常会被抑制(除非其他扩展也抑制了自己的定义)。例如,如果另一个扩展定义了“spawn”,它将覆盖 Expect 的
spawn
命令。为了确保使用 Expect 的命令,可以在其前面加上“exp_”。例如,使用 Tk 时,“send” 是 Tk 的命令,而 “exp_send” 是 Expect 的命令。“exp_” 版本的命令始终可用,即使不使用其他扩展时也可以使用,这样可以避免在不同命令之间切换的烦恼。但对于已经以 “exp” 开头的 Expect 命令(如
expect
),不会提供加前缀的版本,例如不存在 “exp_expect” 命令。|
|退出处理|多个扩展可能会尝试提供退出处理程序和与
exit
命令相关的类似功能,但只能执行一个退出命令。提供
exit
命令的扩展不太可能提供与 Expect 相同的功能或方式。使用 “exit -onexit” 声明的退出处理程序可以使用 “exit -noexit” 调用。终端模式也会被重置,但与普通的 “exit” 不同,控制权会返回,以便可以执行额外的 Tcl 或其他扩展命令。|
|解释器提示|当交互式输入命令时,Expect 使用解释器命令处理它们。tclsh 有自己的解释器,但不能直接作为命令调用。大多数其他扩展不提供交互式命令解释器。Expect 的解释器在大多数方面与 tclsh 的解释器相似,主要的区别在于提示信息。tclsh 的解释器使用
tcl-prompt1
和
tcl-prompt2
变量来指定生成提示信息的函数,而 Expect 的解释器使用
prompt1
和
prompt2
函数直接生成提示信息。如果你运行 tclsh,会看到 Tcl 的提示信息;如果你从 tclsh 中调用
exp_interpreter
,会看到 Expect 的提示信息。|
|.rc 文件|默认情况下,Expect 在启动时会读取多个
.rc
文件,但当将 Expect 作为扩展用于其他程序时,不会读取这些文件。|
4. 向 Expect 添加扩展
向 Expect 添加其他扩展的方式与向 tclsh 添加扩展类似,但这样做可以保留 Expect 的一些特性,如命令行参数处理。具体步骤如下:
1.
复制模板文件
:在 Expect 源目录中找到 exp_main_exp. c 文件,将其复制到一个新目录。
2.
修改代码
:打开该文件,查看
main
函数,会找到以下代码(不一定按此顺序):
if (Tcl_Init(interp) == Tcl_ERROR)
return Tcl_ERROR;
if (Exp_Init(interp) == Tcl_ERROR)
return Tcl_ERROR;
大多数其他扩展可以通过调用
xxx_Init
来添加,其中
xxx
是扩展的前缀,实际调用应类似于 Tcl 和 Expect 的调用。同样,扩展初始化的顺序一般可以任意,但如果它们尝试使用相同的命令名,后添加的扩展会“胜出”。基本的 Tcl 命令在
Tcl_Init
之前创建,其他
xxx_Init
函数通常会为每个扩展定义自己的命令。
3.
包含头文件
:在文件顶部(包含“tcl. h”之后的任意位置)添加适当的头文件包含行,以适应你的扩展。
4.
编译文件
:使用以下命令编译 exp_main_exp. c 文件:
cc -I/usr/local/include exp_main_exp.c ... \
-L/usr/local/lib -lexpect -ltcl -lm
同样,你可能需要根据自己的安装情况调整此命令。
-I
标志指定头文件目录,
-L
标志指定库文件目录,命令末尾指定所需的库。需要 Expect 库(
-lexpect
)、Tcl 库(
-ltcl
)以及其他扩展所需的库,用相应的
.o
文件或库替换 “…”。大多数系统需要数学库(
-lm
),具体可能因系统而异。如果此命令不起作用,可以查看 Tcl 和 Expect 的 Makefiles 以了解它们在你的系统上使用的库。
5.
生成可执行文件
:如果 exp_main_exp. c 文件编译和链接成功,编译器会在当前目录下生成一个
a.out
文件。这是一个可执行文件,它可以理解 Tcl 命令、Expect 命令以及你定义的任何其他扩展。你可以将其重命名并移动到你想要的位置。
6.
处理 C++ 情况
:如果你使用的是 C++ 或者任何扩展使用了 C++,则需要额外的步骤。必须使用 C 编译 exp_main_exp. c 文件,使用 C++ 进行最终的命令链接,将所有内容组合成一个可执行文件。
5. 向 Expectk 添加扩展
向 Expectk 添加扩展与向 Expect 添加扩展非常相似,主要的区别在于:
1.
使用不同的模板文件
:应使用 exp_main_tk. c 模板文件,而不是 exp_main_exp. c。
2.
链接所需的库
:链接时需要 Expectk 和 Tk 库,因此编译命令应如下所示:
cc -I/usr/local/include exp_main_tk.c ... \
-L/usr/local/lib -lexpectk -ltk -ltcl -lX11 -lm
同样,你可能需要根据自己的安装情况调整此命令。如果此命令不起作用,可以查看 Tcl 和 Expect 的 Makefiles 以了解它们在你的系统上使用的库。
6. 创建无脚本的 Expect 程序
通常,Expect 使用脚本来控制其执行,这意味着要运行一个 Expect 应用程序,需要 Expect 解释器和脚本。但可以将脚本和解释器组合在一起,生成一个不依赖于其他文件的单一可执行文件。这个可执行文件可以复制到新的机器上,但这些机器必须是二进制兼容的,并且脚本在新机器上必须有意义。例如,脚本中启动的程序必须在新机器上存在。运行特定脚本的独立可执行文件通常被称为编译后的文件,尽管这不是该词的通常定义。
编译后的脚本体积较大,每个脚本都必须包含脚本本身和 Expect 的可执行代码。如果你在一台计算机上只使用一个 Expect 应用程序,编译后的脚本是有意义的;但如果你使用多个 Expect 应用程序,编译后的脚本会浪费空间。除了空间问题,编译后的脚本和基于文件的脚本在功能上没有显著差异。
要使用 exp_main_exp. c 模板创建编译后的 Expect 脚本,可以将所有对 Expect 解释阶段的调用(如
exp_interpret_cmdfilename
)替换为对
Tcl_Eval
的调用,并将表示文件内容的字符串作为参数传递给
Tcl_Eval
:
Tcl_Eval(interp,cmdstring);
命令的字符串表示必须在可写内存中。一种确保这一点的方法是按以下方式声明:
static char cmdstring[] = "spawn prog; expect .. ";
使用只读内存中的字符串调用
Tcl_Eval
是一个常见的错误。例如,以下声明会使字符串位于只读内存中:
char *cmdstring = "spawn prog; expect .. ";
/* WRONG */
必须从命令字符串中删除任何
source
语句。一些 Tcl 扩展大量使用文件来存储 Tcl 命令,所有这些文件引用都必须像处理脚本文件本身一样被删除。
7. Expect 扩展中的函数和变量
编写使用 Expect 扩展的 C 和 C++ 代码与编写使用 Tcl 的 C 代码类似。例如,你可以调用
Tcl_Eval
来执行任何 Expect 或 Tcl 命令。以下代码展示了如何启动一个 telnet 进程并打印新的
spawn_id
:
char *spawn_id;
char telnet_cmd[] = "spawn telnet";
Tcl_Eval(interp,telnet_cmd);
spawn_id = Tcl_GetVar (interp, "spawn_id", 0);
printf ("spawn id is %s\n", spawn_id);
虽然可以直接调用 Expect 的命令,但这有点困难,并且通常没有很好的理由这样做,因此这里不做详细介绍。
有许多函数和变量通过 C 和 C++ 接口被明确公开,包含
expect_tcl.h
文件可以访问这些公共符号。以下是一些相关的变量和函数说明:
1.
共享变量
-
int exp_disconnected
:初始值为 0,如果成功使用了 Expect 的
disconnect
命令,它会被设置为 1。将其设置为 1 会阻止再次调用
disconnect
命令。
-
int exp_is_debugging
:反映 Expect 命令
exp_internal
的状态,即它包含最近传递给
exp_internal
的参数值,反之,
exp_internal
命令也反映
exp_is_debugging
的值。
-
int exp_loguser
:反映 Expect 命令
log_user
的状态。
2.
非共享变量和函数
-
void (*exp_app_exit) (Tcl_Interp *)
:指向一个函数的指针,该函数描述了一个特定于应用程序的处理程序。该处理程序在脚本定义的退出处理程序运行后执行。默认值为 0 表示没有处理程序。
-
FILE *exp_cmdfile
:Expect 从中读取命令的流。
-
char *exp_cmdfilename
:Expect 打开并从中读取命令的文件的名称。
-
int exp_cmdlinecmds
:如果 Expect 在程序命令行上使用了 Expect(或 Tcl)命令(例如使用
-c
),则该值为 1。
-
int exp_getpid
:Expect 进程本身的进程 ID(不是任何启动的进程的 ID)。
-
int exp_interactive
:如果 Expect 使用
-i
标志启动,或者在 Expect 开始执行时没有调用脚本命令,则该值为 1。
exp_interactive
用于控制 Expect 是否启动其解释器命令与用户进行交互。
-
Tcl_Interp *exp_interp
:指向一个解释器的指针,当没有其他可用的解释器时(如信号处理程序)会使用它。
exp_interp
由
Exp_Init
自动设置,但可以在以后的任何时间重新定义。
-
int exp_tcl_debugger_available
:如果调试器已启用(通常通过命令行参数),则该值为 1。
-
char *exp_cook(char *string,int *length)
:读取其字符串参数,并返回一个静态缓冲区,其中字符串中的换行符被替换为回车换行序列。其主要目的是在不担心终端是处于原始模式还是已处理模式的情况下生成错误消息。该静态缓冲区在下次调用
exp_cook
时会被覆盖。如果
length
指针有效,它将被用作输入字符串的长度,
exp_cook
还会将返回字符串的长度写入
*length
。如果
length
指针为 0,
exp_cook
将使用
strlen
计算字符串的长度,并且不会将长度返回给调用者。
-
void exp_error(Tcl_Interp *interp,char *fmt, ... )
:类似于
printf
的函数,将结果写入
interp->result
。调用者仍必须返回
Tcl_ERROR
以告知 Tcl 解释器发生了错误。
-
void exp_exit(Tcl_Interp *interp,int status)
:类似于 Expect 的
exit
命令。
exp_exit
会调用所有的退出处理程序(见
exp_exit_handlers
),然后强制程序以指定的状态值退出。
-
void exp_exit_handlers(Tcl_Interp *)
:调用任何脚本定义的退出处理程序,然后调用任何应用程序定义的退出处理程序。最后,将终端重置为其原始模式。
-
int exp_interpret_cmdfile(Tcl_Interp *,FILE *)
:读取给定的流并计算其中找到的任何命令。
-
int exp_interpret_cmdfilename(Tcl_Interp *,char *)
:打开给定的文件并计算其中找到的任何命令。
-
void exp_interpret_rcfiles(Tcl_Interp *,int my_rc,int sys_rc)
:读取并计算
.rc
文件。如果
my_rc
为 0,则跳过
~/.expect.rc
;如果
sys_rc
为 0,则跳过系统范围的
expect.rc
文件。
-
int exp_interpreter(Tcl_Interp *)
:交互式地提示用户输入命令并计算它们。
-
void exp-parse_argv(Tcl_Interp *,int argc,char **argv)
:读取程序命令行的表示。根据命令行上找到的内容,初始化其他变量,包括
exp_interactive
、
exp_cmdfilename
、
exp_cmdlinecmds
等。如果合适,
exp-parse_argv
还会读取并计算
.rc
文件。
8. 练习
为了帮助你更好地掌握上述内容,以下是一些相关的练习:
1. 设计并实现一个扩展,以加快使用纯 Tcl 命令将 UNIX 字典加载到内存的速度。
2. 计算解决上一个练习所花费的时间,将其除以使用普通 Tcl 命令加载字典和使用新扩展加载字典的时间差。在你收回在上一个练习中花费的时间之前,需要加载字典多少次?
3. Tcl 的常见问题解答(FAQ)列出了数百个 Tcl 扩展。浏览该 FAQ,找出对你有用的扩展,下载、安装并将它们与 Expect 结合使用。
4. 创建你自己的新扩展。如果你认为它具有普遍的兴趣,可以在 Tcl 新闻组中发布一条消息,让其他人可以尝试使用它。
通过这些练习,你可以进一步巩固所学的知识,并将其应用到实际的开发中。希望本文能够帮助你更好地理解和使用 Expect 与 Tcl 的结合,提升你的编程能力。
深入探索 Expect:与 Tcl 结合的使用指南
9. 操作流程总结
为了更清晰地展示上述各种操作的流程,我们可以用 mermaid 流程图来进行总结。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([开始]):::startend --> B(选择操作类型):::decision
B --> |添加 Expect 到 Tcl 程序| C(复制 tclApplni t.c 文件到新目录):::process
C --> D(修改代码添加 Exp_Init):::process
D --> E(包含 expect_tcl.h 头文件):::process
E --> F(编译文件):::process
F --> G(生成可执行文件):::process
G --> H{是否使用 C++}:::decision
H --> |是| I(使用 C 编译,C++ 链接):::process
H --> |否| J([结束]):::startend
I --> J
B --> |向 Expect 添加扩展| K(复制 exp_main_exp.c 文件到新目录):::process
K --> L(修改代码添加扩展初始化):::process
L --> M(包含适当头文件):::process
M --> N(编译文件):::process
N --> O(生成可执行文件):::process
O --> P{是否使用 C++}:::decision
P --> |是| Q(使用 C 编译,C++ 链接):::process
P --> |否| R([结束]):::startend
Q --> R
B --> |向 Expectk 添加扩展| S(使用 exp_main_tk.c 模板文件):::process
S --> T(链接 Expectk 和 Tk 库编译):::process
T --> U(生成可执行文件):::process
U --> V{是否使用 C++}:::decision
V --> |是| W(使用 C 编译,C++ 链接):::process
V --> |否| X([结束]):::startend
W --> X
B --> |创建无脚本 Expect 程序| Y(替换解释阶段调用为 Tcl_Eval):::process
Y --> Z(确保字符串在可写内存):::process
Z --> AA(删除 source 语句):::process
AA --> AB(生成编译后的脚本):::process
AB --> AC([结束]):::startend
这个流程图展示了不同操作的主要步骤,包括添加 Expect 到 Tcl 程序、向 Expect 添加扩展、向 Expectk 添加扩展以及创建无脚本 Expect 程序。每个操作都有相应的步骤,并且考虑了使用 C++ 的情况。
10. 常见问题及解决方法
在使用 Expect 与 Tcl 结合的过程中,可能会遇到一些常见问题,以下是一些问题及对应的解决方法:
|问题描述|解决方法|
| ---- | ---- |
|编译时找不到头文件或库文件|检查
-I
和
-L
标志指定的目录是否正确,确保头文件和库文件存在于相应的目录中。可以查看 Tcl 和 Expect 的 Makefiles 以获取正确的目录信息。|
|命令行参数处理不符合预期|确认使用的程序是否支持相应的命令行标志。如果使用的是添加了 Expect 的程序,注意其与原生 Expect 在命令行参数处理上的差异。|
|信号处理冲突|确保信号定义由用户控制,避免多个扩展同时处理相同的信号。如果出现冲突,检查扩展的信号处理逻辑并进行调整。|
|命令名称冲突|使用 “exp_” 前缀来确保使用 Expect 的命令。对于已经以 “exp” 开头的命令,注意其没有加前缀的版本。|
|退出处理异常|检查扩展提供的退出处理程序是否与 Expect 的退出处理逻辑冲突。确保只有一个退出命令能够执行。|
|解释器提示显示异常|确认使用的是正确的解释器,并了解 Expect 和 tclsh 解释器在提示信息生成上的差异。|
|.rc 文件未被读取|当将 Expect 作为扩展用于其他程序时,默认不会读取
.rc
文件。如果需要读取,可以手动调用相应的函数来处理。|
11. 性能优化建议
在使用 Expect 与 Tcl 结合的应用程序中,性能优化是一个重要的方面。以下是一些性能优化的建议:
1.
减少不必要的扩展
:只添加实际需要的扩展,避免过多的扩展导致程序启动时间增加和内存占用过高。
2.
优化脚本代码
:在编写脚本时,尽量避免使用复杂的嵌套循环和不必要的命令,提高脚本的执行效率。
3.
使用编译后的脚本
:如果只使用一个 Expect 应用程序,使用编译后的脚本可以避免每次运行时加载脚本文件的开销。
4.
合理处理信号
:避免频繁的信号处理,减少信号处理对程序性能的影响。
5.
缓存数据
:对于一些经常使用的数据,可以进行缓存,避免重复计算和读取。
12. 实际应用案例
为了更好地理解 Expect 与 Tcl 结合的实际应用,以下是一个简单的案例:自动化服务器配置。
假设我们需要在多台服务器上进行相同的配置操作,如安装软件、修改配置文件等。可以使用 Expect 与 Tcl 编写一个脚本,通过 SSH 连接到每台服务器并执行相应的命令。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "expect_tcl.h"
int main() {
Tcl_Interp *interp = Tcl_CreateInterp();
if (Tcl_Init(interp) == Tcl_ERROR) {
fprintf(stderr, "Tcl_Init failed: %s\n", Tcl_GetStringResult(interp));
return 1;
}
if (Exp_Init(interp) == Tcl_ERROR) {
fprintf(stderr, "Exp_Init failed: %s\n", Tcl_GetStringResult(interp));
return 1;
}
char *servers[] = {"server1.example.com", "server2.example.com", "server3.example.com"};
int num_servers = sizeof(servers) / sizeof(servers[0]);
for (int i = 0; i < num_servers; i++) {
char cmd[256];
snprintf(cmd, sizeof(cmd), "spawn ssh user@%s", servers[i]);
Tcl_Eval(interp, cmd);
// 处理 SSH 连接过程中的提示信息
Tcl_Eval(interp, "expect { "
"\"yes/no\" { send \"yes\\r\"; exp_continue } "
"\"password:\" { send \"your_password\\r\" } "
"}");
// 执行配置命令
Tcl_Eval(interp, "send \"sudo apt-get update\\r\"");
Tcl_Eval(interp, "expect \"password for user:\"");
Tcl_Eval(interp, "send \"your_password\\r\"");
Tcl_Eval(interp, "expect \"#\"");
Tcl_Eval(interp, "send \"sudo apt-get install -y some_package\\r\"");
Tcl_Eval(interp, "expect \"#\"");
// 退出 SSH 连接
Tcl_Eval(interp, "send \"exit\\r\"");
}
Tcl_DeleteInterp(interp);
return 0;
}
这个案例展示了如何使用 Expect 与 Tcl 结合来自动化服务器配置。通过 SSH 连接到多台服务器,输入密码,执行更新和安装软件的命令,最后退出连接。
13. 总结
本文详细介绍了如何将 Expect 作为 Tcl 的扩展来使用,包括将 Expect 添加到基于 Tcl 的程序中、向 Expect 添加其他扩展、向 Expectk 添加扩展以及创建无脚本的 Expect 程序。同时,还探讨了 Expect 作为 Tcl 扩展时与原生 Expect 的差异,介绍了 Expect 扩展中的函数和变量,并提供了相关的练习和常见问题的解决方法。通过实际应用案例,我们可以看到 Expect 与 Tcl 结合在自动化任务中的强大功能。希望读者通过本文的学习,能够更好地掌握 Expect 与 Tcl 的结合使用,提升编程效率和应用程序的功能。
在未来的开发中,你可以根据实际需求进一步探索和扩展这些技术,不断优化应用程序的性能和功能。同时,积极参与开源社区,分享自己的经验和扩展,与其他开发者共同进步。
超级会员免费看
92

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



