将标准输出/标准错误重定向到一个字符串

72

之前有很多关于将 stdout/stderr 重定向到文件的问题。有没有一种方法可以将 stdout/stderr 重定向到一个字符串中?

6个回答

74

是的,你可以将它重定向到一个 std::stringstream

std::stringstream buffer;
std::streambuf * old = std::cout.rdbuf(buffer.rdbuf());

std::cout << "Bla" << std::endl;

std::string text = buffer.str(); // text will now contain "Bla\n"

您可以使用一个简单的守卫类来确保缓冲区始终被重置:

struct cout_redirect {
    cout_redirect( std::streambuf * new_buffer ) 
        : old( std::cout.rdbuf( new_buffer ) )
    { }

    ~cout_redirect( ) {
        std::cout.rdbuf( old );
    }

private:
    std::streambuf * old;
};

7
好的,你只需要用 std::cerr 替代原来的输出流即可,其他步骤一样。 - Björn Pollex
11
抱歉,我的'C'(或者说'C++')已有些生疏了,但当程序中其他部分使用'fprintf(stderr, ...)'、'fprintf(stdout, ...)'或'printf()'时,这并没有什么帮助,是吗?而且,对于'write(1, ...)'或'write(2, ...)'也没有帮助,尽管那不是问题的所在 :-) - Christian.K
9
@Christian: 不,它不行。这个解决方案仅适用于C++ iostreams。 - Björn Pollex
2
@MinimusHeximus:这就是关键——输出被重定向到一个字符串,而不是屏幕上。要再次打印到屏幕上,您必须将原始缓冲区恢复为 cout - Björn Pollex
1
@Gizmo 我认为这个邪恶的怪物应该适用于printf和system()以及几乎所有在Linux上的操作... https://wandbox.org/permlink/okEe7xDW4ZtaJZtO - 它通过复制stdout,然后关闭stdout,接着创建一个tmpfile()并希望它能获得id 1 / 成为新的stdout来实现... - hanshenrik
显示剩余4条评论

43
你可以使用这个类:
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>
#include <string>

class StdCapture
{
public:
    StdCapture(): m_capturing(false), m_init(false), m_oldStdOut(0), m_oldStdErr(0)
    {
        m_pipe[READ] = 0;
        m_pipe[WRITE] = 0;
        if (_pipe(m_pipe, 65536, O_BINARY) == -1)
            return;
        m_oldStdOut = dup(fileno(stdout));
        m_oldStdErr = dup(fileno(stderr));
        if (m_oldStdOut == -1 || m_oldStdErr == -1)
            return;

        m_init = true;
    }

    ~StdCapture()
    {
        if (m_capturing)
        {
            EndCapture();
        }
        if (m_oldStdOut > 0)
            close(m_oldStdOut);
        if (m_oldStdErr > 0)
            close(m_oldStdErr);
        if (m_pipe[READ] > 0)
            close(m_pipe[READ]);
        if (m_pipe[WRITE] > 0)
            close(m_pipe[WRITE]);
    }


    void BeginCapture()
    {
        if (!m_init)
            return;
        if (m_capturing)
            EndCapture();
        fflush(stdout);
        fflush(stderr);
        dup2(m_pipe[WRITE], fileno(stdout));
        dup2(m_pipe[WRITE], fileno(stderr));
        m_capturing = true;
    }

    bool EndCapture()
    {
        if (!m_init)
            return false;
        if (!m_capturing)
            return false;
        fflush(stdout);
        fflush(stderr);
        dup2(m_oldStdOut, fileno(stdout));
        dup2(m_oldStdErr, fileno(stderr));
        m_captured.clear();

        std::string buf;
        const int bufSize = 1024;
        buf.resize(bufSize);
        int bytesRead = 0;
        if (!eof(m_pipe[READ]))
        {
            bytesRead = read(m_pipe[READ], &(*buf.begin()), bufSize);
        }
        while(bytesRead == bufSize)
        {
            m_captured += buf;
            bytesRead = 0;
            if (!eof(m_pipe[READ]))
            {
                bytesRead = read(m_pipe[READ], &(*buf.begin()), bufSize);
            }
        }
        if (bytesRead > 0)
        {
            buf.resize(bytesRead);
            m_captured += buf;
        }
        m_capturing = false;
        return true;
    }

    std::string GetCapture() const
    {
        std::string::size_type idx = m_captured.find_last_not_of("\r\n");
        if (idx == std::string::npos)
        {
            return m_captured;
        }
        else
        {
            return m_captured.substr(0, idx+1);
        }
    }

private:
    enum PIPES { READ, WRITE };
    int m_pipe[2];
    int m_oldStdOut;
    int m_oldStdErr;
    bool m_capturing;
    bool m_init;
    std::string m_captured;
};

当你需要开始捕获时,请调用BeginCapture()
当你需要停止捕获时,请调用EndCapture()
调用GetCapture()来检索捕获的输出


4
我不太熟悉管道,但内核中的管道缓冲区有多大?换句话说,在调用read()之前,您可以通过write()传递多少字节而不被阻塞? - user3458
6
+1 对于一个微不足道的问题提供一个非常复杂的解决方案! :) - Nim
18
支持一个可以处理所有标准输出/错误输出的解决方案(与已接受的解决方案不同,后者仅处理cout/cerr)。 - zpasternack
3
O_BINARY和其他一些标识符在我的编译器中是未知的。 - Minimus Heximus
2
@MinimusHeximus:我相信这是为 MSVC 编译器编写的。_pipe() 函数在 MSDN 中有文档记录。 - macetw
显示剩余2条评论

20
为了提供一个线程安全和跨平台的解决方案,我已经采用了rmflow的方法来创建一个类似的接口。由于这个类修改全局文件描述符,所以我将其改为一个受互斥保护的静态类,以防止多个实例干扰全局文件描述符。此外,rmflow的答案没有清理所有使用过的文件描述符,这可能会导致在一个应用程序中使用许多BeginCapture()和EndCapture()调用时无法打开新的输出流或文件。此代码已在Windows 7/8、Linux、OSX、Android和iOS上进行了测试。
注意:如果要使用std::mutex,您必须编译针对c++11。如果您不想或不能使用c++11,则可以完全删除互斥调用(牺牲线程安全性),或者找到一个传统的同步机制来完成工作。
#ifdef _MSC_VER
#include <io.h>
#define popen _popen 
#define pclose _pclose
#define stat _stat 
#define dup _dup
#define dup2 _dup2
#define fileno _fileno
#define close _close
#define pipe _pipe
#define read _read
#define eof _eof
#else
#include <unistd.h>
#endif
#include <fcntl.h>
#include <stdio.h>
#include <mutex>

class StdCapture
{
public:
    static void Init()
    {
        // make stdout & stderr streams unbuffered
        // so that we don't need to flush the streams
        // before capture and after capture 
        // (fflush can cause a deadlock if the stream is currently being 
        std::lock_guard<std::mutex> lock(m_mutex);
        setvbuf(stdout,NULL,_IONBF,0);
        setvbuf(stderr,NULL,_IONBF,0);
    }

    static void BeginCapture()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (m_capturing)
            return;

        secure_pipe(m_pipe);
        m_oldStdOut = secure_dup(STD_OUT_FD);
        m_oldStdErr = secure_dup(STD_ERR_FD);
        secure_dup2(m_pipe[WRITE],STD_OUT_FD);
        secure_dup2(m_pipe[WRITE],STD_ERR_FD);
        m_capturing = true;
#ifndef _MSC_VER
        secure_close(m_pipe[WRITE]);
#endif
    }
    static bool IsCapturing()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_capturing;
    }
    static bool EndCapture()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        if (!m_capturing)
            return;

        m_captured.clear();
        secure_dup2(m_oldStdOut, STD_OUT_FD);
        secure_dup2(m_oldStdErr, STD_ERR_FD);

        const int bufSize = 1025;
        char buf[bufSize];
        int bytesRead = 0;
        bool fd_blocked(false);
        do
        {
            bytesRead = 0;
            fd_blocked = false;
#ifdef _MSC_VER
            if (!eof(m_pipe[READ]))
                bytesRead = read(m_pipe[READ], buf, bufSize-1);
#else
            bytesRead = read(m_pipe[READ], buf, bufSize-1);
#endif
            if (bytesRead > 0)
            {
                buf[bytesRead] = 0;
                m_captured += buf;
            }
            else if (bytesRead < 0)
            {
                fd_blocked = (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
                if (fd_blocked)
                    std::this_thread::sleep_for(std::chrono::milliseconds(10));
            }
        }
        while(fd_blocked || bytesRead == (bufSize-1));

        secure_close(m_oldStdOut);
        secure_close(m_oldStdErr);
        secure_close(m_pipe[READ]);
#ifdef _MSC_VER
        secure_close(m_pipe[WRITE]);
#endif
        m_capturing = false;
    }
    static std::string GetCapture()
    {
        std::lock_guard<std::mutex> lock(m_mutex);
        return m_captured;
    }
private:
    enum PIPES { READ, WRITE };

    int StdCapture::secure_dup(int src)
    {
        int ret = -1;
        bool fd_blocked = false;
        do
        {
             ret = dup(src);
             fd_blocked = (errno == EINTR ||  errno == EBUSY);
             if (fd_blocked)
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        while (ret < 0);
        return ret;
    }
    void StdCapture::secure_pipe(int * pipes)
    {
        int ret = -1;
        bool fd_blocked = false;
        do
        {
#ifdef _MSC_VER
            ret = pipe(pipes, 65536, O_BINARY);
#else
            ret = pipe(pipes) == -1;
#endif
            fd_blocked = (errno == EINTR ||  errno == EBUSY);
            if (fd_blocked)
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        while (ret < 0);
    }
    void StdCapture::secure_dup2(int src, int dest)
    {
        int ret = -1;
        bool fd_blocked = false;
        do
        {
             ret = dup2(src,dest);
             fd_blocked = (errno == EINTR ||  errno == EBUSY);
             if (fd_blocked)
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        while (ret < 0);
    }

    void StdCapture::secure_close(int & fd)
    {
        int ret = -1;
        bool fd_blocked = false;
        do
        {
             ret = close(fd);
             fd_blocked = (errno == EINTR);
             if (fd_blocked)
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
        while (ret < 0);

        fd = -1;
    }

    static int m_pipe[2];
    static int m_oldStdOut;
    static int m_oldStdErr;
    static bool m_capturing;
    static std::mutex m_mutex;
    static std::string m_captured;
};

// actually define vars.
int StdCapture::m_pipe[2];
int StdCapture::m_oldStdOut;
int StdCapture::m_oldStdErr;
bool StdCapture::m_capturing;
std::mutex StdCapture::m_mutex;
std::string StdCapture::m_captured;

在捕获之前,调用一次Init(),将标准输出/标准错误输出缓冲区清空。

需要开始捕获时,调用BeginCapture()

需要停止捕获时,调用EndCapture()

使用GetCapture()来获取已捕获的输出内容。

使用IsCapturing()检查当前是否正在重定向标准输出或标准错误输出。


10
这个答案中列出的代码完全免费供任何想要使用它的人使用。如果需要,我可以附上BSD许可证。 - Sir Digby Chicken Caesar
1
Digby。太酷了!你能在那个 meta 帖子下留言吗?那里的发帖人还没有足够的声望在这里参与讨论。 - Martin Smith
完成。我可以在明天到达台式电脑的范围时附上许可证。 - Sir Digby Chicken Caesar
这个连编译都过不了——首先缺少头文件(chronothread)…… - slashmais
3
哎呀,这段代码有很多缺陷。首先,线程安全的思想是错误的。其次,静态数据成员和非静态成员函数的奇怪混合并不起作用。第三,由于未定义的符号,它无法编译通过。第四,在开头关闭缓冲区没有必要,只需刷新缓冲区即可,而且在停止捕获后没有代码来重新打开缓冲区。第五个问题是处理捕获数据的方式:m_captured 仅在结束捕获时清除,而不是在 GetCapture() 中,这显然是一个合适的地方。 - sh-
显示剩余4条评论

6
我已经根据Björn Pollex的代码提供了一个适用于Qt OSX的版本。
#include <stdio.h>
#include <iostream>
#include <streambuf>
#include <stdlib.h>
#include <string>
#include <sstream>

class CoutRedirect {

public:
    CoutRedirect() {
        old = std::cout.rdbuf( buffer.rdbuf() ); // redirect cout to buffer stream
    }

    std::string getString() {
        return buffer.str(); // get string
    }

    ~CoutRedirect( ) {
        std::cout.rdbuf( old ); // reverse redirect
    }

private:
    std::stringstream buffer;
    std::streambuf * old;
};

4

不需要解释,只需重新分配 stderr,例如 stderr = open_memstream(...); - Mark Lodato
注意:您需要fclose原始的stderr,并且还需要处理错误。 - Mark Lodato
1
stderr 不能用作赋值的左值。它是一个 #define。 - Akhil

4
我修改了来自的类,使其非静态化,并且可以在单元测试中轻松使用。我已经在Windows上使用gcc(g++)编译成功,但我不能保证它是100%正确的,请在有需要时留下评论。
创建StdCapture类的对象,只需调用BeginCapture()开始捕获和在结束时调用EndCapture()。Init()函数中的代码已移至构造函数中。一次只能有一个此类对象工作。
StdCapture.h:
#ifdef _MSC_VER
#include <io.h>
#define popen _popen 
#define pclose _pclose
#define stat _stat 
#define dup _dup
#define dup2 _dup2
#define fileno _fileno
#define close _close
#define pipe _pipe
#define read _read
#define eof _eof
#else
#include <unistd.h>
#endif
#include <fcntl.h>
#include <stdio.h>
#include <mutex>
#include <chrono>
#include <thread>

#ifndef STD_OUT_FD 
#define STD_OUT_FD (fileno(stdout)) 
#endif 

#ifndef STD_ERR_FD 
#define STD_ERR_FD (fileno(stderr)) 
#endif

class StdCapture
{
public:

    StdCapture();

    void BeginCapture();
    bool IsCapturing();
    bool EndCapture();
    std::string GetCapture();

private:
    enum PIPES { READ, WRITE };
    
    int secure_dup(int src);
    void secure_pipe(int * pipes);
    void secure_dup2(int src, int dest);
    void secure_close(int & fd);

    int m_pipe[2];
    int m_oldStdOut;
    int m_oldStdErr;
    bool m_capturing;
    std::mutex m_mutex;
    std::string m_captured;
};

StdCapture.cpp:

#include "StdCapture.h"

StdCapture::StdCapture():
    m_capturing(false)
{
    // make stdout & stderr streams unbuffered
    // so that we don't need to flush the streams
    // before capture and after capture 
    // (fflush can cause a deadlock if the stream is currently being 
    std::lock_guard<std::mutex> lock(m_mutex);
    setvbuf(stdout,NULL,_IONBF,0);
    setvbuf(stderr,NULL,_IONBF,0);
}

void StdCapture::BeginCapture()
{
    std::lock_guard<std::mutex> lock(m_mutex);
    if (m_capturing)
        return;

    secure_pipe(m_pipe);
    m_oldStdOut = secure_dup(STD_OUT_FD);
    m_oldStdErr = secure_dup(STD_ERR_FD);
    secure_dup2(m_pipe[WRITE],STD_OUT_FD);
    secure_dup2(m_pipe[WRITE],STD_ERR_FD);
    m_capturing = true;
#ifndef _MSC_VER
    secure_close(m_pipe[WRITE]);
#endif
}
bool StdCapture::IsCapturing()
{
    std::lock_guard<std::mutex> lock(m_mutex);
    return m_capturing;
}
bool StdCapture::EndCapture()
{
    std::lock_guard<std::mutex> lock(m_mutex);
    if (!m_capturing)
        return true;

    m_captured.clear();
    secure_dup2(m_oldStdOut, STD_OUT_FD);
    secure_dup2(m_oldStdErr, STD_ERR_FD);

    const int bufSize = 1025;
    char buf[bufSize];
    int bytesRead = 0;
    bool fd_blocked(false);
    do
    {
        bytesRead = 0;
        fd_blocked = false;
#ifdef _MSC_VER
        if (!eof(m_pipe[READ]))
            bytesRead = read(m_pipe[READ], buf, bufSize-1);
#else
        bytesRead = read(m_pipe[READ], buf, bufSize-1);
#endif
        if (bytesRead > 0)
        {
            buf[bytesRead] = 0;
            m_captured += buf;
        }
        else if (bytesRead < 0)
        {
            fd_blocked = (errno == EAGAIN || errno == EWOULDBLOCK || errno == EINTR);
            if (fd_blocked)
                std::this_thread::sleep_for(std::chrono::milliseconds(10));
        }
    }
    while(fd_blocked || bytesRead == (bufSize-1));

    secure_close(m_oldStdOut);
    secure_close(m_oldStdErr);
    secure_close(m_pipe[READ]);
#ifdef _MSC_VER
    secure_close(m_pipe[WRITE]);
#endif
    m_capturing = false;
    return true;
}
std::string StdCapture::GetCapture()
{
    std::lock_guard<std::mutex> lock(m_mutex);
    return m_captured;
}

int StdCapture::secure_dup(int src)
{
    int ret = -1;
    bool fd_blocked = false;
    do
    {
         ret = dup(src);
         fd_blocked = (errno == EINTR ||  errno == EBUSY);
         if (fd_blocked)
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    while (ret < 0);
    return ret;
}
void StdCapture::secure_pipe(int * pipes)
{
    int ret = -1;
    bool fd_blocked = false;
    do
    {
#ifdef _MSC_VER
        ret = pipe(pipes, 65536, O_BINARY);
#else
        ret = pipe(pipes) == -1;
#endif
        fd_blocked = (errno == EINTR ||  errno == EBUSY);
        if (fd_blocked)
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    while (ret < 0);
}
void StdCapture::secure_dup2(int src, int dest)
{
    int ret = -1;
    bool fd_blocked = false;
    do
    {
         ret = dup2(src,dest);
         fd_blocked = (errno == EINTR ||  errno == EBUSY);
         if (fd_blocked)
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    while (ret < 0);
}

void StdCapture::secure_close(int & fd)
{
    int ret = -1;
    bool fd_blocked = false;
    do
    {
         ret = close(fd);
         fd_blocked = (errno == EINTR);
         if (fd_blocked)
            std::this_thread::sleep_for(std::chrono::milliseconds(10));
    }
    while (ret < 0);

    fd = -1;
}

4
感谢您清理代码。顺便说一下,我将它上传到了GitHub,并标注了您和其他作者:https://github.com/dmikushin/stdcapture - Dmitry Mikushin
我尝试使用 @Dmitry Mikushin 的解决方案,它可以编译但会卡住,有什么想法吗? - AlGrenadine
@AlGrenadine,嘿,你编译什么,用的是哪个操作系统?如果适用,请在Github上开一个问题。 - Dmitry Mikushin

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