在 Windows 上解析 UTF-8 命令行参数时出现这个奇怪问题的原因是什么?

Mic*_*eyn 5 windows perl utf-8

我正在尝试传入一个使用 UNICODE 字符的字符串:"right single quotation mark" Decimal: 8217 Hex: \\x{2019}

\n

Perl 未正确接收字符。让我向您展示详细信息:

\n

Perl 脚本如下(我们称之为test.pl):

\n
use warnings;\nuse strict;\nuse v5.32;\nuse utf8; # Some UTF-8 chars are present in the code\'s comments\n\n# Get the first argument\nmy $arg=shift @ARGV or die \'This script requires one argument\';\n\n# Get some env vars with sensible defaults if absent\nmy $lc_all=$ENV{LC_ALL} // \'{unset}\';\nmy $lc_ctype=$ENV{LC_CTYPE} // \'{unset}\';\nmy $lang=$ENV{LANG} // \'{unset}\';\n\n# Determine the current Windows code page\nmy ($active_codepage)=`chcp 2>NUL`=~/: (\\d+)/;\n\n# Our environment\nsay "ENV: LC_ALL=$lc_all LC_CTYPE=$lc_ctype LANG=$lang";\nsay "Active code page: $active_codepage"; # Note: 65001 is UTF-8\n\n# Saying the wrong thing, expected: 0\xe2\x80\x99s    #### Note: Between the \'0\' and the \'s\'\n#   is a "right single quotation mark" and should be in utf-8 => \n#   Decimal: 8217 Hex: \\x{2019}\n# For some strange reason the bytes "\\x{2019}" are coming in as "\\x{92}" \n#   which is the single-byte CP1252 representation of the character "right \n#   single quotation mark"\n# The whole workflow is UTF-8, so I don\'t know where there is a CP1252 \n#   translation of the input argument (outside of Perl that is)\n\n# Display the value of the argument and its length\nsay "Argument: $arg length: ",length($arg);\n\n# Display the bytes that make up the argument\'s string\nprint("Argument hex bytes:");\nfor my $chr_idx (0 .. length($arg)-1)\n{\n  print sprintf(\' %02x\',ord(substr($arg,$chr_idx,1)));\n}\nsay \'\'; # Newline\n
Run Code Online (Sandbox Code Playgroud)\n

我运行 Perl 脚本如下:

\n
V:\\videos>c:\\perl\\5.32.0\\bin\\perl test.pl 0\xe2\x80\x99s\n
Run Code Online (Sandbox Code Playgroud)\n

输出:

\n
ENV: LC_ALL=en-US.UTF-8 LC_CTYPE={unset} LANG=en_US.UTF-8\nArgument: 0s length: 3\nArgument hex bytes: 30 92 73\n
Run Code Online (Sandbox Code Playgroud)\n

好吧,也许我们还需要指定 UTF-8所有内容(stdin/out/err 和命令行参数)?

\n
V:\\videos>c:\\perl\\5.32.0\\bin\\perl -CSDA test.pl 0\xe2\x80\x99s\n
Run Code Online (Sandbox Code Playgroud)\n

输出:

\n
ENV: LC_ALL=en-US.UTF-8 LC_CTYPE={unset} LANG=en_US.UTF-8\nActive code page: 65001\nArgument: 0s length: 3\nArgument hex bytes: 30 92 73\n
Run Code Online (Sandbox Code Playgroud)\n

好的,让我们尝试完全删除所有LC*/LANG环境变量,结果是:

\n
@SET LC_ALL=\n@SET LANG=\n\n@REM Proof that everything has been cleared\n@REM Note: The caret before the vertical bar escapes it,\n@REM       because I have grep set up to run through a\n@REM       batch file and need to forward args\n@set | grep -iP "LC^|LANG" || echo %errorlevel%\n
Run Code Online (Sandbox Code Playgroud)\n

输出:

\n
1\n
Run Code Online (Sandbox Code Playgroud)\n

让我们尝试使用 UTF-8 再次执行脚本:

\n
V:\\videos>c:\\perl\\5.32.0\\bin\\perl -CSDA 0\xe2\x80\x99s\n
Run Code Online (Sandbox Code Playgroud)\n

输出(没有任何变化,除了LC*/LANG环境变量已被清除):

\n
ENV: LC_ALL={unset} LC_CTYPE={unset} LANG={unset}\nActive code page: 65001\nArgument: 0s length: 3\nArgument hex bytes: 30 92 73\n
Run Code Online (Sandbox Code Playgroud)\n

此时,我决定跳出 Perl 的范畴,看看 Windows 10 本身对我的命令行参数做了什么。我不久前用 C# 编写了一个小实用程序,可以帮助解决命令行参数问题并使用它进行测试。输出应该是不言自明的:

\n
V:\\videos>ShowArgs 0\xe2\x80\x99s\n\nFilename: |ShowArgs.exe|\nPathname: |c:\\bin\\ShowArgs.exe|\nWork dir:  |V:\\videos|\n\nCommand line: ShowArgs  0\xe2\x80\x99s\n\nRaw command line characters:\n\n000: |ShowArgs  |: S (083:53) h (104:68) o (111:6F) w (119:77) A (065:41) r (114:72) g (103:67) s (115:73)   (032:20)   (032:20)\n010: |0\xe2\x80\x99s       |: 0 (048:30) \xe2\x80\x99 (8217:2019) s (115:73)\n\nCommand line args:\n\n00: |0\xe2\x80\x99s|\n
Run Code Online (Sandbox Code Playgroud)\n

这说明了几件事:

\n
    \n
  1. 传入的参数不需要加引号(我没想到会)
  2. \n
  3. Windows 已将参数以 UTF-8 格式正确传递到应用程序
  4. \n
\n

我一生都无法弄清楚为什么 Perl 此时没有收到 UTF-8 形式的参数。

\n

当然,作为一个绝对的黑客,如果我在 Perl 脚本的底部添加以下内容,问题就会得到解决。但我想了解为什么 Perl 没有收到 UTF-8 形式的参数:

\n
# ... Appended to original script shown at top ...\nuse Encode qw(encode decode);\n\nsub recode \n{ \n  return encode(\'UTF-8\', decode( \'cp1252\', $_[0] ));\n}\n\nsay "\\n@{[\'=\'x60]}\\n"; # Output separator\nsay "Original arg: $arg";\nsay "After recoding CP1252 -> UTF-8: ${\\recode($arg)}";\n
Run Code Online (Sandbox Code Playgroud)\n

脚本执行:

\n
V:\\videos>c:\\perl\\5.32.0\\bin\\perl test.pl 0\xe2\x80\x99s\n
Run Code Online (Sandbox Code Playgroud)\n

新输出:

\n
ENV: LC_ALL=en_US.UTF-8 LC_CTYPE={unset} LANG={unset}\nActive code page: 65001\nArgument: 0s length: 3\nArgument hex bytes: 0030 0092 0073\n\n============================================================\n\nOriginal arg: 0s\nAfter recoding CP1252 -> UTF-8: 0\xe2\x80\x99s\n
Run Code Online (Sandbox Code Playgroud)\n

更新

\n

我构建了一个简单的 C++ 测试应用程序来更好地处理正在发生的情况。

\n

这是源代码:

\n
#include <cstdint>\n#include <cstring>\n#include <iostream>\n#include <iomanip>\n\nint main(int argc, const char *argv[])\n{\n  if (argc!=2)\n  {\n    std::cerr << "A single command line argument is required\\n";\n    return 1;\n  }\n\n  const char *arg=argv[1];\n  std::size_t arg_len=strlen(arg);\n\n  // Display argument as a string\n  std::cout << "Argument: " << arg << " length: " << arg_len << \'\\n\';\n\n  // Display argument bytes\n  // Fill with leading zeroes\n  auto orig_fill_char=std::cout.fill(\'0\');\n\n  std::cout << "Bytes of argument, in hex:";\n  std::cout << std::hex;\n  for (std::size_t arg_idx=0; arg_idx<arg_len; ++arg_idx)\n  {\n    // Note: The cast to uint16_t is necessary because uint8_t is formatted \n    //       "specially" (i.e., still as a char and not as an int)\n    //       The cast through uint8_t is necessary due to sign extension of\n    //       the original char if going directly to uint16_t and the (signed) char\n    //       value is negative.\n    //       I could have also masked off the high byte after the cast, with\n    //       insertion code like (Note: Parens required due to precedence):\n    //         << (static_cast<uint16_t>(arg[arg_idx]) & 0x00ff)\n    //       As they say back in Perl-land, "TMTOWTDI!", and in this case it\n    //       amounts to the C++ version of Perl "line noise" no matter which\n    //       way you slice it. :)\n    std::cout << \' \' \n              << std::setw(2) \n              << static_cast<uint16_t>(static_cast<uint8_t>(arg[arg_idx])); \n  }\n  std::cout << \'\\n\';\n\n  // Restore the original fill char and go back to decimal mode\n  std::cout << std::setfill(orig_fill_char) << std::dec;\n}\n
Run Code Online (Sandbox Code Playgroud)\n

使用 MBCS 字符集设置构建为基于 64 位控制台的应用程序,以上代码的运行方式为:

\n
testapp.exe 0\xe2\x80\x99s\n
Run Code Online (Sandbox Code Playgroud)\n

...,并产生以下输出:

\n
Argument: 0s length: 3\nArgument bytes: 30 92 73\n
Run Code Online (Sandbox Code Playgroud)\n

所以,毕竟,至少部分是Windows。我需要构建这个应用程序的 UNICODE 字符集版本,看看我会得到什么。

\n

关于如何一劳永逸解决此问题的最终更新

\n

感谢Eryk Sun对ikegami接受的答案的评论以及该答案中的链接,我找到了最好的解决方案,至少对于 Windows 10 而言。我现在将概述强制 Windows 发送数据时要遵循的具体步骤将命令行参数以 UTF-8 格式输入 Perl:

\n

需要将清单添加到 perl.exe 和 wperl.exe(如果您使用它),它告诉 Windows 在执行 perl.exe 应用程序时使用 UTF-8 作为活动代码页 (ACP)。这将告诉 Windows 将命令行参数作为 UTF-8 而不是 CP1252 传递到 perl。

\n

需要做出的改变

\n

创建清单文件

\n

转到perl.exe( 和) 的位置,并在该 ( ) 目录中创建一个包含以下内容的wperl.exe文件,并将其命名为:...\\binperl.exe.manifest

\n
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>\n<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">\n  <assemblyIdentity type="win32" name="perl.exe" version="6.0.0.0"/>\n  <application>\n    <windowsSettings>\n      <activeCodePage\n        xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings"\n      >UTF-8</activeCodePage>\n    </windowsSettings>\n  </application>\n</assembly>\n
Run Code Online (Sandbox Code Playgroud)\n

如果您还想修改wperl.exe将上述文件复制perl.exe.manifest到wperl.exe.manifest并修改该文件,请替换以下assemblyIdentity行:

\n
  <assemblyIdentity type="win32" name="perl.exe" version="6.0.0.0"/>\n
Run Code Online (Sandbox Code Playgroud)\n

with (注意分配给属性的值name从perl.exe变为wperl.exe):

\n
  <assemblyIdentity type="win32" name="wperl.exe" version="6.0.0.0"/>\n
Run Code Online (Sandbox Code Playgroud)\n

将清单嵌入可执行文件中

\n

下一步是获取我们刚刚创建的清单文件并将它们嵌入到各自的可执行文件中。在执行此操作之前,请务必备份原始可执行文件,以防万一!

\n

清单可以嵌入到可执行文件中,如下所示:

\n

为了perl.exe:

\n
mt.exe -manifest perl.exe.manifest -outputresource:perl.exe;#1\n
Run Code Online (Sandbox Code Playgroud)\n

对于wperl.exe(可选,仅当您使用时才需要wperl.exe):

\n
mt.exe -manifest wperl.exe.manifest -outputresource:wperl.exe;#1\n
Run Code Online (Sandbox Code Playgroud)\n

如果您还没有mt.exe可执行文件,可以在 Windows 10 SDK 中找到它,目前位于:在developer.microsoft.com 下载 Windows 10 SDK

\n

基本测试和使用

\n

进行上述更改后,UTF-8 命令行参数变得超级简单!

\n

采用以下脚本simple-test.pl:

\n
use strict;\nuse warnings;\nuse v5.32; # Or whatever recent version of Perl you have\n\n# Helper subroutine to provide simple hex table output formatting\nsub hexdump\n{\n  my ($arg)=@_;\n  sub BYTES_PER_LINE {16}; # Output 16 hex pairs per line\n\n  for my $chr_idx (0 .. length($arg)-1)\n  {\n    # Break into groups of 16 hex digit pairs per line\n    print sprintf(\'\\n  %02x: \', $chr_idx+1/BYTES_PER_LINE)\n      if $chr_idx%BYTES_PER_LINE==0;\n    print sprintf(\'%02x \',ord(substr($arg,$chr_idx,1)));\n  }\n  say \'\';\n}\n\n# Test app code that makes no mention of Windows, ACPs, or UTF-8 outside\n# of stuff that is printed. Other than the call out to chcp to get the\n# active code page for informational purposes, it is not particularly tied\n# to Windows, either, as long as whatever environment it is run on\n# passes the script its arg as UTF-8, of course.\nmy $arg=shift @ARGV or die \'No argument present\';\n\nsay "Argument: $arg";\nsay "Argument byte length: ${\\length($arg)} bytes";\nprint \'Argument UTF-8 data bytes in hex:\';\nhexdump($arg);\n
Run Code Online (Sandbox Code Playgroud)\n

让我们测试我们的脚本,确保我们处于 UTF-8 代码页 (65001) 中:

