c++ - 无法在 Bash for Windows 中通过管道输出

标签 c++ windows bash

我正在尝试围绕 bash 编写包装器,将标准输入/输出/错误重定向到父进程或从父进程重定向。到目前为止,我已经对 Window 的 cmd.exe 进行了包装。我可以输入命令并让它在控制台中运行,然后读取该命令的输出并将其显示给用户。 所以我认为以同样的方式将它包装在 bash 周围是一件容易的事。但是...如果我将进程设置为打开 bash,我将得不到任何输出。如果我打开一个 cmd 进程并运行“bash”,甚至像下面的演示一样,运行带有“-c”选项的单个命令,情况也是如此。无输出。 我已经编译了最小的测试用例,如下所示:

#include <algorithm>
#include <cassert>
#include <functional>
#include <string>
#include <tchar.h>
#include <Windows.h>

using wstring = std::wstring;
using astring = std::string;
#ifdef UNICODE
using tstring = wstring;
using tchar = wchar_t;
#else
using tstring = astring;
using tchar = char;
#endif

const tstring PROMPT = L"ATOTALLYRANDOMSTRING";

/**
 * Represents an instance of a terminal process with piped in, out, and err
 * handles.
 */
class Terminal
{
public:
    using OutputCallback = std::function<void(tstring)>;

    /**
     * Terminal constructor.
     */
    Terminal();

    /**
     * Terminal destructor.
     */
    ~Terminal();
    /**
     * Executes the specified command.  If a callback is specified, the output
     * be passed as the first argument.
     *
     * @param command
     * @param callback
     * @param buffer   If specified, the callback parameter will be called as
     * output is available until the command is complete.
     */
    void exec(astring command, OutputCallback callback = nullptr, bool buffer = true);
    /**
     * Reads from the terminal, calling the specified callback as soon as any
     * output is available.
     *
     * @param callback
     */
    void read(OutputCallback callback);
    /**
     * Reads from the terminal, calling the specified callback upon reaching a
     * newline.
     *
     * @param callback
     * @param buffer   If specified, causes the callback to be called as output
     * is available until a newline is reached.
     */
    void readLine(OutputCallback callback, bool buffer = true);
    /**
     * Reads from the terminal, calling the specified callback upon reaching
     * the specified terminator.
     *
     * @param terminator
     * @param callback
     * @param buffer     If specified, causes the callback to be called as
     * output is available until the specified terminator is reached.
     */
    void readUntil(const tstring& terminator, OutputCallback callback, bool buffer = true);
    /**
     * Read from the terminal, calling the specified callback upon reaching the
     * prompt.
     *
     * @param callback
     * @param buffer   If specified, causes the callback to be called as output
     * is available until the specified prompt is reached.
     */
    void readUntilPrompt(OutputCallback callback, bool buffer = true);
    /**
     * Writes the specified text to the terminal.
     *
     * @param data
     */
    void write(const astring& data);
private:
    struct
    {
        struct
        {
            HANDLE in;
            HANDLE out;
            HANDLE err;
        } read, write;
    } pipes;
    tstring bufferedData;
    PROCESS_INFORMATION process;

    void initPipes();
    void initProcess();
    void terminatePipes();
    void terminateProcess();
};

int CALLBACK WinMain(HINSTANCE, HINSTANCE, LPSTR, int)
{
    Terminal terminal;

    terminal.readUntilPrompt([] (tstring startupInfo) {
        MessageBox(nullptr, startupInfo.c_str(), L"Startup Information", MB_OK);
    });
    // This works
    terminal.exec("dir", [] (tstring result) {
        MessageBox(nullptr, result.c_str(), L"dir", MB_OK);
    });
    // This doesn't (no output)
    terminal.exec("bash -c \"ls\"", [] (tstring result) {
        MessageBox(nullptr, result.c_str(), L"bash -c \"ls\"", MB_OK);
    });
    return 0;
}

Terminal::Terminal()
{
    this->initPipes();
    this->initProcess();
}

Terminal::~Terminal()
{
    this->terminateProcess();
    this->terminatePipes();
}

void Terminal::exec(astring command, OutputCallback callback, bool buffer)
{
    command.append("\r\n");

    this->write(command);
    this->readUntilPrompt([&callback, &command] (tstring data) {
        if (!callback) {
            return;
        }
        // Remove the prompt from the string
        data.erase(data.begin(), data.begin() + command.length());
        callback(data);
    }, buffer);
}

void Terminal::initPipes()
{
    SECURITY_ATTRIBUTES attr;

    attr.nLength = sizeof(attr);
    attr.bInheritHandle = TRUE;
    attr.lpSecurityDescriptor = nullptr;
    if(!CreatePipe(&this->pipes.read.in, &this->pipes.write.in, &attr, 0))
    {
        throw std::exception("Failed to create stdin pipe.");
    }
    if(!SetHandleInformation(this->pipes.write.in, HANDLE_FLAG_INHERIT, 0))
    {
        throw std::exception("Failed to unset stdin pipe inheritance flag.");
    }
    if(!CreatePipe(&this->pipes.read.out, &this->pipes.write.out, &attr, 0))
    {
        throw std::exception("Failed to create stdout pipe.");
    }
    if(!SetHandleInformation(this->pipes.read.out, HANDLE_FLAG_INHERIT, 0))
    {
        throw std::exception("Failed to unset stdout pipe inheritance flag.");
    }
    if(!CreatePipe(&this->pipes.read.err, &this->pipes.write.err, &attr, 0))
    {
        throw std::exception("Failed to create stderr pipe.");
    }
    if(!SetHandleInformation(this->pipes.read.err, HANDLE_FLAG_INHERIT, 0))
    {
        throw std::exception("Failed to unset stderr pipe inheritance flag.");
    }
}

void Terminal::initProcess()
{
    tstring     command;
    STARTUPINFO startup;

#ifdef UNICODE
    command.append(L"cmd /U /K \"prompt ");
    command.append(PROMPT);
    command.append(L"\"");
#else
    command.append("cmd /A /K \"prompt ");
    command.append(PROMPT);
    command.append("\"");
#endif
    ZeroMemory(&this->process, sizeof(this->process));
    ZeroMemory(&startup, sizeof(startup));
    startup.cb = sizeof(startup);
    startup.dwFlags |= STARTF_USESTDHANDLES;
    startup.hStdInput = this->pipes.read.in;
    startup.hStdOutput = this->pipes.write.out;
    startup.hStdError = this->pipes.write.err;
    startup.dwFlags |= STARTF_USESHOWWINDOW;
    startup.wShowWindow = SW_HIDE;
    auto created = CreateProcess(
        nullptr,
        _tcsdup(command.c_str()),
        nullptr,
        nullptr,
        TRUE,
        0,
        nullptr,
        nullptr,
        &startup,
        &this->process
    );
    if (!created) {
        throw std::exception("Failed to create process.");
    }
}

void Terminal::read(OutputCallback callback)
{
    this->readUntil(L"", callback);
}

void Terminal::readLine(OutputCallback callback, bool buffer)
{
    this->readUntil(L"\n", callback, buffer);
}

