简体   繁体   English

无法在Windows的Bash中管道输出

[英]Can't Pipe Output in Bash for Windows

I'm trying to write a wrapper around bash, redirecting standard in/out/err to and from a parent process. 我正在尝试围绕bash写一个包装器,将标准输入/输出/错误与父进程重定向。 So far, I've got a wrapper around Window's cmd.exe. 到目前为止,我已经对Window的cmd.exe进行了包装。 I can type in a command and have it run in the console, then read the output of that command and display it to the user. 我可以键入命令并使其在控制台中运行,然后读取该命令的输出并将其显示给用户。 So I thought it would be an easy task to wrap it around bash in the same manner. 因此,我认为以相同的方式将其包裹在bash上将是一件容易的事。 However... If I set the process to open bash, I get no output whatsoever. 但是...如果我将进程设置为打开bash,则不会输出任何信息。 Same goes if I open a cmd process and run "bash" or even, as in the demonstration below, run a single command with the "-c" option. 如果我打开一个cmd进程并运行“ bash”,甚至如下文中的演示所示,使用“ -c”选项运行一个命令,情况也是如此。 No output. 无输出。 I've compiled the smallest test case I could, which is as follows: 我已经编译了最小的测试用例,如下所示:

#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);
}

It turns out I'm an idiot. 原来我是个白痴。 Because I was only ever reading from stdout, I didn't notice that cmd was sending output to stderr in the form of a message reading, "'bash' is not recognized as an internal or external command, operable program or batch file." 因为我只是从stdout中读取内容,所以我没有注意到cmd以消息读取的形式将输出发送到stderr,“ bash不被识别为内部或外部命令,可操作程序或批处理文件。” So I then displayed the results of the command dir "%windir%\\System32" | findstr bash.exe 因此,我随后显示了命令dir "%windir%\\System32" | findstr bash.exe dir "%windir%\\System32" | findstr bash.exe . dir "%windir%\\System32" | findstr bash.exe Nothing. 没有。 Empty output. 空输出。 That was weird, I thought. 我觉得那很奇怪。

Turns out if you're running a 64-bit copy of Windows, it redirects any requests for System32 to SysWOW64 if the request is coming from a 32-bit application. 原来,如果您运行的是Windows的64位副本,如果该请求来自32位应用程序,它将把对System32的所有请求重定向到SysWOW64。 Bash is installed to System32. Bash已安装到System32。 Recompiled my application to run in a 64-bit environment et voilà, "bash -c ls" output the contents of the folder my executable was running in. Neat. 重新编译我的应用程序使其在64位环境中运行,“ bash -c ls”输出我的可执行文件正在其中运行的文件夹的内容。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM