如何在C语言中以编程方式实现“tee”功能?

12

我想在C语言中以编程方式实现'tee'的功能,使得我的stdout输出既能够到标准输出流,又能够记录到一个日志文件中,而不是通过命令行重定向实现。这需要同时适用于我的代码和所有输出到stdout的链接库。有没有什么方法可以实现这个功能?


我可以问一下你需要这样的解决方案的更具体细节吗?是为了节省调试时间吗? - lorenzog
5个回答

7

您可以使用popen()函数来调用tee程序。

或者您可以使用fork()函数,并通过子进程将stdout进行管道传输,例如下面这个示例(该示例是我写的一个真实程序,所以它可行!):

void tee(const char* fname) {
    int pipe_fd[2];
    check(pipe(pipe_fd));
    const pid_t pid = fork();
    check(pid);
    if(!pid) { // our log child
        close(pipe_fd[1]); // Close unused write end
        FILE* logFile = fname? fopen(fname,"a"): NULL;
        if(fname && !logFile)
            fprintf(stderr,"cannot open log file \"%s\": %d (%s)\n",fname,errno,strerror(errno));
        char ch;
        while(read(pipe_fd[0],&ch,1) > 0) {
            //### any timestamp logic or whatever here
            putchar(ch);
            if(logFile)
                fputc(ch,logFile);
            if('\n'==ch) {
                fflush(stdout);
                if(logFile)
                    fflush(logFile);
            }
        }
        putchar('\n');
        close(pipe_fd[0]);
        if(logFile)
            fclose(logFile);
        exit(EXIT_SUCCESS);
    } else {
        close(pipe_fd[0]); // Close unused read end
        // redirect stdout and stderr
        dup2(pipe_fd[1],STDOUT_FILENO);  
        dup2(pipe_fd[1],STDERR_FILENO);  
        close(pipe_fd[1]);  
    }
}

+1 表示不假设任何事情(例如,上面的代码很容易适应于接受已经打开的文件描述符而不是文件名作为参数) - Michael
tee("tee.txt"); system("ftp"); --> 看上去没有打印标准输入或输出,你有什么想法吗? - Victor
一旦我删掉了:if('\n'==ch),它就可以打印了。我猜它在等待\n。现在,除非我按下回车键,否则tee函数似乎会被卡住。 - Victor

6
"popen() tee"的答案是正确的。这是一个完全实现这一功能的示例程序:
#include "stdio.h"
#include "unistd.h"

int main (int argc, const char * argv[])
{
    printf("pre-tee\n");

    if(dup2(fileno(popen("tee out.txt", "w")), STDOUT_FILENO) < 0) {
        fprintf(stderr, "couldn't redirect output\n");
        return 1;
    }

    printf("post-tee\n");

    return 0;
}

解释:

popen() 返回一个 FILE*,但 dup2() 需要一个文件描述符(fd),因此 fileno()FILE* 转换为 fd。然后,dup2(..., STDOUT_FILENO) 表示用来替换 stdout 的 fd 是从 popen() 得到的。

也就是说,你会产生一个子进程(popen),它将所有的输入都复制到 stdout 和一个文件中,然后将你的 stdout 重定向到该进程。


2
您可以使用pipe(2)dup2(2)将您的标准输出连接到一个文件描述符,然后可以有一个单独的线程监视该文件描述符,并将其收到的所有内容写入日志文件和原始的标准输出(通过dup2保存到另一个文件描述符中再连接管道之前)。但是需要一个后台线程。
实际上,我认为vatine建议的popen tee方法可能更简单和更安全(只要您不需要对日志文件进行任何额外操作,如时间戳或编码等)。

1

您可以使用forkpty()exec()来执行带有参数的监视程序。forkpty()返回一个文件描述符,该文件描述符被重定向到程序的标准输入和标准输出。无论写入文件描述符的内容是什么,都将作为程序的输入。程序写入的任何内容都可以从文件描述符中读取。

第二部分是循环读取程序的输出并将其写入文件,同时将其打印到标准输出。

示例:

pid = forkpty(&fd, NULL, NULL, NULL);
if (pid<0)
    return -1;

if (!pid) /* Child */
{
execl("/bin/ping", "/bin/ping", "-c", "1", "-W", "1", "192.168.3.19", NULL);
}

/* Parent */
waitpid(pid, &status, 0);
return WEXITSTATUS(status);

但这意味着他要从另一个程序中运行自己的程序,不是吗?想必他想避免这种情况,否则他就会愿意使用tee了。 - Benj

-2

在C语言中没有简单的方法来实现这个。我猜最简单的方法是使用popen(3)调用tee命令和所需的日志文件作为参数,然后将新打开的FILE*文件描述符重定向到fd 1。

但是看起来有点丑陋,我必须说我还没有尝试过这个方法。


尝试过这样做,会导致竞态条件。问题可能是tee有时会先打印。 - PALEN
正如我所说的,“未经测试,看起来很丑陋”。如果您使用GNU libc,则有一种创建自己的FILE *-类似对象的功能,您可以使用它,但这将导致不可移植性。 - Vatine

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