void Terminal::readUntil(const tstring& terminator, OutputCallback callback, bool buffer)
{
    auto terminatorIter = terminator.cbegin();
    auto terminatorEnd = terminator.cend();
    auto bufferIter = this->bufferedData.begin();
    auto bufferEnd = this->bufferedData.end();
    do {
        DWORD  bytesAvailable = 0;

        if (!PeekNamedPipe(this->pipes.read.out, nullptr, 0, nullptr, &bytesAvailable, nullptr)) {
            throw std::exception("Failed to peek command input pipe.");
        }
        if (bytesAvailable)
        {
            DWORD  bytesRead;
            tchar* data;

            data = new tchar[bytesAvailable / sizeof(tchar)];
            if (!ReadFile(this->pipes.read.out, data, bytesAvailable, &bytesRead, nullptr)) {
                throw std::exception("ReadFile failed.");
            }
            assert(bytesRead == bytesAvailable);
            auto iterDistance = bufferIter - this->bufferedData.begin();
            this->bufferedData.append(data, bytesRead / sizeof(tchar));
            bufferIter = this->bufferedData.begin() + iterDistance;
            bufferEnd = this->bufferedData.end();
        }
        if (terminator.empty()) {
            if (!this->bufferedData.empty())
            {
                bufferIter = bufferEnd;
                terminatorIter = terminatorEnd;
            }
        } else {
            while(bufferIter != bufferEnd && terminatorIter != terminatorEnd) {
                if (*bufferIter == *terminatorIter) {
                    ++terminatorIter;
                } else {
                    terminatorIter = terminator.begin();
                }
                ++bufferIter;
            }
        }
        if (!buffer || terminatorIter == terminatorEnd) {
            callback(tstring(this->bufferedData.begin(), bufferIter - terminator.length()));
            this->bufferedData.erase(this->bufferedData.begin(), bufferIter);
        }
    } while (terminatorIter != terminatorEnd);
}

void Terminal::readUntilPrompt(OutputCallback callback, bool buffer)
{
    this->readUntil(PROMPT, callback, buffer);
}

void Terminal::terminatePipes()
{
    if (this->pipes.read.err) {
        CloseHandle(this->pipes.read.err);
    }
    if (this->pipes.write.err) {
        CloseHandle(this->pipes.write.err);
    }
    if (this->pipes.read.out) {
        CloseHandle(this->pipes.read.out);
    }
    if (this->pipes.write.out) {
        CloseHandle(this->pipes.write.out);
    }
    if (this->pipes.read.in) {
        CloseHandle(this->pipes.read.in);
    }
    if (this->pipes.write.in) {
        CloseHandle(this->pipes.write.in);
    }
}

void Terminal::terminateProcess()
{
    if (this->process.hProcess) {
        CloseHandle(this->process.hProcess);
    }
}

void Terminal::write(const astring& data)
{
    DWORD byteCount;
    DWORD bytesWritten;

    byteCount = data.length();
    if (!WriteFile(this->pipes.write.in, data.c_str(), byteCount, &bytesWritten, nullptr)) {
        throw std::exception("WriteFile failed.");
    }
    assert(bytesWritten == byteCount);
}

最佳答案

原来我是个白痴。因为我只是从 stdout 读取,所以我没有注意到 cmd 以消息读取的形式将输出发送到 stderr,“'bash' 未被识别为内部或外部命令、可运行的程序或批处理文件。”因此,我随后显示了命令 dir "%windir%\System32"| 的结果。 findstr bash.exe。没有什么。空输出。这很奇怪,我想。

事实证明,如果您运行的是 64 位的 Windows 拷贝,如果请求来自 32 位应用程序,它会将对 System32 的任何请求重定向到 SysWOW64。 Bash 安装到 System32。重新编译我的应用程序以在 64 位环境中运行 et voilà,“bash -c ls”输出我的可执行文件在其中运行的文件夹的内容。整洁。

关于c++ - 无法在 Bash for Windows 中通过管道输出,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41747974/

相关文章:

c++ - 需要为 C++ 插件模块生成 UML 图

c++ - 在 Windows 中获取下一个打开的 tcp 端口

c++ - C++接口(interface)中的私有(private)方法?

c# - 如何使 MdiChild 表单位于 C# 中的选项卡中?

linux - XDG_SESSION_COOKIE 环境变量有什么用?

mysql - Bash 包含不工作

c++ - 同一个 C++ 类的所有实例共享一个 vtable 还是每个实例都有自己的?

c# - .NET 中的 CoCreateInstance 完全匹配?

python - 命令提示符/Anaconda 命令提示符无法在 Windows 11 中打开

python - 如何在 Linux 上管理基于 Python 的守护进程?