C/C++中使用Expect库实现命令行程序自动化交互
1. 项目概述为什么要在C/C里用Expect如果你写过需要和命令行程序交互的C或C代码比如自动配置一个网络设备、测试一个命令行工具或者写一个自动化安装脚本那你肯定对system()、popen()这些函数又爱又恨。爱的是它们用起来简单一行代码就能调用外部程序恨的是它们太“笨”了——你只能单向地发送命令然后祈祷程序按你预想的输出再想办法从一堆文本里解析结果。一旦遇到需要输入密码、回答交互式提示比如“Are you sure? [Y/N]”的情况用原生C/C处理起来就异常麻烦代码会迅速变得臃肿且脆弱。这就是Expect库大显身手的地方。简单来说Expect是一个用来实现程序间自动化交互的库。它的核心思想是“期望-发送”你的程序父进程会fork出一个子进程比如ssh,ftp,telnet然后监视子进程的输出。当输出中出现了你“期望”的特定字符串比如password:提示符你的程序就自动“发送”相应的回应比如密码。整个过程是双向、有状态的完美模拟了一个真实用户坐在终端前的操作。虽然Expect本身是用Tcl写的并且有一个非常著名的同名Tcl扩展模块但它的核心思想被移植到了多种语言。我们今天讨论的就是如何在C和C项目中利用libexpect库通常作为Tcl/Tk的一部分提供或者其思想来构建健壮的自动化交互程序。这对于开发嵌入式系统测试工具、网络设备管理后台、或是任何需要与黑盒命令行程序打交道的场景都是一个极具价值的技能。2. 核心概念与工作原理拆解在深入代码之前我们必须理解Expect模式下的几个核心概念这能帮你更好地设计程序而不是仅仅复制粘贴代码。2.1 交互式程序与自动化困境为什么printf/scanf或者简单的管道对付不了交互式程序因为交互式程序的状态是动态的。考虑一个sudo命令流程父进程输出[sudo] password for user:程序阻塞等待用户从标准输入stdin键入密码。用户输入密码并回车。程序验证密码输出结果。如果用popen(“sudo ls”, “r”)你只能读到第一步的输出然后进程就卡在那里等待永远无法到达的输入。Expect库通过伪终端Pseudo Terminal简称PTY解决了这个问题。PTY模拟了一个真实的终端设备为子进程提供了一个完整的、可交互的终端环境包括终端尺寸、控制字符处理等同时父进程可以通过文件描述符读写这个“虚拟终端”的两端从而完全掌控交互过程。2.2 Expect的核心状态机Expect库在内部维护了一个经典的状态机其工作流程可以概括为生成Spawn启动目标交互程序并与之建立PTY连接。期望Expect监听子进程的输出流将其与一组预定义的“模式”可以是字符串、正则表达式、超时或文件结束符进行匹配。匹配与动作如果匹配到某个模式则执行与该模式关联的动作通常是发送字符串。如果发生超时则执行超时处理动作。如果遇到文件结束符EOF则知道子进程已结束。循环重复步骤2和3直到交互完成或达到结束条件。这个“模式-动作”对是Expect编程的灵魂。你需要精确地定义在什么情况下做什么事这要求你对目标程序的交互流程有清晰的了解。2.3 libexpect 与 Tcl Expect 的异同你可能会在网上搜到大量TclExpect脚本的例子。它们语法简洁非常适合快速编写一次性脚本。而我们使用的libexpect是一个C语言库它提供了类似的API但需要编译链接到你的C/C程序中。选择libexpect的主要原因包括性能与集成作为原生库它避免了启动Tcl解释器的开销并且能更好地与现有C/C代码库集成直接操作内存和数据结构。部署简便最终生成的是一个独立的可执行文件无需目标机器安装Tcl/Expect环境。精细控制提供更底层的文件描述符和控制接口适合构建复杂的、作为核心服务组件的自动化工具。3. 环境准备与libexpect库的获取在开始编码前我们需要准备好开发环境。libexpect通常不作为一个独立的包分发而是随着Tcl/Tk的源代码一起提供。3.1 在Linux系统上安装在基于Debian/Ubuntu的系统上你可以通过安装Tcl开发包来获取libexpectsudo apt update sudo apt install tcl-dev expect-dev安装后关键的头文件如expect.h和库文件libexpect.so通常会被放置在/usr/include/tcl和/usr/lib等标准路径下。在基于RHEL/CentOS/Fedora的系统上使用yum或dnfsudo yum install tcl-devel expect-devel # 或 sudo dnf install tcl-devel expect-devel3.2 手动编译与安装通用方法如果包管理器没有提供或者你需要特定版本从源码编译是最可靠的方式。下载源码从Tcl的官方源码仓库如https://core.tcl-lang.org/tcl/下载包含expect模块的完整Tcl源码包或者直接下载expect的独立源码如从SourceForge。配置与编译# 假设解压到目录 /path/to/expect-5.45.4 cd /path/to/expect-5.45.4 ./configure --prefix/usr/local make sudo make install这个过程会编译并安装libexpect以及expect可执行文件即Tcl版本的expect。验证安装安装完成后检查库文件是否存在ls /usr/local/lib/libexpect*同时确认头文件位置通常在/usr/local/include或/usr/local/include/tcl子目录下。3.3 集成到你的C/C项目在你的项目编译命令中需要正确链接expect库以及它依赖的tcl库。一个典型的gcc编译命令如下gcc -o my_auto_tool my_auto_tool.c -I/usr/local/include -L/usr/local/lib -lexpect -ltcl -lm-I指定expect.h等头文件的搜索路径。-L指定libexpect.so等库文件的搜索路径。-lexpect -ltcl链接libexpect和libtcl库。-lm是数学库有时也需要。注意在动态链接环境下如果你的程序要在其他机器运行需要确保目标机器上也有相应版本的libexpect和libtcl或者采用静态链接的方式。静态链接命令会更复杂需要指定.a静态库文件。4. 核心API详解与基础用法libexpect提供了一套以exp_为前缀的C函数。理解这些核心API是编写代码的关键。4.1 初始化与进程生成任何使用libexpect的程序都应该包含头文件并进行初始化。#include expect.h int main() { // 初始化expect库 exp_init(); // 关键关闭标准输入的缓冲这对于交互至关重要 exp_loguser 1; // 可选将expect的内部日志输出到stderr调试时非常有用 // 生成子进程 const char* cmd /usr/bin/ssh; const char* argv[] {“ssh”, “userremotehost”, NULL}; int pid exp_spawnv(cmd, (char* const*)argv); if (pid 0) { perror(“exp_spawnv failed”); exit(1); } // ... 后续交互代码 }exp_init()初始化库内部数据结构必须调用。exp_loguser 1这是一个全局变量设置为真非零时expect内部详细的匹配过程会打印到标准错误是调试的利器。exp_spawnv()这是最常用的生成函数。它接受一个命令路径和参数数组类似execv通过PTY启动子进程并返回子进程的PID。更简单的exp_spawnl()类似于execl接受可变参数列表。4.2 模式匹配与expect函数这是最核心的部分。exp_expectl()函数用于等待并匹配模式。int exp_expectl(int fd, exp_case [pattern1, action1, pattern2, action2, ..., EXP_END]);实际上由于C语言的限制我们通常使用一个更易用的变体并通过exp_fexpectv()或循环配合exp_expectv()来使用。但概念是相通的。下面是一个典型的使用模式我们通过一个switch语句来处理不同的匹配结果#include expect.h #include stdio.h #include unistd.h int main() { exp_init(); exp_loguser 1; // 开启调试 int pid exp_spawnl(“/usr/bin/ssh”, “ssh”, “testuserlocalhost”, NULL); if (pid 0) { perror(“spawn”); return 1; } // 定义我们期望的模式 enum { PROMPT_PASSWORD, PROMPT_YESNO, PROMPT_SHELL, TIMEOUT, EOF_OR_ERROR }; int match_result; char* patterns[] { “password:”, // 索引0对应PROMPT_PASSWORD “yes/no”, // 索引1对应PROMPT_YESNO “$ “, // 索引2对应PROMPT_SHELL (假设shell提示符) NULL }; // 主交互循环 while (1) { // exp_expectv 会阻塞直到匹配到模式或超时/错误 match_result exp_expectv(exp_pid, patterns, 3, exp_timeout, 30); // 30秒超时 switch (match_result) { case PROMPT_PASSWORD: printf(“[父进程] 检测到密码提示发送密码。\n”); exp_send(“my_secret_password\r”); // \r 模拟回车 exp_continue; // 继续等待下一个模式 break; case PROMPT_YESNO: printf(“[父进程] 检测到yes/no提示发送yes。\n”); exp_send(“yes\r”); exp_continue; break; case PROMPT_SHELL: printf(“[父进程] 成功登录进入shell。现在可以发送命令了。\n”); // 例如发送一个命令 exp_send(“ls -la\r”); // 然后可以继续expect命令的输出... // 这里我们简单等待一下然后退出 sleep(2); exp_send(“exit\r”); break; case EXP_TIMEOUT: fprintf(stderr, “[父进程] 操作超时\n”); exp_close(); return 1; case EXP_EOF: printf(“[父进程] 子进程已结束。\n”); exp_close(); return 0; default: // 通常是负的错误码 fprintf(stderr, “[父进程] Expect错误: %s\n”, exp_error()); exp_close(); return 1; } } return 0; }代码解析与关键点exp_expectv()这是实际进行模式匹配的函数。它监听与exp_pid一个全局变量通常在exp_spawn后设置关联的PTY。patterns是一个字符串数组3是模式数量。exp_timeout是一个全局变量这里设置为30秒。匹配索引函数返回匹配到的模式在数组中的索引从0开始。我们通过enum和switch来清晰地处理不同情况。exp_send()向子进程发送字符串。务必注意要模拟终端输入通常需要在字符串末尾加上回车符\r或换行符\n\r\n具体取决于目标程序。只发送命令而不发送回车相当于在终端里打了字但没按回车键。exp_continue这是一个宏告诉expect库在完成当前匹配动作后立即回到exp_expectv()继续等待下一个模式。如果没有exp_continue循环会进入下一次迭代这通常也是可以的但exp_continue在简单循环中更清晰。exp_close()清理与子进程关联的资源。在程序结束或出错时应调用。exp_error()当exp_expectv()返回负的错误码时可以用此函数获取错误信息字符串。4.3 处理超时与EOF超时和EOF是必须妥善处理的边界情况。超时EXP_TIMEOUT如果子进程在指定时间内没有输出任何匹配的内容就会触发。这可能是因为网络延迟、程序卡死、或者你的模式写错了。处理方式可以是重试、发送一个中断字符如CtrlC或者直接失败退出。EOFEXP_EOF当子进程关闭了它的输出流即进程结束时触发。这是正常交互结束的信号。你需要调用exp_close()并获取子进程的退出状态通过waitpid或exp_waitpid。5. 高级模式匹配技巧基础的字符串匹配能满足大部分需求但libexpect支持更强大的模式特别是正则表达式。5.1 使用正则表达式要使用正则表达式你需要包含regex.h并在模式字符串前加上-re标志这是Tcl Expect的约定libexpect也支持类似的机制但通常通过exp_expectv配合特定模式对象实现更直接的方式是使用exp_expect系列函数支持的正则标志。实际上libexpect的exp_expectv函数本身不支持直接内嵌正则标志。更强大的方式是使用exp_expect函数族中支持exp_glob或exp_regexp的类型。一个更接近实际高级用法的示例是我们可能直接使用exp_expect函数它接受一个exp_case结构体数组。但为了简化很多开发者会结合使用libexpect和C语言自己的正则表达式库regex.h或PCRE库在exp_expectv匹配到大致内容后再用正则精确提取信息。不过libexpect内部是支持类Tcl的正则匹配的只是C API不那么直观。一种常见模式是你可以将模式定义为-re “pattern”这样的字符串但需要确保exp_expectv能正确解析。更可靠的方法是查阅你所使用的libexpect版本的文档或者采用以下“两步法”先用exp_expectv匹配一个宽泛的模式例如一行输出。从exp_buffer一个全局变量存储了最近匹配到的输出中读取数据。用regex.h对exp_buffer中的数据进行精确的正则匹配和提取。#include regex.h // ... 在某个case分支中 ... case PROMPT_SHELL: exp_send(“uname -a\r”); // 期望一个以换行结尾的行 char* patterns2[] {“\n”, NULL}; if (exp_expectv(exp_pid, patterns2, 1, exp_timeout, 5) 0) { // exp_buffer 现在包含了 ‘uname -a’ 命令的输出直到换行符 regex_t regex; regcomp(regex, “Linux ([^ ])”, REG_EXTENDED); regmatch_t matches[2]; if (regexec(regex, exp_buffer, 2, matches, 0) 0) { // 提取主机名 int start matches[1].rm_so; int end matches[1].rm_eo; char hostname[100]; strncpy(hostname, exp_buffer start, end - start); hostname[end - start] ‘\0’; printf(“提取到的主机名: %s\n”, hostname); } regfree(regex); } break;5.2 匹配缓冲区与部分输出exp_buffer和exp_match是高级使用的关键。exp_buffer指向包含自上次匹配以来或自生成进程以来所有输出的缓冲区。每次成功匹配后expect会更新缓冲区指针和长度。你可以直接读取它来获取子进程的输出内容。exp_match和exp_match_start/exp_match_end当使用正则表达式匹配时这些全局变量或通过exp_expect函数返回的结构体包含了匹配到的子字符串在exp_buffer中的位置。这对于提取信息如提取文件名、IP地址至关重要。实操心得直接操作exp_buffer需要小心缓冲区管理和线程安全libexpect非线程安全。对于复杂输出解析将exp_buffer的内容拷贝到本地内存再进行操作是更稳妥的做法。6. 构建一个完整的自动化SSH会话示例让我们整合以上知识编写一个更健壮、功能更完整的C程序用于自动登录SSH服务器执行一条命令并获取结果。/** * auto_ssh.c - 使用libexpect自动化SSH登录并执行命令 */ #include stdio.h #include stdlib.h #include string.h #include unistd.h #include expect.h #include sys/wait.h #define TIMEOUT_SHORT 10 #define TIMEOUT_LONG 30 int main(int argc, char *argv[]) { if (argc ! 4) { fprintf(stderr, “用法: %s 用户名主机 密码 命令\n”, argv[0]); return 1; } char *target argv[1]; char *password argv[2]; char *remote_cmd argv[3]; exp_init(); // exp_loguser 1; // 调试时开启 // 构建SSH命令。禁用严格主机密钥检查(-o StrictHostKeyCheckingno)用于自动化 // 在生产环境中应使用更安全的方式处理密钥。 char ssh_cmd[512]; snprintf(ssh_cmd, sizeof(ssh_cmd), “ssh -o StrictHostKeyCheckingno %s”, target); // 注意这里我们使用exp_spawnl它通过shell启动命令。 // 更安全的方式是像之前一样用exp_spawnv直接执行ssh避免shell注入。 // 这里为了示例清晰使用shell方式。 printf(“[] 正在生成SSH进程: %s\n”, ssh_cmd); int pid exp_spawnl(“/bin/sh”, “sh”, “-c”, ssh_cmd, NULL); if (pid 0) { perror(“[-] exp_spawnl 失败”); return 1; } // 定义交互模式 char *patterns[] { “password:”, // 0: 密码提示 “yes/no”, // 1: 首次连接确认 “[$#] “, // 2: Shell提示符 (bash的$或root的#) “Permission denied”, // 3: 权限错误 NULL }; int pattern_count 4; int ret; int timeout TIMEOUT_LONG; int authenticated 0; while (1) { ret exp_expectv(exp_pid, patterns, pattern_count, exp_timeout, timeout); switch (ret) { case 0: // “password:” if (authenticated) { // 如果已经认证过又出现密码提示可能是sudo或二次认证这里简单处理为失败 fprintf(stderr, “[-] 意外的密码提示。认证可能失败。\n”); exp_send(“\003”); // 发送CtrlC goto cleanup; } printf(“[] 检测到密码提示发送密码。\n”); exp_send(password); exp_send(“\r”); authenticated 1; // 发送密码后等待可能出现的shell提示符或错误设置较短超时 timeout TIMEOUT_SHORT; break; case 1: // “yes/no” printf(“[] 首次连接确认主机密钥。\n”); exp_send(“yes\r”); timeout TIMEOUT_LONG; break; case 2: // Shell提示符 “[$#] ” printf(“[] 登录成功正在执行远程命令: %s\n”, remote_cmd); exp_send(remote_cmd); exp_send(“\r”); // 命令执行后我们需要捕获命令输出然后退出。 // 先等待命令输出完成遇到下一个提示符 char *post_cmd_patterns[] {“[#$] “, NULL}; // 临时修改模式等待命令输出后的新提示符 // 注意这里简化处理复杂输出可能需要循环读取 printf(“[] 命令输出开始 ---\n”); fflush(stdout); // 确保我们的提示先输出 // 我们将子进程的输出直接“中继”到我们的标准输出一段时间 // 一种简单方法设置exp_loguser不那会输出调试信息。 // 更好的方法使用exp_expectv匹配提示符但在此期间手动读取并打印exp_buffer // 这里采用一个技巧设置超时并循环读取直到遇到提示符或超时。 // 实际上对于简单命令我们可以直接expect一个换行符序列来获取输出。 // 我们改为发送命令后expect一个通用的行结束模式来获取输出。 // 清空当前缓冲区expect库会自动管理。 // 我们重新调用exp_expectv来等待提示符但在此之前输出可能已经在我们标准输出上了吗 // 不需要我们从exp_fd读取。这里展示一个更手动的混合方法。 // 临时关闭exp_loguser如果开着并直接读取文件描述符 // 获取与子进程pty关联的文件描述符 int exp_fd exp_pid; // 注意exp_pid是进程ID不是文件描述符这是一个常见误区。 // 正确的获取fd方式通常通过exp_pid查询但libexpect内部管理。 // 更直接的方法使用exp_pid对应的exp_out。这变得复杂。 // 为了示例的简洁和正确性我们回到标准expect流程 // 1. 发送命令。 // 2. 等待一个独特的“命令结束标记”比如我们知道的提示符。 // 3. 在这期间expect库会将所有输出收集在exp_buffer。 // 4. 匹配成功后我们打印exp_buffer中从命令发送后到提示符之前的部分。 // 由于我们已经在shell提示符的case里需要先发送命令上面已做。 // 然后跳出这个switch在外层循环中下一次exp_expectv会捕获输出。 // 我们需要修改模式来区分“刚登录的提示符”和“命令执行后的提示符”。 // 一个简单方法在发送命令后修改patterns数组将提示符模式放在最后 // 并添加一个匹配“任何文本换行”的通用模式来捕获输出。 // 这超出了基础示例的范围但指出了真实世界的复杂性。 // 为了本示例能运行我们采用一个简化方案休眠一下然后假设命令执行完毕。 printf(“[] (简化处理等待命令执行)\n”); sleep(2); // 糟糕的实践仅用于演示。 // 然后发送退出命令 exp_send(“exit\r”); printf(“[] 命令执行完毕退出SSH会话。\n”); goto wait_for_exit; break; case 3: // “Permission denied” fprintf(stderr, “[-] 权限被拒绝。密码错误或用户无权限。\n”); goto cleanup; case EXP_TIMEOUT: fprintf(stderr, “[-] 等待超时。网络或主机可能有问题。\n”); goto cleanup; case EXP_EOF: printf(“[] SSH会话已结束。\n”); goto cleanup; default: fprintf(stderr, “[-] Expect错误: %s\n”, exp_error()); goto cleanup; } } wait_for_exit: // 等待子进程完全结束 int status; waitpid(pid, status, 0); printf(“[] 子进程退出状态: %d\n”, WEXITSTATUS(status)); cleanup: exp_close(); return (ret EXP_EOF) ? 0 : 1; }这个示例的局限性与改进方向简化输出捕获示例中为了简化用sleep(2)等待命令执行这是不可靠的。正确做法是发送命令后expect一个能唯一标识命令结束的模式比如新的提示符并在此期间从exp_buffer或文件描述符中读取所有输出。安全警告在命令行传递密码是极不安全的会出现在进程列表(ps)中。生产环境应使用SSH密钥认证或从安全存储中读取凭据。错误处理需要更精细的错误处理和重试逻辑。代码结构对于更复杂的交互使用状态机模式来管理不同的交互阶段连接、认证、执行、清理会使代码更清晰。7. 常见问题、调试技巧与避坑指南在实际使用libexpect时你会遇到各种稀奇古怪的问题。下面是我踩过的一些坑和总结的技巧。7.1 编译与链接问题问题undefined reference toexp_spawnl‘ 或类似链接错误。原因编译器找不到libexpect库。-lexpect必须放在源文件之后。确保库路径(-L)正确且安装了开发包(-dev或-devel)。解决检查pkg-config如果支持pkg-config –libs expect。或者明确指定库路径-L/usr/local/lib -lexpect -ltcl8.6 -lm版本号可能不同。问题程序运行时报告libexpect.so.xx: cannot open shared object file。原因动态链接器找不到共享库。解决将库所在目录如/usr/local/lib添加到/etc/ld.so.conf并运行ldconfig或者设置环境变量LD_LIBRARY_PATH。7.2 运行时交互问题问题程序卡住没有任何输出也不超时。原因1模式字符串不匹配。子进程的输出可能包含隐藏的控制字符、颜色码或空格差异。调试设置exp_loguser 1;。这会将expect内部看到的每一个字符都打印到stderr让你能精确看到子进程输出了什么。这是最重要的调试手段。技巧使用更宽松的模式比如用*password:*如果支持通配符或正则表达式.*password:来匹配。原因2没有发送回车符。你发送了“password”但没发送\r或\n。解决确保exp_send的字符串以\r结尾。可以同时试试\r\n。原因3缓冲区问题。某些程序的输出可能是行缓冲或块缓冲而PTY的设置可能影响了缓冲行为。解决尝试在exp_send后调用exp_send(“”);发送空字符串有时会刷新缓冲区。或者在生成进程时可以尝试用expect的-nottycopy和-nottyinit标志如果libexpect的exp_spawn接口支持来更精细地控制PTY初始化。问题匹配到了错误的模式或者动作执行后流程混乱。原因模式定义有重叠或顺序不当。exp_expectv按模式数组顺序匹配第一个匹配到的生效。解决将更具体、更精确的模式放在前面将更通用、更宽松的模式如提示符放在后面。仔细设计你的交互状态机。问题如何同时处理多个子进程说明标准的libexpect不是为直接的多进程并发设计的。它主要围绕一个全局的exp_pid工作。解决如果需要自动化多个独立会话通常建议用多进程或多线程每个线程/进程管理自己的expect会话注意初始化。或者使用select()或poll()multiplexing多个由exp_spawn返回的文件描述符但这需要更底层的操作并手动调用exp_expectv类似的逻辑复杂度很高。对于这种需求评估是否可以用多个独立的进程来实现会更简单。7.3 安全与最佳实践绝不硬编码密码示例中从命令行参数读密码也只是为了演示。真实应用应从加密的配置文件、环境变量或交互式输入如使用getpass()函数获取。使用SSH密钥认证对于SSH自动化首选公钥认证。生成密钥对将公钥部署到目标服务器你的expect脚本只需要处理可能存在的密钥密码短语passphrase或者使用无密码的密钥。处理主机密钥示例中使用了-o StrictHostKeyCheckingno这会接受任何主机密钥存在中间人攻击风险。对于生产环境应在受控环境中预先建立已知主机指纹或使用-o UserKnownHostsFile指向一个包含预期指纹的文件。超时设置为不同的操作阶段设置合理的超时。登录网络设备可能比登录本地SSH慢。设置过短的超时会导致偶发性失败过长则影响程序响应。日志记录除了exp_loguser用于调试生产代码应该将重要的交互步骤、发送的命令和关键输出记录到日志文件便于问题追溯。资源清理确保在程序所有退出路径上都调用exp_close()避免僵尸进程和资源泄漏。8. 在C项目中的封装与实践在C项目中使用C风格的libexpect可能会显得格格不入。一个好的实践是编写一个简单的C封装类提供更安全、更面向对象的接口。// ExpectSession.hpp #ifndef EXPECT_SESSION_HPP #define EXPECT_SESSION_HPP #include string #include vector #include memory class ExpectSession { public: ExpectSession(); ~ExpectSession(); // 禁用拷贝 ExpectSession(const ExpectSession) delete; ExpectSession operator(const ExpectSession) delete; // 移动语义 ExpectSession(ExpectSession other) noexcept; ExpectSession operator(ExpectSession other) noexcept; bool spawn(const std::string command, const std::vectorstd::string args {}); void send(const std::string str); enum class ExpectResult { Match, Timeout, EOF, Error }; struct Match { int index; // 匹配到的模式索引 std::string before; // 匹配点之前的内容 std::string matched; // 匹配到的内容 }; ExpectResult expect(const std::vectorstd::string patterns, int timeout_seconds, Match out_match); void setLogUser(bool enable); void close(); bool isActive() const { return m_pid 0; } private: int m_pid{-1}; bool m_logUser{false}; }; #endif // EXPECT_SESSION_HPP// ExpectSession.cpp #include “ExpectSession.hpp” #include expect.h #include unistd.h #include sys/wait.h #include string.h #include stdexcept ExpectSession::ExpectSession() { exp_init(); } ExpectSession::~ExpectSession() { close(); } ExpectSession::ExpectSession(ExpectSession other) noexcept : m_pid(other.m_pid), m_logUser(other.m_logUser) { other.m_pid -1; } ExpectSession ExpectSession::operator(ExpectSession other) noexcept { if (this ! other) { close(); m_pid other.m_pid; m_logUser other.m_logUser; other.m_pid -1; } return *this; } bool ExpectSession::spawn(const std::string command, const std::vectorstd::string args) { if (m_pid 0) { close(); } // 构建参数数组符合C接口要求 std::vectorconst char* argv; argv.push_back(command.c_str()); for (const auto arg : args) { argv.push_back(arg.c_str()); } argv.push_back(nullptr); m_pid exp_spawnv(command.c_str(), const_castchar* const*(argv.data())); if (m_pid 0) { exp_loguser m_logUser ? 1 : 0; } return m_pid 0; } void ExpectSession::send(const std::string str) { if (m_pid 0) { throw std::runtime_error(“ExpectSession not active”); } exp_send(str.c_str()); } ExpectSession::ExpectResult ExpectSession::expect(const std::vectorstd::string patterns, int timeout_seconds, Match out_match) { if (m_pid 0) { return ExpectResult::Error; } // 将std::string转换为C字符串数组 std::vectorconst char* c_patterns; for (const auto p : patterns) { c_patterns.push_back(p.c_str()); } int old_timeout exp_timeout; exp_timeout timeout_seconds; int ret exp_expectv(exp_pid, const_castchar**(c_patterns.data()), patterns.size(), exp_timeout, timeout_seconds); exp_timeout old_timeout; out_match.index ret; // 注意这里简化处理实际应使用exp_buffer等获取匹配前后内容 // out_match.before …; // out_match.matched …; if (ret 0 ret static_castint(patterns.size())) { return ExpectResult::Match; } else if (ret EXP_TIMEOUT) { return ExpectResult::Timeout; } else if (ret EXP_EOF) { return ExpectResult::EOF; } else { return ExpectResult::Error; } } void ExpectSession::setLogUser(bool enable) { m_logUser enable; if (m_pid 0) { exp_loguser enable ? 1 : 0; } } void ExpectSession::close() { if (m_pid 0) { exp_close(); int status; waitpid(m_pid, status, 0); // 等待子进程结束 m_pid -1; } }这个封装类提供了RAII资源获取即初始化风格的管理自动清理资源并使用C类型如std::string和std::vector让代码更安全、更易读。在实际项目中你还可以进一步扩展它比如添加正则表达式匹配、超时回调、异步支持等。将libexpect的强大功能与C的现代特性结合你可以构建出既稳健又易于维护的自动化工具轻松应对那些令人生畏的命令行交互任务。记住理解其基于PTY和状态机的本质善用exp_loguser进行调试你就能驾驭绝大多数交互式程序的自动化挑战。