将生成的进程标准输出捕获为 unicode

Posted

技术标签:

【中文标题】将生成的进程标准输出捕获为 unicode【英文标题】:Capture spawned process stdout as unicode 【发布时间】:2017-01-03 09:23:51 【问题描述】:

在我的 C++/WinAPI 代码中,我想运行一些命令并捕获它们的输出。为了测试非 ASCII 输出,我将网络连接重命名为 Ethérnét אבג БбГгДд 并运行 ipconfig。在命令提示符下运行时,输出正确(使用 Courier New 等支持字体时可见):

C:\>ipconfig
Windows IP Configuration

Ethernet adapter Ethérnét אבג БбГгДд:
(...)

我尝试在the example in this answer 之后将输出重定向到管道。但是从ReadFile() 返回的字节数组不是 unicode - 它以 CP_OEMCP(在我的情况下为 CP437)编码,因此希伯来语和俄语字符以“?”的形式出现。由于字符已经丢失,没有进一步的处理可以恢复它们。

显然这是可能的,因为控制台窗口中的 cmd 可以做到这一点。我该怎么做?

【问题讨论】:

ReadFile 返回字节,它不知道 Unicode 是什么。展示你如何处理它的缓冲区。 我检查了调试器返回的字节,它们是用 CP437 编码的文本,希伯来语/俄语字符替换为实际的“?”。由于字符丢失了,因此没有任何处理可以恢复它。我想知道 cmd.exe(或控制台窗口?)如何正确捕获这些字符。 所以通过MultiByteToWideChar(CP_OEMCP, 将其转换为unicode - 字符不会丢失 这就是我现在所做的。但是,由于 CP_OEMCP 无法对所有字符进行编码(例如我的示例中的希伯来语+俄语),它们显示为实际的 '?',并且转换无法恢复它们,因为它们已丢失。 @RbMm:很明显,仅包含 256 个字符的字符集不能用于编码所有 100.000+ Unicode 字符。 【参考方案1】:

似乎ipconfig 在检测到输出设备是控制台时会产生 Unicode 输出,否则会产生 ANSI 输出。这可能是一种向后兼容的措施。

出于同样的原因,大多数其他内置命令行工具可能只是 ANSI 或行为方式与 ipconfig 相同。在 Windows 中,命令行工具意味着在命令行上使用。不鼓励程序员对他们进行炮轰和解析输出。相反,您应该使用相应的 API。

如果您知道您所期望的语言,您也许可以选择一个代码页来保留内容。

由@Jonathan 添加:未记录: 原来您可以使用环境变量OutputEncoding 控制内置命令的编码。我用 ipconfig 进行了测试,但大概它也可以与其他内置工具一起使用:

> for %e in ("" Unicode Ansi UTF8) do (set OutputEncoding=%~e& ipconfig >ipconfig-%~e.txt)
> (set OutputEncoding=  & ipconfig  1>ipconfig-.txt )
> (set OutputEncoding=Unicode  & ipconfig  1>ipconfig-Unicode.txt )
> (set OutputEncoding=Ansi  & ipconfig  1>ipconfig-Ansi.txt )
> (set OutputEncoding=UTF8  & ipconfig  1>ipconfig-UTF8.txt )

确实,ipconfig-*.txt 已按预期进行编码!请注意,这是未记录的,但它确实对我有用。

附录: 从 Windows 10 v1809 开始,另一种选择是创建 pseudoconsole.

【讨论】:

这就解释了。我查看了ipconfig,并将我的发现添加到了答案中。我希望我们可以将 CP_OEMCP 设置为 CP_UTF8(以及 CP_ACP)... @Jonathan,您发布的代码片段仅在输出到控制台的情况下才能到达,与输出已重定向到管道的情况无关。然而有趣的是,负责将 UTF-16 转换为当前语言环境的是 C 运行时库。从我在 CRT 源代码中看到的内容来看,它使用 wcstomb_s 来执行此操作,尽管我正在查看 Visual Studio CRT,它与 Windows 中内置的不太一样。不幸的是,似乎没有任何方法可以让 CRT 生成 UTF-8。 确实,我的代码无关紧要。但是,我发现转换发生在 ipconfig.exe 内部 - 您可以使用未记录的 OutputEncoding env 变量控制代码页。我会在你的答案中添加一个示例。 很好找! (可能值得将其作为单独的答案发布,我会赞成。)奇怪的是,字符串OutputEncoding 没有出现在 Visual Studio 2010 CRT 源代码中,或者在 msvcrt.dll 中,但确实出现在shell32.dll 中,这让我认为这可能是操作系统正在做的事情,而不是 CRT。不过细节并不重要。 正确 - OutputEncoding 发生在 ipconfig.exe,而不是 msvcrt - 您可以使用 SysInternal 字符串查看它。它似乎只适用于某些工具 - netstat.exe,但不适用于 robocopy.exe【参考方案2】:

控制台应用程序可以使用不同的方式进行输出。

对于控制台句柄,我们可以使用WriteConsoleW 输出已经在 UNICODE。 如果我们想使用WriteConsoleAWriteFile 作为控制台 处理需要首先将UNICODE文本转换为多字节 WideCharToMultiByteCodePage := GetConsoleOutputCP() 如果我们最初没有UNICODE 文本用于输出(比如UTF-8Ansi),需要先将其转换为UNICODE by MultiByteToWideChar(与CP_UTF8CP_ACP)然后 已经再次将其转换为多字节WideCharToMultiByte(GetConsoleOutputCP(), ..)

通常(默认情况下)GetConsoleOutputCP() 返回与GetOEMCP() 相同的值,因此在MultiByteToWideCharWideCharToMultiByte 中具有与CP_OEMCP 相同的效果(此常量值转换为GetOEMCP()

当输出句柄被重定向到一个文件时,只需要使用WriteFile。但是应用程序可以以任何格式将数据写入文件:UNICODEAnsi (CP_ACP)、UTF-8 (CP_UTF8) 等。将使用什么格式 - 非常取决于具体的应用程序。你无法完全控制这一点。通常你会收到CP_OEMCP 编码的多字节输出。然后您需要决定如何处理它 - 首先您需要更快地将其转换为 UNICODE 并使用 unicode 表单。如果您需要Ansi - 您将需要进行其他一次转换。

说如果你尝试在CP_OEMCP 编码中使用管道输出和OutputDebugStringA - 你得到非英文文本的错误(不可读)输出。 但经过 2 次转换后 CP_OEMCP -> UNICODE -> CP_ACP 您可以使用 OutputDebugStringA 更正显示的文本 但是因为 OutputDebugStringW 存在 - 这里只需要 UNICODE 转换

还有一些应用程序具有用于控制输出到文件格式的特殊选项。说ipconfig.exe 寻找"OutputEncoding" 环境变量并依赖于它的字符串值("Unicode""Ansi""UTF-8")产生不同的输出。默认情况下(如果此环境变量不存在或未知值)CP_OEMCP 使用

管道读取过程的示例。假设输入数据采用CP_OEMCP 编码:

void OnRead(PVOID buf, ULONG cbTransferred)

    if (cbTransferred)
    
        if (int len = MultiByteToWideChar(CP_OEMCP, 0, (PSTR)buf, cbTransferred, 0, 0))
        
            PWSTR pwz = (PWSTR)alloca((1 + len) * sizeof(WCHAR));

            if (len = MultiByteToWideChar(CP_OEMCP, 0, (PSTR)buf, cbTransferred, pwz, len))
            
                if (g_bUseAnsi)
                
                    if (cbTransferred = WideCharToMultiByte(CP_ACP, 0, pwz, len, 0, 0, 0, 0))
                    
                        PSTR psz = (PSTR)alloca(cbTransferred + 1);

                        if (cbTransferred = WideCharToMultiByte(CP_ACP, 0, pwz, len, psz, cbTransferred, 0, 0))
                        
                            DoPrint(psz, cbTransferred, OutputDebugStringA);
                        
                    
                
                else
                
                    DoPrint(pwz, len, OutputDebugStringW);
                
            
        
    


// debugger can incomplete print too big buffer, so split it on small chunks
template<typename T> void DoPrint(T* p, ULONG len, void (WINAPI* fnOutput)(const T*))

    ULONG cb;
    T* q = p;
    do 
    
        cb = min(len, 256);

        q = p + cb;

        T c = *q;

        *q = 0;

        fnOutput(p);

        *q = c;

        p = q;

     while (len -= cb);


关于您的具体案例 - ipconfig.exe 使用 WriteConsoleW 输出到控制台。因此它不依赖于当前的系统区域设置,并且可以更正显示多语言文本。但是另一个工具,如route.exe 使用WriteFile 输出(既用于控制台也用于文件)并在此UNICODE 文本之前由WideCharToMultiByte(CP_OEMCP,..) 转换为多字节 - 结果这里会出现问题,如果尝试显示字符CP_OEMCP 代码页(当前系统区域设置)中不存在。如果你有CP437 - 如果使用UNICODE -> CP_OEMCP,希伯来语和俄语字符将丢失,只需要使用unicode直接输出到控制台和文件。这可能吗 - 取决于具体应用程序。比如说route.exe 这是不可能的。对于ipconfig.exe,这是可能的,因为它始终以 unicode 格式写入控制台,并且如果您将 "OutputEncoding" 设置为 "Unicode""UTF-8",也可以以 unicodeutf-8 写入文件

【讨论】:

这无法考虑跨包的多字节字符。如果 IsDBCSLeadByte 是最终代码单元的 TRUE,则转换会破坏此块以及以下字节块。 @IInspectable - 什么是失败?你是关于? 对不起,这是我不懂的语言。

以上是关于将生成的进程标准输出捕获为 unicode的主要内容,如果未能解决你的问题,请参考以下文章

在 cmake 中,我怎样才能始终执行一个进程? (或从 add_custom_command 捕获标准输出)

使用 libuv 捕获子进程的标准输出

如何将标准输出转换为字符串(Python)[重复]

将标准输出捕获到变量,但仍将其显示在控制台中

从子进程中实时捕获标准输出

SharpPcap - 从标准输出捕获