\n
v:\\videos>chcp 65001 && perl.exe simple-test.pl "\xd0\xa0\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x82\xd0\xb0 \xd1\x81 \xe2\x80\x99 vis-\xc3\xa0-vis 0\'s using UTF-8"\n
Run Code Online (Sandbox Code Playgroud)\n

输出(假设您的控制台字体可以处理特殊字符):

\n
Active code page: 65001\nArgument: \xd0\xa0\xd0\xb0\xd0\xb1\xd0\xbe\xd1\x82\xd0\xb0 \xd1\x81 \xe2\x80\x99 vis-\xc3\xa0-vis 0\'s using UTF-8\nArgument byte length: 54 bytes\nArgument UTF-8 data bytes in hex:\n  00: d0 a0 d0 b0 d0 b1 d0 be d1 82 d0 b0 20 d1 81 20\n  10: f0 9d 9f 98 e2 80 99 f0 9d 99 a8 20 76 69 73 2d\n  20: c3 a0 2d 76 69 73 20 30 27 73 20 75 73 69 6e 67\n  30: 20 55 54 46 2d 38\n
Run Code Online (Sandbox Code Playgroud)\n

我希望我的解决方案能够帮助其他遇到此问题的人。

\n

ike*_*ami 3

每个处理字符串的 Windows 系统调用都有两种类型:使用活动代码页(又名 ANSI 代码页)的“A”NSI 版本和使用 UTF-16le 的“W”ide 版本。[1] Perl 使用A所有系统调用的版本。这包括获取命令行的调用。

\n

ACP 是硬编码的。(或者Windows可能在安装过​​程中要求提供系统语言并以此为基础?我不记得了。)例如,我的系统上的语言是1252,我无法改变它。值得注意的是,chcp对 ACP 没有影响。

\n

至少,直到最近都是这样。2019 年 5 月的 Windows 更新添加了通过其清单针对每个应用程序更改ACP 的功能。(该页面表明可以更改现有应用程序的清单。)

\n

chcp更改控制台的 CP,但不更改系统A调用使用的编码。将其设置为包含的代码页\xe2\x80\x99可确保您可以输入\xe2\x80\x99,并且 Perl 可以打印出 a \xe2\x80\x99(如果编码正确)。[2]由于 65001 包含\xe2\x80\x99,因此执行这两件事没有问题。

\n

控制台 CP 的选择(由 设定chcp)对 Perl 如何接收命令行没有影响。因为 Perl 使用A系统调用的版本,所以命令行将使用 ACP 进行编码,而不管控制台的 CP 和 OEM CP。

\n
\n

\xe2\x80\x99基于编码为 的事实92,您的系统似乎也使用 1252 作为其活动代码页。因此,您可以按如下方式解决您的问题:

\n
use Encode qw( decode );\n\nmy @ARGV = map { decode("cp1252", $_) } @ARGV;\n
Run Code Online (Sandbox Code Playgroud)\n

请参阅这篇文章,了解更通用和可移植的解决方案,该解决方案还向 STDIN、STDOUT 和 STDERR 添加了适当的编码/解码层。

\n
\n

但是,如果您想支持任意 Unicode 字符而不是仅限于系统 ACP 中的字符,该怎么办?正如上面提到的,你可以改变 perlACP。将其更改为 650001 (UTF-8) 将使您能够访问整个 Unicode 字符集。

\n

如果不这样做,您需要使用W系统调用的版本从操作系统获取命令行并解析它。

\n

虽然 Perl 使用A系统调用的版本,但这并不限制模块执行相同的操作。他们可能会使用W系统调用。[3]所以也许有一个模块可以满足您的需要。如果没有,我之前已经编写了执行此操作的代码。

\n
\n

非常感谢@Eryk Sun 在评论中提供的意见。

\n
\n
    \n
  • ACP 可以使用 获得Win32::GetACP()。
  • \n
  • OEM CP 可以使用 获得Win32::GetOEMCP()。
  • \n
  • 控制台的 CP 可以使用Win32::GetConsoleCP()/获取Win32::GetConsoleOutputCP()。
  • \n
\n
\n
    \n
  1. SetFileApisToOEM可用于更改AOEM CP 的某些系统调用所使用的编码。[2]
  2. \n
  3. 控制台的 CP 默认为系统的 OEM CP。这可以通过更改注册表项CodePage的值来覆盖HKCU\\Console\\<window title>,其中<window title>是控制台的初始窗口标题。当然,也可以使用它chcp进行的底层系统调用来覆盖它。
  4. \n
  5. 值得注意的是,请参阅Win32::LongPath。
  6. \n
\n