将生成的进程标准输出捕获为 unicode
Capture spawned process stdout as unicode
在我的 C++/WinAPI 代码中,我想 运行 一些命令并捕获它们的输出。为了测试非 ASCII 输出,我将我的网络连接重命名为 Ethérnét אבג БбГгДд
和 运行 ipconfig
。当 运行ning 在命令提示符下时,输出正确(使用像 Courier New 这样的支持字体时可见):
C:\>ipconfig
Windows IP Configuration
Ethernet adapter Ethérnét אבג БбГгДд:
(...)
我试图在 the example in this answer 之后将输出重定向到管道。但是从 ReadFile()
返回的字节数组不是 unicode - 它在 CP_OEMCP 中编码(在我的例子中是 CP437),所以希伯来语和俄语字符以 '?' 的形式出现。由于字符已经丢失,无法进行进一步处理。
显然这是可能的,因为控制台 window 中的 cmd 可以做到这一点。我该怎么做?
控制台应用程序可以使用不同的输出方式。
- 对于控制台句柄,我们可以使用
WriteConsoleW
作为已经在
UNICODE
.
- 如果我们想使用
WriteConsoleA
or WriteFile
作为控制台
handle 需要先将 UNICODE
文本转换为多字节
WideCharToMultiByte
与 CodePage :=
GetConsoleOutputCP()
- 如果我们最初没有
UNICODE
输出文本(例如 UTF-8
或
Ansi
), 需要先将其转换为 UNICODE
MultiByteToWideChar
(使用 CP_UTF8
或 CP_ACP
)然后
已经再次将其转换为多字节 WideCharToMultiByte(GetConsoleOutputCP(), ..)
通常(默认)GetConsoleOutputCP()
return 与 GetOEMCP()
相同的值,因此在 MultiByteToWideChar
中具有相同的效果和 WideCharToMultiByte
作为 CP_OEMCP
(这个常量值被翻译成 GetOEMCP()
)
当输出句柄被重定向到文件时只需要使用 WriteFile
。但是应用程序可以以任何格式将数据写入文件:UNICODE
、Ansi
(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"
设置为 [=53=,也可以 unicode
或 utf-8
写入文件] 或 "UTF-8"
似乎 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.
在我的 C++/WinAPI 代码中,我想 运行 一些命令并捕获它们的输出。为了测试非 ASCII 输出,我将我的网络连接重命名为 Ethérnét אבג БбГгДд
和 运行 ipconfig
。当 运行ning 在命令提示符下时,输出正确(使用像 Courier New 这样的支持字体时可见):
C:\>ipconfig
Windows IP Configuration
Ethernet adapter Ethérnét אבג БбГгДд:
(...)
我试图在 the example in this answer 之后将输出重定向到管道。但是从 ReadFile()
返回的字节数组不是 unicode - 它在 CP_OEMCP 中编码(在我的例子中是 CP437),所以希伯来语和俄语字符以 '?' 的形式出现。由于字符已经丢失,无法进行进一步处理。
显然这是可能的,因为控制台 window 中的 cmd 可以做到这一点。我该怎么做?
控制台应用程序可以使用不同的输出方式。
- 对于控制台句柄,我们可以使用
WriteConsoleW
作为已经在UNICODE
. - 如果我们想使用
WriteConsoleA
orWriteFile
作为控制台 handle 需要先将UNICODE
文本转换为多字节WideCharToMultiByte
与CodePage :=
GetConsoleOutputCP()
- 如果我们最初没有
UNICODE
输出文本(例如UTF-8
或Ansi
), 需要先将其转换为UNICODE
MultiByteToWideChar
(使用CP_UTF8
或CP_ACP
)然后 已经再次将其转换为多字节WideCharToMultiByte(GetConsoleOutputCP(), ..)
通常(默认)GetConsoleOutputCP()
return 与 GetOEMCP()
相同的值,因此在 MultiByteToWideChar
中具有相同的效果和 WideCharToMultiByte
作为 CP_OEMCP
(这个常量值被翻译成 GetOEMCP()
)
当输出句柄被重定向到文件时只需要使用 WriteFile
。但是应用程序可以以任何格式将数据写入文件:UNICODE
、Ansi
(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"
设置为 [=53=,也可以 unicode
或 utf-8
写入文件] 或 "UTF-8"
似乎 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.