在什么编码readdir返回一个文件名?

这里有一个Perl脚本,我期望打印时found执行时:

 #!/usr/bin/perl use warnings; use strict; use utf8; use Encode; use constant filename => 'Bärlauch'; open (my $out, '>', filename) or die; close $out; opendir(my $dir, '.') or die; while (my $filename_read = readdir($dir)) { # $filename_read = encode('utf8', $filename_read); print "found\n" if $filename_read eq filename; } 

该脚本首先创build一个名称为常量filename 。 (在运行脚本之后,我可以使用ls来validation文件的存在,并且文件不是用“funny”字符创build的。)

然后,脚本遍历当前工作目录中的文件,如果有名称与刚刚创build的文件相同的文件,则打印该文件。 这显然应该是这样的。

但是,它不(Ubuntu,bash, LANG=en_US.UTF8

如果我将这个常量改为Barlauch ,它就会按预期工作,并found打印件。

取消注释$filename_read = encode('utf8', $filename_read); 不会改变行为。

有没有解释这个,我该怎么做才能识别Umlaute的文件名呢?

这个问题(正如我解释的那样)是:

为什么不readdir返回新创建的文件名? (这里,由设置为Bärlauch的变量filename表示)。

(注意: filename是一个Perl常量变量,所以这就是为什么它在前面缺少$ sigil。)

背景:

首先注意:由于在程序开始时use utf8语句,所以在编译时filename名将被升级为Unicode字符串,因为它包含非ASCII字符。 从utf8编译指南的文档:

启用utf8编译指示具有以下效果:源文本中不在ASCII字符集中的字节将被视为文字UTF-8序列的一部分。 这包括大多数文字,例如标识符名称,字符串常量和常量正则表达式模式。

另外,根据perluniintro部分“Perl的Unicode模型”

一般的原则是,Perl试图尽可能长时间保持数据为八位字节,但是一旦Unicode变得无法避免,数据就会被透明地升级到Unicode。

在内部,Perl当前使用平台的本地八位字符集(例如Latin-1)(默认为UTF-8)编码Unicode字符串。

filename的非ASCII字符是字母ä 。 如果使用ISO 8859-1扩展ASCII编码(Latin-1),则编码为字节值0xE4 ,请参阅0xE4此表 。 但是,如果从filename删除了ä字符,则它将只包含ASCII字符,因此即使使用了utf8编译指示,也不会将其内部升级为Unicode。

所以filename现在是一个Unicode字符串,内部设置了UTF-8标志(有关UTF-8标志的更多信息,请参见utf8编译指示)。 请注意,字母ä以UTF-8编码为两个字节0xC3 0xA4

编写文件:

在编写文件时,文件名会发生什么? 如果filename是一个Unicode字符串,它将被编码为UTF-8。 但是请注意,不需要先编码filenameencode_utf8( filename ) )。 有关更多信息,请参阅使用unicode字符创建文件名 。 所以文件名以UTF-8编码的字节写入磁盘。

读回文件名:

当尝试从磁盘读取文件名时,即使文件名包含以UTF-8编码的字节, readdir也不会返回Unicode字符串(即设置了UTF-8标志的字符串)。 它返回二进制或字节字符串,参见perlunitut讨论字节串vs字符(Unicode)字符串。

为什么不readdir返回Unicode字符串? 首先,根据perlunicode节“当Unicode不发生时”

还有很多地方可以将Unicode(在一些编码或其他编码中)作为参数给出,或者以Perl的形式作为结果接收,或者两者都不是。 (……)

以下是这样的接口。 对于所有这些接口,Perl当前(从v5.16.0开始)只是假定字节串作为参数和结果。 (……)

Perl在这些情况下不试图解决Unicode角色的一个原因是答案高度依赖于操作系统和文件系统。 例如,文件名是否可以使用Unicode,以及使用何种编码,并不是一个便携式的概念。 (……)

  • chdir,chmod,chown,chroot,exec,link,lstat,mkdir,rename,rmdir, – stat,符号链接,truncate,unlink,utime,-X
  • %ENV
  • glob(又名<*>)
  • 打开,opendir,sysopen
  • qx(又名反向运营商),系统
  • readdir,readlink

所以readdir返回字节字符串,因为通常不可能事先知道文件名的编码。 有关为什么这是不可能的背景信息,请参阅例如:

  • 维基百科中的文件名 ,子部分“编码互操作性”,
  • 了解 unix.stackexchange.com上的Unix文件名编码

字符串比较:

现在,最后你试着比较读取文件名$filename_read和变量filename

 print "found\n" if $filename_read eq filename; 

在这种情况下, $filename_readfilename之间的唯一区别是$filename_read没有设置UTF-8标志(这不是Perl内部认为是“Unicode字符串”的东西 )。

现在有趣的是, eq运算符的结果将取决于$filename_read的字节是否是纯ASCII。 根据编码模块的文档:

在Perl中引入Unicode支持之前, eq运算符只是比较两个标量所代表的字符串。 从Perl 5.8开始, eq比较了两个字符串,同时考虑了UTF8标志。

当你解码时,UTF8的标志是打开的 – 除非你能明确地表示数据。

所以在你的情况下, eq将考虑UTF-8标志,因为$file_name_read不包含纯ASCII,因此它会考虑这两个字符串相等。 如果$filename_readfilename在相同的地方并且只包含纯ASCII字节(并且filename还设置了UTF-8标志, $filename_read没有设置UTF-8标志),那么eq会认为这两个字符串是相等的。 请参阅文档中的讨论以编码有关此行为背景的更多信息。

结论:

因此,如果您确信所有的文件名都是UTF-8编码的,您可以通过将从readdir返回的字符串解码为Unicode字符串(强制设置UTF-8标志)来解决问题。

 $filename_read = Encode::decode_utf8( $filename_read ); 

更多细节

注意:因为Unicode允许多个表示相同的字符,所以在Bärlauch存在两种形式的ä (拉丁语小写字母A组合)。 例如,

  • U + 00E4是NFC(Normalization Form标准构成)形式,
  • U + 0061.0308是NFD(归一化形式规范分解)形式。

在我的平台(Linux)上,UTF-8编码的文件名使用NFC形式存储,但是在Mac OS上它们使用NFD形式。 有关更多信息,请参阅Encode::UTF8Mac 。 这意味着如果您在Linux机器上工作,例如克隆由Mac用户创建的Git存储库,则可以轻松地在您的Linux机器上获得NFD编码的文件名。 所以Linux文件系统并不关心编码文件的编码方式, 它只是把它看作一个字节序列。 因此,即使我的语言环境是"en_US.UTF-8" ,我也可以轻松地编写一个创建ISO-Latin-1编码文件名的脚本。 当前的语言环境设置只是应用程序的指导原则,但是如果应用程序忽略语言环境设置,则不会阻止他们这样做。

所以如果你不确定从readdir返回的文件名是否使用NFC或NFD,你应该在解码完成后分解:

 use Unicode::Normalize; print "found\n" if NFD( $filename_read ) eq NFD( filename ); 

另请参阅Perl Unicode Cookbook部分“始终分解和重构”。

最后,为了更多地了解Locale如何在Perl中与Unicode一起工作,你可以看看:

  • perllocale ,“Unicode和UTF-8”部分
  • Encode :: Locale 。