【问题标题】:How to pass variable to shell command in C?如何在C中将变量传递给shell命令?
【发布时间】:2015-08-05 00:28:08
【问题描述】:

例如,我编码:

fp = popen("wc -l < myfile", "r");

myfile 应该是解析到该项目的任何文件名。它可以是文件abc.txt123.txtxy.txt 等。

然后我想得到执行这个wc -l &lt; myfile的输出。但问题是我不知道C中的哪个函数可以帮助我将myfile的名称解析为这个shell命令,我也可以得到输出。 谁能给我一些建议?

编辑: 我要阅读的文件非常大。我想将它的数据读入一个数组。我不能使用列表来存储它,因为在列表中定位特定数据太慢了。问题是如果我用一维数组来malloc()内存空间给数组,笔记本上没有足够的连续内存空间。因此,我打算使用二维数组来存储它。所以我必须得到文件中的行数,然后通过log决定这个数组中每个维度的大小。

感谢所有回答。这个项目是关于读取两个文件。第一个文件比第二个文件大得多。第二个文件是这样的:

1   13  0
2   414 1
3   10  0
4   223 1
5   2   0

每行中的第三个数字称为“ID”。例如,num“1”的 ID 为 0,num“2”的 ID 为 1,num“3”的 ID 为“0”。 (忽略每行中间的数字) 第一个文件是这样的:

1   1217907
1   1217908
1   1517737
1   2
2   3
2   4
3   5
3   6

如果第一个文件中的每个 num 的 ID 为“0”,我应该将每行中的 num 都存储到数据结构数组中。例如,我们可以在第二个文件中看到 num "1" 的 ID 为 "0",所以我需要存储:

1   1217907
1   1217908
1   1517737
1   2

从我的第一个文件到数据结构数组。数字“2”的 ID 为“1”,但数字“3”的 ID 为“0”,数字“4”的 ID 为“1”,因此需要存储:2 3,但不要从我的第一个文件中存储 2 4。这就是为什么我需要使用数组来存储这两个文件。如果我使用两个数组来存储它们,我可以在数组属于第二个文件中快速检查这个num的ID是否为“0”,因为使用数组可以快速定位特定数据,索引可以直接是num的值。

【问题讨论】:

  • “没有足够的连续内存空间”真的没有意义。现代操作系统使用比物理内存大得多的虚拟内存空间。即使您没有足够的物理内存,也没有关系。
  • 即使文件是20G?
  • 我的大多数同学都未能分配足够的连续内存空间。他们告诉我他们的程序在用于测试他们程序的计算机上崩溃了。他们说电脑太旧了,没有足够的内存空间......
  • 如果是32位平台,那我可以看到。
  • 这次他们应该使用64位平台来测试我们的程序,但是在程序中我们需要大约8个数组来存储数据,每个数组存储不同种类的数据。

标签: c linux shell


【解决方案1】:

我认为,您需要先使用snprintf() 生成要传递给popen() 的字符串,然后您可以使用该字符串调用popen()

伪代码

char buf[32] = {0};
snprintf(buf, 32, "wc -l < %s", myfile);
fp = popen(buf, "r");

编辑

使其适用于任何长度的myfile

int len = strlen(myfile) + strlen("wc -l < ") + 1;
char *buf = malloc(len);
snprintf(buf, len, "wc -l < %s", myfile);
fp = popen(buf, "r");

...

free(buf);

注意:正如Ed Heal in the comment 所述,此处的32 仅用于演示 目的。您应该根据myfile 持有的字符串的长度,加上强制字符,加上空终止符来选择您的临时数组长度。

【讨论】:

  • myFile 多长时间 - 希望不会太长
  • @NatashaDutta - 为什么要使用 shell 等来执行此操作?似乎开销很大
  • 太棒了!谢谢!
  • 从安全角度来看,这是一个非常糟糕的消息;绝对没有尝试防止 shell 命令注入。考虑一个包含$(rm -rf /) 的文件名。
  • 真的 - 源代码在这里 - gnu.org/software/cflow/manual/html_node/… - 我的速度稍快,因为我避免寻找单词
【解决方案2】:

如果您不打算自己执行此操作(没有外壳),您应该这样做,至少以这样的方式传递文件名,外壳只会将其解释为数据而不是代码,以避免潜在的安全性事件。

setenv("filename", "myfile");            /* put filename in the environment */
fp = popen("wc -l <\"$filename\"", "r"); /* check it from your shell script */

【讨论】:

  • 我们是否假设wc 做了我们认为的事情。考虑它可能是路径中较早的shell脚本?!
  • 这很聪明并且被赞成,但它不是可重入的。
  • @jxh,确实;在一个理想的世界中,人们会在分叉之后和执行 shell 之前在子进程内执行setenv(),从而将其与进程的其余部分隔离开来。很遗憾 popen() 的设计没有考虑到用例(与您的 execv* 系列调用相反,它确实允许传入显式环境变量)。
【解决方案3】:

忘记popen - 自己做吧

FILE *f = fopen(argv[1], "r");
int lines = 0;
int ch;
while ((ch = fgetc(f)) != EOF) {
   if (c == '\n') lines++;
}

编辑 - 因为发帖人想要将整个文件加载到内存中

添加错误检查

FILE *f = fopen(argv[1], "r");
struct stat size;
fstat(fileno(f), &size);

char buf = malloc(size.st_size)
fread(buf, size.st_size, 1, f);
fclose(f);

【讨论】:

  • 这是示例问题的正确解决方案,但不适用于一般问题。
  • 一般问题是什么 - 我以为是读取文件中的数字行 - 为什么要生成 shell 等。
  • 一般问题是向popen() 调用的shell 命令发送任意文件名。命令不必是wc -l &lt;
  • 我要读取的文件非常大,所以我必须使用shell命令快速读取它以获得它的行号。
  • @beasone - 您已将问题从计算行数更改为分配空间
【解决方案4】:

以下所有代码均未经测试。如果我有时间测试,我会删除这个警告。

您可以为popen() 创建自己的包装器,以允许您形成任意命令。

FILE * my_popen (const char *mode, const char *fmt, ...) {
    va_list ap;
    int result = 511;

    for (;;) {
        char buf[result+1];

        va_start(ap, fmt);
        result = vsnprintf(buf, sizeof(buf), fmt, ap);
        va_end(ap);

        if (result < 0) return NULL;
        if (result < sizeof(buf)) return popen(buf, mode);
    }

    /* NOT REACHED */
    return NULL;
}

那么,你可以这样称呼它:

const char *filename = get_filename_from_input();
FILE *fp = my_popen("r", "%s < %s", "wc -l", filename);
if (fp) {
  /* ... */
  pclose(fp); /* make sure to call pclose() when you are done */
}

在这里,我们假设get_filename_from_input() 将文件名输入字符串转换为可安全使用的外壳程序。


将文件名可靠地修复成 shell 可以安全处理的东西是相当复杂的(并且容易出错)。自己打开文件更安全。但是,这样做之后,您可以将文件提供给命令,然后读出结果输出。问题是,您不能使用popen() 来完成此操作,因为标准popen() 仅支持单向通信。

Some variations of popen() exist that support bidirectional communication.

FILE * my_cmd_open (const char *cmd) {
    int s[2], p, status, e;
    if (socketpair(AF_UNIX, SOCK_STREAM, 0, s) < 0) return NULL;
    switch (p = fork()) {
    case -1: e = errno; close(s[0]); close(s[1]); errno = e; return NULL;
    case 0: close(s[0]); dup2(s[1], 0); dup2(s[1], 1); dup2(s[1], 2);
            switch (fork()) {
            case -1: exit(EXIT_FAILURE);
            case 0: execl("/bin/sh", "-sh", "-c", cmd, (void *)NULL);
                    exit(EXIT_FAILURE);
            default: exit(0);
            }
    default: for (;;) {
                 if (waitpid(p, &status, 0) < 0 && errno == EINTR) continue;
                 if (WIFEXITED(status) && WEXITSTATUS(status) == 0) break;
                 close(s[0]); close(s[1]); errno = EPIPE;
                 return NULL;
             }
    }
    close(s[1]);
    return fdopen(s[0], "r+");
}

要高效地将整个文件读入内存,您可以使用mmap()

void * mmap_filename (const char *filename, size_t *sz) {
    int fd = open(filename, O_RDONLY);
    if (fd < 0) return NULL;
    struct stat st;
    if (fstat(fd, &st) < 0) {
        close(fd);
        return NULL;
    }
    *sz = st.st_size;
    void *data = mmap(NULL, *sz, PROT_READ, MAP_PRIVATE, fd, 0);
    close(fd);
    return data != MAP_FAILED ? data : NULL;
}

那么,你可以这样称呼它:

size_t sz;
void *data = mmap_filename(filename, &sz);
if (data) {
    /* ... */
    munmap(data, sz);
}

上面的示例代码一次映射整个文件。但是,mmap() API 允许您将文件的某些部分从特定偏移量映射到文件中。

【讨论】:

  • 仍然对 shell 注入漏洞开放(例如,一个包含 '$(rm -rf /)'myfile,以重用示例)。
  • 仍然对 shell 注入漏洞开放,只是转义机制略有不同。
  • @CharlesDuffy:你的意思是文件名字符串是否也包含单引号。
  • 没错。在 POSIX 路径中,除 NUL 之外的任何字符都是合法的。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-01-08
  • 1970-01-01
  • 2013-06-26
  • 2012-04-24
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多