有没有一种方法可以在bash、csh和dash中将stderr重定向到文件?

4
如何将标准错误(或标准输出+标准错误)重定向到文件中,如果不知道哪个 shell(bash、csh、dash)在解析命令?
我的 C 代码在 Linux/FreeBSD/OSX 上运行,需要通过 system() 函数调用外部程序,该程序会使用 /bin/sh 来解析提供的命令行。我想捕获该外部程序打印的标准错误消息并保存到文件中。问题是,在不同的系统上 /bin/sh 指向不同的 shell,这些 shell 有不同的语法来将标准错误流重定向到文件中。
我找到的最接近的东西是,bash 实际上可以理解 csh 风格的语法来将标准错误+标准输出重定向到文件中:
some_program >& output.txt

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

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


1
我会将这个问题简化为“有没有一种在csh中有效的重定向stderr的方法?”按照下面给出的建议,忽略csh。 - William Pursell
确实,意识到/bin/sh不能指向csh,因此我应该使用POSIX(类似于bash)的重定向语法解决了问题!感谢大家提供详细的答案! - Kirill Sokolovsky
@thatotherguy的示例非常有用,与我的直觉相反,当/bin/sh指向dash时,system("some_program > output.txt 2>&1");具有期望的效果,而system("some_program 2>&1 > output.txt");则无法将stderr重定向到文件。 - Kirill Sokolovsky
@WilliamPursell,这将是一个略有不同的问题,因为它将是针对csh的特定问题。我的问题是关于一种可移植的方法来重定向stderr,而正确的答案确实是使用类似于bash的语法并忘记csh - Kirill Sokolovsky
1
@KirillSokolovsky 每当你在理解 shell 重定向时遇到困难,就把它们看作变量赋值,因为这就是它们的工作原理:>output 2>&1 就相当于 fd1="output.txt"; fd2=fd1,而 2>&1 >output 则是 fd2=fd1; fd1="output.txt"。在 C 中,你不会被 a = "foo"; b = ab = a; a = "foo" 之间的区别搞混吧?;-) - user10678532
5个回答

6
您有一个错误的假设,即/bin/sh可以作为“备用”shell,例如与csh不兼容的shell。如果您安装了这样的系统,它将无法使用;几乎所有现代系统都尝试表面上符合POSIX标准,其中sh命令处理在POSIX中指定的Shell Command Language,它大致相当于历史悠久的Bourne shell以及bashdashash等(通常安装为/bin/sh)都是99.9%兼容的。
您可以完全忽略csh和类似的内容。它们从未被安装为sh,只有真正想要使用它们或因为一些邪恶的系统管理员设置了登录shell默认值而被迫使用它们的人才需要关心它们。

4
在任何类POSIX系统上,您都可以使用。
system("some_program > output.txt 2>&1");

这是因为 POSIX system 等同于调用 sh,而 POSIX sh 支持这种重定向。无论用户是否在系统上打开终端并看到 Csh 提示符,都可以使用此功能。

4
如果我不知道使用的是哪个 shell(如 bash、csh、dash),我想要将stderr(或stdout+stderr)重定向到文件中,该怎么办?
你不能这样做。Bourne家族的shell和csh家族的shell具有不同且不兼容的语法来重定向stderr。事实上,csh和tcsh根本没有用于仅重定向stderr的语法 - 它们只能与stdout一起重定向。
如果您真的可能使用任何shell,则在执行任何操作方面都会非常困难。人们可以想象一个晦涩而深奥的shell,其具有完全不兼容的语法。此外,即使是标准shell的不寻常配置也可能会使您遇到困难 - 例如,在Bourne族shell中设置IFS变量为不寻常的值,那么您将无法执行不考虑它的任何命令。
如果您可以保证至少执行简单命令,则可以在未知的shell中执行已知shell以处理您的命令,但好像并不需要这样做。
或者,是否有一种方法可以告诉system() (或其他类似C函数?)使用/usr/bin/env bash而不是/bin/sh来解释提供的命令行?
在符合POSIX的系统上不行。POSIX明确规定了system()函数通过使用/bin/sh -c [the_command]来执行命令。但这不应该是需要担心的事情,因为/bin/sh应该是符合POSIX标准的shell,或者至少非常接近。绝对应该是Bourne家族的shell,bash和dash都是,但tcsh绝对不是。
在POSIX shell中重定向标准错误流的方法是使用2>重定向运算符(这是适用于任何文件描述符的更一般重定向功能的特例)。无论/bin/sh实际上是什么样的shell,都应该识别该语法,特别是bash和dash都是如此:
some_program 2> output.txt

1

我认为还有一个值得一提的可能性:在调用system()之前,您可以在c代码中打开要重定向到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);

这应该按照您的需求执行输出重定向。


1
如果你不了解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)语法的超集,因此可以在其中运行为sh(1)制作的shell脚本。例如,将/bin/sh重命名为perl可能会使您的系统完全无法使用,因为许多系统工具依赖于/bin/sh是一个Bourne兼容的shell。
顺便说一句,system(3)库函数始终调用sh(1)作为命令解释器,因此使用它应该没有问题,但是没有解决方案可以捕获输出并由父进程处理它(实际上,父进程是system(3)fork(2)的sh(1))。

另一件你可以做的事情是使用popen(3)打开一个进程。此调用会给你一个指向进程管道的FILE指针。如果你想写入,那么你需要将其输入popen(3),如果你想读取输出,那么你需要将其输出popen(3)。请查看手册以获取详细信息,因为我现在不知道它是否仅重定向标准输出还是也重定向标准错误(我认为只重定向标准输出,原因将在下面讨论,并且仅在你使用"r"标志进行popen(3)时才会这样)。

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) 子进程之后,在 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()调用)。该程序重复执行您在命令行中传递给它的命令。它需要访问子进程的输出来计算行数,因为在调用之间,程序将向上移动与程序输出相同数量的行,以使下一个调用重叠上一个调用的输出。您可以尝试并进行修改,以满足您的需求。
注意:在您的样本重定向中,当您使用>&时,您需要在符号“&”后添加一个数字,以指示您正在dup()ing的描述符。由于“&”前的数字是可选的,“&”后的数字是必需的。因此,如果您没有使用它,则准备接收一个错误消息(如果您正在重定向stderr,则可能看不到它)。拥有两个单独的输出描述符的想法是允许您重定向stdout ,同时保留一个通道以放置错误消息。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接