将 stdout 和 stderr 复制到日志文件,仅在控制台上显示 stderr 输出并将 stderr 消息记录到另一个文件中

将 stdout 和 stderr 复制到日志文件,仅在控制台上显示 stderr 输出并将 stderr 消息记录到另一个文件中

我遇到一种情况,我需要按以下方式在 bash 脚本中进行输出重定向:

  1. 在控制台上仅应显示错误消息
  2. 错误消息还应该记录到一个特定文件中,例如 errout.log
  3. 标准输出和错误消息都应记录到文件中,例如 allout.log
  4. 不是那么重要,很高兴拥有,但可能只需付出很多努力就可以实现 - allout.log 中的顺序可能非常相似,或者与消息出现的顺序完全相同。

我在其他类似提问和回答的问题中没有发现完全相同的情况。

答案1

在 zsh 中可能是foo 2>&2 2> err.log > all.log 2>&1

在 bash 中,这可能有效:

foo 2>&1 >> all.log | tee -a all.log err.log >&2

或者

{ foo >> all.log; } 2>&1 | tee -a all.log err.log >&2

答案2

我想不出基于 shell 的解决方案,所以我建议这样做。其他人提供了更好的基于 shell 的解决方案。我将把它留在这里以防万一有人觉得有趣,但其他答案更好。

这是一个 C 程序,可以完成您的任务:

#include <poll.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <fcntl.h>

// Some wrappers around system calls to hide error handling
static void Pipe(int pipe_fds[]);
static pid_t Fork();
static void Dup2(int old_fd, int new_fd);
static void Execvp(const char *file, char *const argv[]);

int main(int argc, char* argv[])
{
    if (argc < 2) {
        fprintf(stderr, "usage: %s <script> [<arg>, ...]\n", argv[0]);
        return EXIT_FAILURE;
    }

    // Create a pipe to handle stdout
    int out_pipe[2];
    Pipe(out_pipe);

    // Create a second pipe to handle stderr
    int err_pipe[2];
    Pipe(err_pipe);

    if (Fork() == 0) { // Child
        // Wire the child's stdout stream to the write end of out_pipe
        close(out_pipe[0]);
        Dup2(out_pipe[1], STDOUT_FILENO);
        close(out_pipe[1]);

        // Wire the child's stderr stream to the write end of err
        close(err_pipe[0]);
        Dup2(err_pipe[1], STDERR_FILENO);
        close(err_pipe[1]);

        // Invoke the target program with the remaining args
        Execvp(argv[1], argv + 1);
    }

    // Parent only from here on out

    // Close the write ends of the pipes
    close(out_pipe[1]);
    close(err_pipe[1]);

    // Set up the file descriptors and events we're interested in monitoring
    struct pollfd poll_fds[] = {
        { .fd = out_pipe[0], .events = POLLIN },
        { .fd = err_pipe[0], .events = POLLIN },
    };

    int incomplete_fds = 0;

    while (incomplete_fds != 2 && poll(poll_fds, 2, -1) > 0) {
        for (int i = 0; i < 2; ++i) {
            // Is this file descriptor readable?
            if (poll_fds[i].revents & POLLIN) {
                char buffer[4096];
                const ssize_t num_bytes = read(poll_fds[i].fd, buffer, sizeof(buffer));

                // (3) write both stdout and stderr our stderr
                write(STDERR_FILENO, buffer, num_bytes);

                if (i == 1) { // if this was a write to stderr
                    // (1) write standard error to out stdout
                    write(STDOUT_FILENO, buffer, num_bytes);
                }

            }

            if (poll_fds[i].revents & (POLLHUP | POLLNVAL)) {
                // Don't expect anything more from this
                poll_fds[i].events = 0;
                poll_fds[i].fd = -1;
                ++incomplete_fds;
            }
        }
    }

    return EXIT_SUCCESS;
}

static void Pipe(int pipe_fds[])
{
    if (pipe(pipe_fds) < 0) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
}

static pid_t Fork()
{
    const pid_t pid = fork();

    if (pid < 0) {
        perror("fork");
        exit(EXIT_FAILURE);
    }

    return pid;
}

static void Dup2(const int old_fd, const int new_fd)
{
    if (dup2(old_fd, new_fd) < 0) {
        perror("pipe");
        exit(EXIT_FAILURE);
    }
}

static void Execvp(const char *file, char *const argv[])
{
    execvp(file, argv);
    perror("execvp");

    exit(EXIT_FAILURE);
}

该程序将您想要运行的应用程序作为参数,以及您想要传递给该应用程序的任何参数。它创建两个管道,一个用于捕获应用程序写入的内容stdout,另一个用于捕获stderr.然后fork(),它将管道连接到子级的输出流,然后使用任何参数调用应用程序。

然后父进程可以从这些管道中读取数据;它用于poll()等待任何这些文件描述符上的活动。程序从应用程序的stdoutstderr流读取的任何内容都会写入stderr.程序从stderr应用程序流中读取的任何内容都会写入stdout(这看起来是倒退的,但它简化了重定向)。

我可以用以下命令编译C程序

$ gcc prog.c -o pipe_wrapper

现在,假设我有一些东西可以生成输出到stdoutstderr

#!/bin/bash
# ex.sh

echo stdout
echo stderr 1>&2

然后我可以运行:

# Note that the content written to stderr appears on the console
$ ./pipe_wrapper ./ex.sh 2> allout.log | tee errout.log
stderr
$

# That content written to stderr is captured in errout.log
$ cat errout.log
stderr
$

# And that everything written to either stdout of stderr is captured
# in allout.log
$ cat allout.log
stdout
stderr
$

我不能保证这会保留顺序;如果应用程序快速连续地写入stderr和,则该程序可能会先处理写入的内容,然后再处理写入的内容。stdoutstdoutstderr

相关内容