readdir以哪种编码返回文件名?

Ren*_*ger 4 ubuntu perl encoding character-encoding

这是我希望found在执行时会打印的Perl脚本:

#!/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;
}
Run Code Online (Sandbox Code Playgroud)

该脚本首先创建一个带有常量名称的文件filename。(运行脚本后,我可以使用验证文件的存在,ls并且不使用“有趣”字符创建文件。)

然后,脚本将遍历当前工作目录found中的文件,并打印是否存在名称与刚创建的文件相同的文件。显然应该是这样。

但是,事实并非如此(Ubuntu,bash,LANG=en_US.UTF8

如果将常数更改为Barlauch,它将按预期方式工作并打印found

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

请问对此有什么解释,我该怎么做才能识别其中包含Umlaute的文件名?

Håk*_*and 5

改述的问题(按照我的解释)是:

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

(注意:这filename是一个Perl常数变量,因此这就是它缺少$前面的标记的原因。)

背景:

首先要注意:由于use utf8程序开头的语句filename,由于它包含非ASCII字符,因此将在编译时升级为Unicode字符串。从utf8编译指示的文档中:

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

并且,根据perluniintro部分“ Perl的Unicode模型”

一般原则是Perl尝试将其数据尽可能长地保留为八位字节,但是一旦无法避免Unicodeness,则将数据透明升级为Unicode。

...

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

中的非ASCII字符filename是字母ä。如果您使用ISO 8859-1扩展ASCII编码(Latin-1),则将其编码为字节值0xE4,请参见位于的ascii-code.com。但是,如果从中删除了该ä字符filename,则该字符将仅包含ASCII字符,因此即使您使用了utf8编译指示,也不会在内部将其升级为Unicode 。

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

写入文件:

写入文件时,文件名会怎样?如果filename是Unicode字符串,它将被编码为UTF-8。但是,请注意,不必先编码filenameencode_utf8( filename ))。有关更多信息,请参见使用Unicode字符创建文件名。因此,文件名以UTF-8编码字节的形式写入磁盘。

读回文件名:

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

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

在Perl中,仍有很多地方可以将Unicode(以某种编码或另一种编码)作为参数或作为结果接收,或者在Perl中都可以,但事实并非如此。(...)

以下是此类接口。对于所有这些接口,Perl当前(自v5.16.0起)仅假设字节字符串既作为参数又作为结果。(...)

在这种情况下,Perl不尝试解析Unicode角色的一个原因是答案高度依赖于操作系统和文件系统。例如,文件名是否可以采用Unicode以及采用哪种编码方式,并不是完全可移植的概念。(...)

  • chdir,chmod,chown,chroot,exec,链接,lstat,mkdir,重命名,rmdir,-stat,symlink,truncate,取消链接,utime,-X
  • %ENV
  • 全局(又名<*>)
  • 打开,opendir,sysopen
  • qx(又名反引号运算符),系统
  • readdir,readlink

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

字符串比较:

现在,最后您尝试将读取的文件名$filename_read与变量进行比较filename

print "found\n" if $filename_read eq filename;
Run Code Online (Sandbox Code Playgroud)

在这种情况下,$filename_read和之间的唯一区别filename$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 );
Run Code Online (Sandbox Code Playgroud)

更多细节

注意:由于Unicode允许相同字符的多种表示形式,因此在中存在两种形式的ä(带有组合DIAERESIS的拉丁文小写字母A)Bärlauch。例如,

  • U + 00E4是NFC(规范化形式规范组成)形式,
  • U + 0061.0308是NFD(规范化形式规范分解)形式。

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

因此,如果不确定从返回的文件名readdir是否使用NFC或NFD,则应在解码后始终分解:

use Unicode::Normalize;
print "found\n" if NFD( $filename_read ) eq NFD( filename );
Run Code Online (Sandbox Code Playgroud)

另请参见Perl Unicode Cookbook的 “始终分解和重新组合”部分。

最后,要了解有关语言环境如何在Perl中与Unicode一起工作的更多信息,可以看一下: