Linux

关注公众号 jb51net

关闭
首页 > 网站技巧 > 服务器 > Linux > Linux Expect自动化交互操作

Linux使用Expect脚本实现自动化交互操作

作者:知远漫谈

Expect是一个基于Tcl的工具,用于自动化处理需要输入的交互式命令,它可以模拟人类与程序交互,实现无人值守的自动化流程,文章介绍了Expect的基础使用、常见场景,并提供了Java集成示例,同时,文章还讨论了Expect的局限性、替代方案及性能优化建议,需要的朋友可以参考下

在 Linux 系统管理和运维工作中,我们经常需要与那些不支持非交互式输入的命令打交道 —— 比如 sshscppasswdftptelnet 等。这些程序设计之初就假定用户会在终端中手动键入密码或确认信息,因此无法通过简单的管道或重定向完成自动化。

这时候,Expect 就登场了 !

Expect 是一个基于 Tcl(Tool Command Language)的扩展工具,它能够“模拟人类”,自动响应程序提出的交互式提示,从而实现完全无人值守的自动化流程。无论你是系统管理员、DevOps 工程师,还是 Java 开发者希望集成自动化部署,掌握 Expect 都是提升效率的关键技能之一。

什么是 Expect?

Expect 最初由 Don Libes 在 1990 年代开发,目的是解决 Unix/Linux 下交互式程序难以脚本化的问题。它本质上是一个“对话机器人”:你告诉它“当看到某某提示时,就输入某某内容”,它就会忠实地执行下去。

它的核心思想是:

安装 Expect

大多数现代 Linux 发行版默认没有安装 Expect,但安装非常简单:

# Ubuntu / Debian
sudo apt update && sudo apt install expect -y

# CentOS / RHEL / Fedora
sudo yum install expect -y
# 或
sudo dnf install expect -y

# Arch Linux
sudo pacman -S expect

验证是否安装成功:

expect -v

输出类似:

expect version 5.45.4

Expect 基础语法速览

虽然 Expect 是 Tcl 的扩展,但你不需要成为 Tcl 专家也能写出实用脚本。以下是几个关键命令:

命令说明
spawn启动一个新的进程(你要自动化的程序)
expect等待特定字符串出现(如 “password:”)
send发送字符串到子进程(如密码)
interact将控制权交还给用户(用于调试)
set timeout N设置超时时间(秒),默认10秒

第一个 Expect 脚本:自动登录 SSH

假设我们要自动登录一台远程服务器 192.168.1.100,用户名为 admin,密码为 secret123

创建脚本文件 auto_ssh.exp

#!/usr/bin/expect -f

# 设置超时时间为20秒
set timeout 20

# 设置变量
set host "192.168.1.100"
set user "admin"
set password "secret123"

# 启动 ssh 连接
spawn ssh $user@$host

# 等待密码提示
expect {
    "*yes/no*" {
        send "yes\r"
        exp_continue
    }
    "*password:*" {
        send "$password\r"
    }
}

# 交出控制权,进入交互模式(可选)
interact

赋予执行权限并运行:

chmod +x auto_ssh.exp
./auto_ssh.exp

成功!你现在无需手动输入密码即可登录远程主机。

自动修改用户密码

另一个常见场景:批量修改多台服务器上的用户密码。

脚本 change_password.exp

#!/usr/bin/expect -f

set timeout 15
set user [lindex $argv 0]
set oldpass [lindex $argv 1]
set newpass [lindex $argv 2]

spawn passwd $user

expect "current password:"
send "$oldpass\r"

expect "new password:"
send "$newpass\r"

expect "retype new password:"
send "$newpass\r"

expect eof

调用方式:

./change_password.exp john old123 new456

注意:这里使用了 $argv 来接收命令行参数,非常灵活。

批量执行远程命令

有时我们不仅想登录,还想在远程机器上执行命令后退出。

脚本 remote_exec.exp

#!/usr/bin/expect -f

set timeout 30
set host [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
set command [lindex $argv 3]

spawn ssh $user@$host $command

expect {
    "*yes/no*" {
        send "yes\r"
        exp_continue
    }
    "*password:*" {
        send "$password\r"
    }
}

expect eof

执行示例:

./remote_exec.exp 192.168.1.100 admin secret123 "df -h"

这将在远程主机上执行 df -h 并返回结果。

Expect 流程图解(mermaid)

下面用 mermaid 图表展示 Expect 脚本的典型工作流:

这个循环结构是 Expect 的核心机制,理解它就能举一反三写出各种自动化脚本。

Expect 在 Java 项目中的集成应用

虽然 Expect 是 Tcl 脚本,但它完全可以被 Java 程序调用,实现“Java 控制交互式命令”的能力。

场景举例:

Java 调用 Expect 脚本示例

我们先写一个通用的 Java 工具类,用于执行外部脚本并捕获输出:

import java.io.*;
import java.util.ArrayList;
import java.util.List;
public class ExpectExecutor {
    /**
     * 执行 Expect 脚本并返回输出
     */
    public static String executeScript(String scriptPath, String... args) throws IOException, InterruptedException {
        List<String> command = new ArrayList<>();
        command.add("expect");
        command.add(scriptPath);
        for (String arg : args) {
            command.add(arg);
        }
        ProcessBuilder pb = new ProcessBuilder(command);
        pb.redirectErrorStream(true); // 合并错误流和标准输出
        Process process = pb.start();
        // 读取输出
        StringBuilder output = new StringBuilder();
        try (BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream()))) {
            String line;
            while ((line = reader.readLine()) != null) {
                output.append(line).append("\n");
            }
        }
        int exitCode = process.waitFor();
        if (exitCode != 0) {
            throw new RuntimeException("Expect script failed with exit code: " + exitCode);
        }
        return output.toString();
    }
    public static void main(String[] args) {
        try {
            // 示例:执行远程 df -h
            String result = executeScript(
                "/home/user/scripts/remote_exec.exp",
                "192.168.1.100", "admin", "secret123", "df -h"
            );
            System.out.println("远程磁盘使用情况:\n" + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

更复杂的 Java + Expect 场景:自动化部署 WAR 包

假设你有一个 Web 项目,编译后生成 app.war,你想自动部署到远程 Tomcat 的 webapps 目录。

首先编写 Expect 脚本 deploy_war.exp

#!/usr/bin/expect -f
set timeout 60
set host [lindex $argv 0]
set user [lindex $argv 1]
set password [lindex $argv 2]
set local_war [lindex $argv 3]
set remote_dir [lindex $argv 4]
# 上传文件
spawn scp $local_war $user@$host:$remote_dir/
expect {
    "*yes/no*" {
        send "yes\r"
        exp_continue
    }
    "*password:*" {
        send "$password\r"
    }
}
expect eof
# 登录并重启 Tomcat(假设路径已知)
spawn ssh $user@$host
expect {
    "*yes/no*" {
        send "yes\r"
        exp_continue
    }
    "*password:*" {
        send "$password\r"
    }
}
expect "$ "
send "cd /opt/tomcat && ./bin/shutdown.sh\r"
expect "$ "
send "./bin/startup.sh\r"
expect "$ "
send "exit\r"
expect eof

然后在 Java 中调用:

public class WarDeployer {
    public static void deployWar(String host, String user, String password,
                                 String localWarPath, String remoteDir) {
        try {
            System.out.println("开始部署 WAR 包到 " + host + "...");
            String output = ExpectExecutor.executeScript(
                "/scripts/deploy_war.exp",
                host, user, password, localWarPath, remoteDir
            );
            System.out.println("部署完成!Tomcat 已重启。\n输出:\n" + output);
        } catch (Exception e) {
            System.err.println("部署失败:" + e.getMessage());
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        deployWar(
            "192.168.1.200",
            "deployer",
            "mypassword",
            "/target/myapp.war",
            "/opt/tomcat/webapps"
        );
    }
}

一键部署完成!再也不用手动 SCP + SSH + 重启 Tomcat 了。

使用 Java 生成动态 Expect 脚本

有时你希望根据运行时参数动态生成 Expect 脚本,而不是预先写死。这在 CI/CD 系统中特别有用。

下面是一个 Java 类,负责生成临时 Expect 脚本并执行:

import java.io.*;
import java.nio.file.Files;
import java.nio.file.Paths;
public class DynamicExpectGenerator {
    public static String generateAndExecuteScript(String template, Object... params) {
        try {
            // 用参数填充模板
            String scriptContent = String.format(template, params);
            // 创建临时脚本文件
            File tempScript = File.createTempFile("expect_", ".exp");
            tempScript.deleteOnExit(); // JVM退出时删除
            Files.write(Paths.get(tempScript.getAbsolutePath()), scriptContent.getBytes());
            // 赋予执行权限
            Process chmod = Runtime.getRuntime().exec("chmod +x " + tempScript.getAbsolutePath());
            chmod.waitFor();
            // 执行脚本
            return ExpectExecutor.executeScript(tempScript.getAbsolutePath());
        } catch (Exception e) {
            throw new RuntimeException("生成或执行脚本失败", e);
        }
    }
    public static void main(String[] args) {
        String sshTemplate =
            "#!/usr/bin/expect -f\n" +
            "set timeout 20\n" +
            "spawn ssh %s@%s\n" +
            "expect {\n" +
            "    \"*yes/no*\" { send \"yes\\r\"; exp_continue }\n" +
            "    \"*password:*\" { send \"%s\\r\" }\n" +
            "}\n" +
            "expect \"$ \"\n" +
            "send \"%s\\r\"\n" +
            "expect \"$ \"\n" +
            "send \"exit\\r\"\n" +
            "expect eof";
        String result = generateAndExecuteScript(
            sshTemplate,
            "admin",           // 用户名
            "192.168.1.100",   // 主机
            "secret123",       // 密码
            "uptime"           // 要执行的命令
        );
        System.out.println("远程 uptime 结果:\n" + result);
    }
}

这种“模板+参数”的方式让 Expect 脚本具备了极强的灵活性,适合集成进配置管理系统或 DevOps 平台。

Expect 调试技巧

Expect 脚本调试有时比较棘手,因为看不到内部状态。以下是一些实用技巧:

1. 开启日志输出

在脚本开头加入:

log_user 1
exp_internal 1

这会输出详细的匹配过程。

2. 使用interact临时接管

在关键步骤后插入 interact,你可以手动接管终端,观察当前状态:

expect "password:"
send "$password\r"
interact  ;# 此时你可以手动输入命令调试

3. 捕获超时错误

expect {
    timeout {
        puts "操作超时,请检查网络或密码是否正确"
        exit 1
    }
    "*password:*" {
        send "$password\r"
    }
}

Expect 的局限性与替代方案

虽然 Expect 强大,但也存在一些限制:

问题说明
❌ 依赖终端输出格式如果程序输出变化(比如语言、提示符改变),脚本可能失效
❌ 不适合高并发每个 spawn 启动独立进程,大量并发时资源消耗大
❌ 安全性风险密码明文写在脚本中,容易泄露

替代方案推荐:

SSH 密钥认证 —— 免密码登录的最佳实践
教程参考:https://www.ssh.com/academy/ssh/keygen

Ansible —— 企业级自动化工具,支持无密码操作
官网:https://www.ansible.com/

Fabric (Python) —— 简洁的远程执行库
文档:http://www.fabfile.org/

JSch (Java SSH 库) —— 纯 Java 实现 SSH,无需 Expect

Java 替代方案:使用 JSch 库实现 SSH 自动化

如果你希望完全脱离 Expect,在 Java 内部实现 SSH 自动化,推荐使用 JSch

添加 Maven 依赖:

<dependency>
    <groupId>com.jcraft</groupId>
    <artifactId>jsch</artifactId>
    <version>0.1.55</version>
</dependency>

Java 示例代码:

import com.jcraft.jsch.*;
public class JschExample {
    public static void executeRemoteCommand(String host, String user, String password, String command) {
        try {
            JSch jsch = new JSch();
            Session session = jsch.getSession(user, host, 22);
            session.setPassword(password);
            // 跳过主机密钥检查(仅用于测试环境)
            session.setConfig("StrictHostKeyChecking", "no");
            session.connect();
            Channel channel = session.openChannel("exec");
            ((ChannelExec) channel).setCommand(command);
            channel.setInputStream(null);
            ((ChannelExec) channel).setErrStream(System.err);
            InputStream in = channel.getInputStream();
            channel.connect();
            byte[] tmp = new byte[1024];
            while (true) {
                while (in.available() > 0) {
                    int i = in.read(tmp, 0, 1024);
                    if (i < 0) break;
                    System.out.print(new String(tmp, 0, i));
                }
                if (channel.isClosed()) {
                    if (in.available() > 0) continue;
                    break;
                }
                Thread.sleep(1000);
            }
            channel.disconnect();
            session.disconnect();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        executeRemoteCommand("192.168.1.100", "admin", "secret123", "ls -la /tmp");
    }
}

优势:

Expect 高级技巧:正则匹配与多分支处理

Expect 支持使用 -re 参数进行正则表达式匹配,应对更复杂的输出场景。

例如,等待多种可能的提示:

expect {
    -re "(yes/no)|(continue connecting)" {
        send "yes\r"
        exp_continue
    }
    "*password:*" {
        send "$password\r"
    }
    "*denied*" {
        puts "认证失败!"
        exit 1
    }
    timeout {
        puts "连接超时"
        exit 1
    }
}

你也可以捕获匹配的内容:

expect -re {Welcome, (.*)!}
set username $expect_out(1,string)
puts "登录用户:$username"

实用 Expect 脚本合集

1. 自动备份远程 MySQL 数据库

#!/usr/bin/expect -f

set host [lindex $argv 0]
set dbuser [lindex $argv 1]
set dbpass [lindex $argv 2]
set dbname [lindex $argv 3]
set backup_file "/backups/${dbname}_$(date +%Y%m%d).sql"

spawn ssh admin@$host "mysqldump -u$dbuser -p$dbpass $dbname > $backup_file"

expect {
    "*password:*" { send "ssh_password\r" }
    timeout { puts "SSH 超时"; exit 1 }
}

expect eof
puts "备份完成:$backup_file"

2. 批量测试多台主机连通性

#!/usr/bin/expect -f

set timeout 5
set hosts {"192.168.1.100" "192.168.1.101" "192.168.1.102"}
set user "monitor"
set password "monitor123"

foreach host $hosts {
    puts "正在测试 $host..."

    spawn ssh $user@$host "echo 'OK'"

    expect {
        "*password:*" {
            send "$password\r"
            expect {
                "OK" { puts "$host ✅ 正常" }
                timeout { puts "$host ❌ 无响应" }
            }
        }
        timeout { puts "$host ❌ SSH 超时" }
    }

    expect eof
}

安全加固建议

虽然方便,但 Expect 脚本中的明文密码是安全隐患。以下是几种加固方法:

方法 1:从环境变量读取密码

set password $env(MY_SECRET_PASSWORD)

启动前设置:

export MY_SECRET_PASSWORD="real_password"
./script.exp

方法 2:从加密文件读取

结合 gpgopenssl 解密:

set password [exec echo mypass.enc | gpg --decrypt --quiet --batch --yes --passphrase-file key.txt]

方法 3:使用 Vault 或 Secret Manager

在 Java 中集成 HashiCorp Vault 获取密码,再传给 Expect 脚本。

Expect 性能优化建议

当需要并发执行多个 Expect 脚本时,注意以下几点:

  1. 避免阻塞主线程 —— 使用线程池异步执行
  2. 限制并发数 —— 避免同时打开过多 SSH 连接
  3. 设置合理超时 —— 避免僵尸进程
  4. 复用连接 —— 如可能,一次 SSH 执行多个命令

Java 并发执行示例:

import java.util.concurrent.*;
public class ConcurrentExpectRunner {
    private static final ExecutorService executor = Executors.newFixedThreadPool(5);
    public static Future<String> runAsync(String script, String... args) {
        return executor.submit(() -> ExpectExecutor.executeScript(script, args));
    }
    public static void main(String[] args) throws Exception {
        List<Future<String>> futures = new ArrayList<>();
        // 并发执行10台主机的磁盘检查
        for (int i = 100; i <= 110; i++) {
            String host = "192.168.1." + i;
            futures.add(runAsync("/scripts/check_disk.exp", host, "admin", "secret123"));
        }
        // 收集结果
        for (Future<String> future : futures) {
            System.out.println(future.get()); // 阻塞直到完成
        }
        executor.shutdown();
    }
}

Expect 与 Cron 结合实现定时任务

将 Expect 脚本加入 crontab,实现无人值守的周期性操作。

编辑定时任务:

crontab -e

添加一行(每天凌晨2点执行备份):

0 2 * * * /home/user/scripts/mysql_backup.exp 192.168.1.100 dbuser dbpass mydb >> /var/log/backup.log 2>&1

记得设置脚本权限和日志轮转!

Expect 在 DevOps 中的角色

在 CI/CD 流水线中,Expect 常用于:

虽然现代工具如 Ansible、Terraform 更受欢迎,但在“最后一公里”的特殊场景中,Expect 仍是不可替代的利器。

总结:何时该用 Expect?

场景推荐方案
简单一次性 交互✅ Expect 脚本
企业级自动化⚠️ 优先考虑 Ansible / SaltStack
Java 应用内集成✅ JSch / SSHJ 库
高安全性要求❌ 避免明文密码,改用密钥或 Vault
多平台兼容❌ Expect 仅限 Unix/Linux,Windows 需 Cygwin

未来展望:Expect 的演进方向

随着基础设施即代码(IaC)和 API 化趋势,Expect 的使用确实在减少。但它的思想 —— “程序模拟人类交互” —— 依然活跃在:

学习 Expect,不仅是学一个工具,更是理解“自动化”的本质。

结语

Expect 是 Linux 世界中一颗低调但璀璨的明珠。它不华丽,不时髦,却能在关键时刻解决别人束手无策的问题。无论是系统管理员、运维工程师,还是 Java 开发者,掌握 Expect 都能让你在自动化之路上走得更远、更稳。

以上就是Linux使用Expect脚本实现自动化交互操作的详细内容,更多关于Linux Expect自动化交互操作的资料请关注脚本之家其它相关文章!

您可能感兴趣的文章:
阅读全文