有没有办法将 stderr 重定向到在 bash、csh 和 dash 中工作的文件?

Is there a way to redirect stderr to a file that works in bash, csh and dash?

如果我不知道哪个 shell(bash,我该如何重定向 stderr(或 stdout+stderr)到一个文件csh, dash) 正在解释我的命令?

我在 Linux/FreeBSD/OSX 上的 C 代码 运行 需要通过 system() 函数调用外部程序,该函数将使用 /bin/sh 来解释提供的命令行。我想将该外部程序打印的消息捕获到 stderr 并将它们保存到一个文件中。问题是在不同的系统上,/bin/sh 指向不同的 shell,它们具有将 stderr 流重定向到文件的不同语法。

我发现最接近的是 bash 实际上理解 csh 风格的语法,用于将 stderr+stdout 重定向到一个文件:

some_program >& output.txt

但是 dash,这是 Ubuntu 上的默认 shell(即非常常见),不理解这种语法。

是否有一种 stderr 重定向的语法可以被所有常见的 shell 正确解释?或者,有没有办法告诉 system()(或其他一些类似的 C 函数?)使用 /usr/bin/env bash 而不是 /bin/sh 来解释提供的命令行?

在任何 POSIX-like 系统上,您可以使用

system("some_program > output.txt 2>&1");

这是因为 POSIX systemequivalent to calling sh, and POSIX sh supports this kind of redirection。这与打开系统终端的用户是否会看到 Csh 提示无关。

How do I redirect stderr (or stdout+stderr) to a file if I don't know which shell (bash, csh, dash) is interpreting my command?

你不知道。 Bourne-family shells 和 csh-family shells 具有不同的、不兼容的重定向 stderr 语法。事实上,cshtcsh 根本没有仅重定向 stderr 的语法——它们只能将它与 stdout 一起重定向。

如果您真的可以在任何 shell 中,那么您对做任何事情都非常满意。人们可以想象一个晦涩、深奥的 shell 和完全不兼容的语法。就此而言,即使是标准 shell 的不寻常配置也可能使您出错——例如,如果 IFS 变量在 Bourne-family shell 中设置为不寻常的值,那么您将无法执行任何未考虑到这一点的命令。

如果你能指望至少执行简单的命令,那么你可以在未知命令中执行已知的 shell 来处理你的命令,但对于似乎感兴趣。

Alternatively, is there a way to tell system() (or some other similar C function?) to use /usr/bin/env bash instead of /bin/sh to interpret the supplied command line?

不在 POSIX-conforming 系统上。 POSIX 明确指定 system() 函数通过使用 /bin/sh -c [the_command] 来执行命令。但这 不应该 是值得担心的事情,因为 /bin/sh 应该 是一个合格的 POSIX shell,或者至少非常接近一个。绝对应该是 Bourne-family shell,bashdash 都是,但 tcsh 绝对不是。

在 POSIX shell 中重定向标准错误流的方法是使用 2> 重定向运算符(这是适用于任意 文件描述符)。无论 shell /bin/sh 实际上是什么,都应该识别该语法,特别是 bashdash 都可以:

some_program 2> output.txt

您有一个错误的假设,即 /bin/sh 可以是 "alternate" shell 之类的 csh,它与标准 shell 语法不兼容。如果您有这样的系统设置,它将无法使用;没有 shell 脚本可以工作。几乎所有现代系统都试图至少在表面上符合 POSIX 标准,其中 sh 命令处理 POSIX 中指定的 Shell 命令语言,这大致是相当于历史上的 Bourne shell 和 bashdashash 等(通常安装为 /bin/sh 的 shell)都 99.9% 兼容。

你完全可以忽略csh之类的。它们从未安装为 sh,只有那些真正想要使用它们的人,或者因为一些邪恶的系统管理员设置登录 shell 默认而被困在使用它们作为他们的互动 shell 的人方式,永远不必关心他们。

我认为,还有另一种可能性值得一提:您可以在调用 system() 之前在 c-code 中打开要在 stderr 上重定向的文件。可以先dup()原来的stderr,然后再恢复。

