readdir以什么编码方式返回文件名?

时间:2016-05-04 11:53:22

标签: perl ubuntu 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;
}

该脚本首先创建一个名为常量filename的文件。 (运行脚本后,我可以使用ls验证文件是否存在,并且文件不是使用"搞笑"字符创建的。)

然后脚本迭代当前工作目录中的文件,如果存在名称等于刚刚创建的文件的文件,则打印found。显然情况应该如此。

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

如果我将常量更改为Barlauch,它会按预期工作并打印found

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

是否有这方面的解释,我该怎么办才能识别出包含Umlaute的文件名?

1 个答案:

答案 0 :(得分:4)

重新提出的问题(正如我解释的那样)是:

  

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

(注意:filename是一个Perl常量变量,这就是为什么它错过了前面的$ sigil。)

<强>背景

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

  

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

并且,根据perluniintro部分&#34; Perl的Unicode模型&#34;

  

一般原则是Perl试图将其数据保持为8位   尽可能长的字节,但只要Unicodeness不能   避免,数据透明地升级到Unicode。

     

...

     

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

filename中的非ASCII字符是字母ä。如果使用ISO 8859-1扩展ASCII编码(Latin-1),则将其编码为字节值0xE4,请在ascii-code.com处查看此table。 但是,如果从ä中删除了filename字符,它将只包含ASCII字符,因此即使您使用了utf8 pragma,它也不会在内部升级为Unicode。 / p>

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

撰写文件:

编写文件时,文件名会怎么样?如果filename是Unicode字符串,则它将被编码为UTF-8。但请注意,不必先对filename进行编码(encode_utf8( filename ))。有关详细信息,请参阅Creating filenames with unicode characters。因此文件名以UTF-8编码的字节写入磁盘。

回读文件名:

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

为什么没有readdir返回Unicode字符串?首先,根据 perlunicode部分&#34;当Unicode不发生时&#34;

  

仍有许多地方使用Unicode(在某些编码或   另一个)可以作为参数提供或作为结果或两者同时提供   在Perl中,但事实并非如此。 (...)

     

以下是此类界面。对于所有这些接口Perl   目前(从v5.16.0开始)只假设字节字符串为   论点和结果。 (...)

     

Perl不尝试解析Unicode角色的一个原因   在这些情况下,答案高度依赖于   操作系统和文件系统。例如,是否   文件名可以是Unicode,也可以是什么样的编码   不完全是便携式概念。 (...)

     
      
  • chdir,chmod,chown,chroot,exec,link,lstat,mkdir,rename,rmdir, - stat,symlink,truncate,unlink,utime,-X
  •   
  • %ENV
  •   
  • glob(又名&lt; *&gt;)
  •   
  • open,opendir,sysopen
  •   
  • qx(又名反引号运算符),system
  •   
  • readdir,readlink
  •   

所以readdir返回字节字符串,因为通常不可能先验地知道文件名的编码。有关无法实现此目的的背景信息,请参阅:

字符串比较:

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

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

在这种情况下,$filename_readfilename之间的唯一区别 是$filename_read没有设置UTF-8标志(它不是Perl内部认可的&#34; Unicode字符串&#34; )。

现在有趣的是eq运算符的结果将取决于$filename_read中的字节是否为纯ASCII。根据{{​​3}}模块的文档:

  

在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标志set),然后eq会认为两个字符串相等。请参阅Encode文档中有关此行为背景的更多信息的讨论。

<强>结论:

因此,如果您相信所有文件名都是UTF-8编码的,那么您可以通过将从readdir返回的字节字符串解码为Unicode字符串来解决问题中的问题(强制使用UTF-8标志)待定):

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

更多详情

注意:由于Unicode允许多个相同字符的表示,因此在ä中存在两种形式的Bärlauch(LATIN SMALL LETTER A WITH COMBINING DIAERESIS)。例如,

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

在我的平台(Linux)上,UTF-8编码的文件名使用NFC格式存储,但在Mac OS上,它们使用NFD格式。有关详细信息,请参阅Encode。这意味着如果您在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 );

另请参阅Encode::UTF8Mac部分&#34;始终分解和重组&#34;。

最后,为了更多地了解Locale如何与Perl中的Unicode一起使用,您可以查看: