You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

如何在自制C++ Shell中实现Linux的script命令

很棒的需求!要在自制C++ Shell里实现Linux script命令的核心功能,本质上是要搭建一个伪终端(PTY)中间层,同时捕获用户输入和命令执行的输出,还要让被执行的命令以为自己在和真实终端交互。下面是具体的实现思路和关键代码片段,帮你一步步搞定:

核心原理先理清

script命令的工作逻辑其实很清晰:

它创建一个伪终端(PTY),让目标Shell/程序把PTY当作自己的标准输入输出;同时script自身作为中间代理,一边把用户的输入转发给PTY,一边把PTY返回的输出回显给用户,并且全程把双向数据流写入日志文件。

分步实现指南

1. 创建并初始化伪终端(PTY)

首先要调用POSIX标准的PTY相关函数,创建主从设备对:

#include <fcntl.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <iostream>

int main() {
    // 打开PTY主设备
    int master_fd = posix_openpt(O_RDWR);
    if (master_fd == -1) {
        perror("Failed to open PTY master");
        exit(EXIT_FAILURE);
    }

    // 设置PTY从设备的权限
    if (grantpt(master_fd) == -1) {
        perror("Failed to grant PTY access");
        close(master_fd);
        exit(EXIT_FAILURE);
    }

    // 解锁PTY从设备,允许打开
    if (unlockpt(master_fd) == -1) {
        perror("Failed to unlock PTY");
        close(master_fd);
        exit(EXIT_FAILURE);
    }

    // 获取PTY从设备的路径(比如 /dev/pts/2)
    const char* slave_path = ptsname(master_fd);
    if (slave_path == nullptr) {
        perror("Failed to get PTY slave path");
        close(master_fd);
        exit(EXIT_FAILURE);
    }
    // ... 后续代码
}

2. Fork子进程并绑定到PTY从设备

接下来要创建子进程,让它把标准输入/输出/错误重定向到PTY从设备,然后启动目标Shell:

pid_t pid = fork();
    if (pid == -1) {
        perror("Fork failed");
        close(master_fd);
        exit(EXIT_FAILURE);
    }

    if (pid == 0) { // 子进程逻辑
        // 创建新会话,让子进程成为会话组长,PTY成为控制终端
        setsid();

        // 打开PTY从设备
        int slave_fd = open(slave_path, O_RDWR);
        if (slave_fd == -1) {
            perror("Failed to open PTY slave");
            exit(EXIT_FAILURE);
        }

        // 重定向标准IO到PTY从设备
        dup2(slave_fd, STDIN_FILENO);
        dup2(slave_fd, STDOUT_FILENO);
        dup2(slave_fd, STDERR_FILENO);

        // 关闭不需要的文件描述符
        close(slave_fd);
        close(master_fd);

        // 启动默认Shell(优先用环境变量SHELL,否则用bash)
        char* shell = getenv("SHELL");
        if (shell == nullptr) shell = "/bin/bash";
        execl(shell, shell, nullptr);

        // 如果execl失败,输出错误
        perror("Failed to execute shell");
        exit(EXIT_FAILURE);
    }

    // 父进程保留master_fd用于和子进程通信
    // ...

3. 父进程监听双向数据流并记录日志

父进程需要同时处理两个数据源:用户的终端输入,以及PTY主设备返回的子进程输出。这里用select()实现多路复用,确保不会阻塞在单一输入上:

#include <sys/select.h>
#include <fstream>
#include <cstring>
#include <sys/wait.h>
#include <termios.h>

    // 保存原始终端属性,退出前恢复
    struct termios original_attr;
    tcgetattr(STDIN_FILENO, &original_attr);
    atexit([&]() {
        tcsetattr(STDIN_FILENO, TCSANOW, &original_attr);
    });

    // 设置终端为原始模式,捕获所有输入字符(包括特殊控制符)
    struct termios raw_attr = original_attr;
    cfmakeraw(&raw_attr);
    tcsetattr(STDIN_FILENO, TCSANOW, &raw_attr);

    // 打开日志文件(默认名为typescript,和标准script一致)
    std::ofstream log_file("typescript", std::ios::binary);
    if (!log_file.is_open()) {
        perror("Failed to open log file");
        close(master_fd);
        kill(pid, SIGTERM);
        exit(EXIT_FAILURE);
    }

    // 写入日志开头信息(模仿标准script)
    time_t now = time(nullptr);
    log_file << "Script started on " << ctime(&now);

    fd_set read_fds;
    char buffer[1024];
    ssize_t bytes_read;

    while (true) {
        FD_ZERO(&read_fds);
        FD_SET(STDIN_FILENO, &read_fds);
        FD_SET(master_fd, &read_fds);
        int max_fd = std::max(STDIN_FILENO, master_fd) + 1;

        // 等待有数据可读
        int ret = select(max_fd, &read_fds, nullptr, nullptr, nullptr);
        if (ret == -1) {
            perror("Select failed");
            break;
        }

        // 处理用户输入:转发给PTY,同时写入日志
        if (FD_ISSET(STDIN_FILENO, &read_fds)) {
            bytes_read = read(STDIN_FILENO, buffer, sizeof(buffer));
            if (bytes_read <= 0) break; // 用户输入结束(比如Ctrl+D)

            write(master_fd, buffer, bytes_read);
            log_file.write(buffer, bytes_read);
            log_file.flush();
        }

        // 处理PTY输出:回显给用户,同时写入日志
        if (FD_ISSET(master_fd, &read_fds)) {
            bytes_read = read(master_fd, buffer, sizeof(buffer));
            if (bytes_read <= 0) break; // 子进程退出

            write(STDOUT_FILENO, buffer, bytes_read);
            log_file.write(buffer, bytes_read);
            log_file.flush();
        }
    }

    // 写入日志结尾信息
    now = time(nullptr);
    log_file << "\nScript done on " << ctime(&now);

    // 清理资源
    log_file.close();
    close(master_fd);
    waitpid(pid, nullptr, 0);

    return 0;
}
关键注意事项
  • 信号处理:比如SIGINT(Ctrl+C)这类信号会发给父进程,你可以设置信号处理函数,把信号转发给子进程,避免父进程意外退出。
  • 终端属性恢复:一定要在退出前恢复原始终端属性,否则终端可能会出现输入不回显等异常。
  • 错误处理:每个系统调用都要检查返回值,比如read()write()可能因为各种原因失败,要及时清理资源并退出。
  • 日志格式:标准script会包含时间戳和会话信息,你可以根据需求调整日志的开头和结尾内容。

内容的提问来源于stack exchange,提问作者Aayush Upadhyaya

火山引擎 最新活动