fflush(stderr); // Flush pending output
int saved_stderr = dup(fileno(stderr));
int fd = open("output.txt", O_RDWR|O_CREAT|O_TRUNC, 0600);
dup2(fd, fileno(stderr));
close(fd);
system("some_program");
dup2(saved_stderr, fileno(stderr));
close(saved_stderr);

这应该根据需要执行输出重定向。

如果你不知道 shell...当然你不知道如何从它重定向,尽管你可以看到 $SHELL 的值有,并因此采取行动:

char *shell = getenv("SHELL");
if (*shell) { /* no SHELL variable defined */
    /* ... */
} else if (!strcmp(shell, "/bin/sh")) { /* bourne shell */
    /* ... */
} /* ... more shells */

不管你在问题中说了什么,重命名 /bin/sh 以使用另一个 shell 是很不寻常的,因为 shell 脚本使用依赖于它的语法。我知道的唯一情况是 bash(1),我只在 Linux 中看到过这种情况(值得注意的是,solaris 的最新版本),但是 bash(1) 的语法是语法的超集sh(1),使 运行 shell 脚本成为可能 sh(1)。例如,将 /bin/sh 重命名为 perl 会使您的系统可能完全无法使用,因为许多系统工具依赖于 /bin/sh 才能与 bourne 兼容 shell.

顺便说一句,system(3)库函数总是调用sh(1)作为命令解释器,所以使用它应该没有问题,但是没有捕获输出并处理它的解决方案通过 parent 过程(实际上,parent 过程是 sh(1)system(3) fork(2)s)

您可以做的另一件事是 popen(3) 一个进程。此调用为您提供了一个指向进程管道的 FILE 指针。如果你 popen(3) 它用于写入,你可以打开它的输入,如果你想要或读取它的输出,你可以打开它的输出。查看手册以获取详细信息,因为我现在不知道它是仅重定向其标准输出还是还重定向标准错误(我认为仅重定向标准输出,原因如下所述,并且仅当您 popen(3)它带有 "r" 标志。

FILE *f_in = popen("ps aux", "r");
/* read standard output of 'ps aux' command. */
pclose(f_in);  /* closes the descriptor and waits for the child to finish */

您可以做的另一件事是在 fork(2) 调用 child 之后和 exec(2) 调用之前重定向自己(这样您可以决定是否只需要 stdout 或者如果你还想 stderr 重定向回你):

int fd[2];
int res = pipe(fd);
if (res < 0) {
    perror("pipe");
    exit(EXIT_FAILURE);
}
if ((res = fork()) < 0) {
    perror("fork");
    exit(EXIT_FAILURE);
} else if (res == 0) { /* child process */
    dup2(fd[1], 1); /* redirect pipe to stdout */
    dup2(fd[1], 2); /* redirect pipe also to stderr */
    close(fd[1]); close(fd[0]); /* we don't need these */
    execvp(program, argv);
    perror("execvp");
    exit(EXIT_FAILURE);
} else { /* parent process */
    close(fd[1]); /* we are not going to write in the pipe */
    FILE *f_in = fdopen(fd[0]);
    /* read standard output and standard error from program from f_in FILE descriptor */
    fclose(f_in);
    wait(NULL); /* wait for child to finish */
}

您可以看到一个完整的示例(不是读取标准错误,但很容易添加 --- 您只需添加上面的第二个 dup2() 调用)here。该程序重复执行您在命令行上传递给它的命令。它需要访问子进程的输出以计算行数,因为在两次调用之间,程序增加与程序输出一样多的行数,以使下一次调用与上次调用的输出重叠。大家可以试玩,随意修改。

注意

在您的示例重定向中,当您使用 >& 时,您需要在 & 符号后添加一个数字,以表明您正在 dup()ing 哪个描述符。由于 > 之前的数字是可选的,因此 & 之后的数字是必填的。所以,如果你还没有使用它,准备接收一个错误(如果你正在重定向,你可能看不到 stderr) 有两个单独的输出描述符的想法是允许你重定向 stdout 同时,保留一个放置错误信息的渠道。