C语言执行shell命令

C语言执行shell命令

在 c/c++ 编程中常常需要执行一些终端命令,可以使用 system 函数,也可以采用 popen 函数。这两个函数都是 C 标准库的,因此跨平台。

system

https://www.man7.org/linux/man-pages/man3/system.3.html

system 函数用于执行一个命令,它会先启动一个 shell(如 /bin/sh,然后在 shell 中执行该命令。它会阻塞等待命令执行完毕,且无法获取命令的输出内容,也无法向命令传递输入。

#include <stdlib.h>
int system(const char *command);

参数

  • command: 要执行的命令。

返回值

  • 如果 command 为 NULL,且系统支持shell,返回非零值;否则返回 0。
  • 如果 command 不为 NULL:
    • 如果无法创建用来启动shell的子进程或者无法获取该子进程的状态,返回 -1 并设置 errno
    • 如果在子进程中无法启动shell,则返回退出状态码 127(需要使用宏如 WEXITSTATUS 来获取真正的返回码)。
    • 如果执行命令成功,则返回命令的退出状态码(需要使用宏如 WEXITSTATUS 来获取真正的返回码)。

注意

  • 由于底层通过 sh -c 实现,所以形如 system("sh -c \"ls -l\"") 的做法纯属画蛇添足,直接用 system("ls -l") 即可。

适用场景:只需简单执行命令(如创建目录 mkdir、清理临时文件 rm、调用脚本),不需要关心命令的输出或交互。

#include <stdlib.h>
#include <stdio.h>

int main() {
    printf("开始执行 ls 命令...\n");

    // 执行 ls -l 命令
    int ret = system("ls -l | wc");

    if (ret == -1) {
        perror("system 执行失败");
        return 1;
    }

    printf("命令执行完毕,返回值: %d\n", ret); // Linux上需要用 WEXITSTATUS 检查
    return 0;
}

popen

https://www.man7.org/linux/man-pages/man3/popen.3.html

popen 函数也会启动一个 shell 来执行命令,但它会同时创建一个管道。通过这个管道,你的程序可以读取命令的标准输出,或者向命令的标准输入写入数据。

#include <stdio.h>
FILE *popen(const char *command, const char *type);
int pclose(FILE *stream);

参数

popen

  • command: 要执行的命令。
  • type: 打开管道的模式:
    • "r" (read): 父进程读取命令的标准输出。
    • "w" (write): 父进程向命令的标准输入写入。
    • "e" (close-on-exec): 需要配合"r"或"w"使用,标识这个管道的fd将会在启动的子进程中自动关闭。

pclose:等待相关联的子进程结束,并返回命令的退出状态码。

返回值

popen:成功返回管道的 FILE 指针,失败返回 NULL 并设置 errno

pclose:成功返回命令的退出状态码,失败返回 -1 并设置 errno

注意

  • popen 打开的管道是单向的,要么读,要么写,不能同时读写。

  • 由于底层通过 sh -c 实现,所以形如 popen("sh -c \"ls -l\"", "r") 的做法纯属画蛇添足,直接用 popen("ls -l", "r") 即可。

  • 由于 popen 的原理是启动shell来执行命令,因此只要你的系统支持shell,则 popen 肯定返回成功(和 system 行为一致)。而如果要执行的命令不存在,则会在 pclose 收集退出状态码时发现错误。

#include <stdio.h>
#include <stdlib.h>

#ifdef _WIN32
#define popen _popen
#define pclose _pclose
#endif

int main() {
    FILE *fp;
    char buffer[256];

    // 示例1:演示读取命令输出
    {
        // 以读模式打开管道,执行 ls -l
        fp = popen("ls -l | wc", "r");
        if (fp == NULL) {
            perror("popen 失败");
            return 1;
        }

        // 像读文件一样读取命令的输出
        printf("命令输出如下:\n");
        while (fgets(buffer, sizeof(buffer), fp) != NULL) {
            printf("%s", buffer);
        }

        int status = pclose(fp);
        printf("\n命令执行完毕,退出状态: %d\n", status); // Linux上需要用 WEXITSTATUS 检查
    }

    // 示例2:演示向命令写入数据
    {
        fp = popen("sort", "w");
        if (fp == NULL) {
            perror("popen 失败");
            return 1;
        }

        // 向管道写入数据 (即向 sort 的标准输入写入)
        fprintf(fp, "Banana\n");
        fprintf(fp, "Apple\n");
        fprintf(fp, "Orange\n");
        fprintf(fp, "Grape\n");
        fprintf(fp, "Cherry\n");

        int status = pclose(fp);
        printf("\n命令执行完毕,退出状态: %d\n", status); // Linux上需要用 WEXITSTATUS 检查
    }

    return 0;
}

直接使用系统API

由于 systempopen 会启动shell,因此存在shell命令注入的风险。因此最好的方式实际上是直接用系统API,从而不经过shell解析,不存在命令注入的风险。

只是实践上可能需要自行进行跨平台封装,或者用三方库,如 Boost.Process、subprocess.h。

UNIX

使用 fork() + execvp()

exec 家族函数是直接替换当前进程的内存映像来运行新程序,不经过 Shell 解析

Windows

使用 CreateProcess()

shell命令注入

无论是 system 还是 popen,底层都是通过启动 Shell 来解析和执行命令的。因此如果你的命令字符串中包含了来自用户的输入,极易引发命令注入漏洞。

反面教材

char filename[128];
scanf("%127s", filename); // 用户输入: test.txt; rm -rf /

// 危险!Shell 会先创建文件,然后执行 rm -rf /
popen(sprintf("touch %s", filename), "r"); 

安全建议

  1. 尽量不要使用 systempopen 处理包含用户输入的命令。
  2. 如果必须执行外部程序并传参,请使用 fork + exec 系列函数(如 execvp)。exec 是直接执行程序,不经过 Shell 解析,因此不存在命令注入风险。
  3. 如果一定要用 popen/system,必须对用户输入进行严格的过滤和转义(例如只允许字母数字,过滤掉 ; | & $ 等特殊字符)。
posted @ 2026-05-27 11:31  3的4次方  阅读(10)  评论(0)    收藏  举报