我正在开发一个应用程序,该应用程序在命令行接受用户的regex作为输入,然后应用该regex来查找当前目录下的某些文件。该应用程序支持UTF-8输入,并且应该能够找到UTF-8编码的文件名。这里有一个例子:
use feature qw(say);
use open qw( :std :utf8 );
use strict;
use utf8;
use warnings;
use Encode ();
use File::Find::Rule;
system 'touch', 'aæ', 'bæ', 'aa'; # some test files,
my $pat = 'æ$';
my $pat_encode = encode( $pat );
run_test( $pat_encode, 'With encode()' );
run_test( $pat, 'Without encode()' );
my $pat2 = '[æ]$';
my $pat2_encode = encode( $pat2 );
run_test( $pat2_encode, 'With encode()' );
sub encode {
return Encode::encode('UTF-8', $_[0], Encode::FB_CROAK | Encode::LEAVE_SRC);
}
sub run_test {
my ( $pat_encode, $test_str ) = @_;
say $test_str;
say '-' x length $test_str;
say "";
my @files = File::Find::Rule->new->name( qr/$pat_encode/ )->in('.');
for (@files) {
$_ = Encode::decode('UTF-8', $_, Encode::FB_CROAK | Encode::LEAVE_SRC );
}
say $_ for @files;
}
输出为:
With encode()
-------------
aæ
bæ
Without encode()
----------------
With encode()
-------------
aæ
bæ
我预计最后一个正则表达式[æ]$
在编码后将无法工作,因为æ
将扩展到两个字节的0xC3A6
,但不知何故,Perl似乎知道正则表达式是用UTF-8编码的,并为其工作做了一些神奇的事情。
我想知道是否有人知道后一个例子为什么有效,以及是否有其他情况下编码regex不起作用?(因此,我正试图决定是否可以使用File::Find::Rule
,或者是否应该切换到File::Find
,这将使我避免对正则表达式进行编码。)
事实证明,对正则表达式进行编码是不安全的。特别是如果括号表达式后面跟着一个或多个字符,正则表达式可能会选择不需要的文件。原因是UTF-8编码版本中只有一个字节会与括号表达式匹配。考虑对我的脚本进行以下修改:
system 'touch', 'aæ', 'aæ1', 'aa'; # some test files,
my $pat = 'æ.$';
my $pat_encode = encode( $pat );
run_test( $pat_encode, 'With encode()' );
run_test( $pat, 'Without encode()' );
my $pat2 = '[æ].$';
my $pat2_encode = encode( $pat2 );
run_test( $pat2_encode, 'With encode()' );
现在,这应该只返回文件aæ1
,但是$pat2
regex也将返回aæ
,因为括号表达式只会用完编码的æ
的两个字节中的第一个,留下最后一个字节由$pat2
中的尾随.
匹配。
输出为:
With encode()
-------------
aæ1
Without encode()
----------------
With encode()
-------------
aæ
aæ1
解决方案似乎是使用File::Find
:
use File::Find ();
system 'touch', 'aæ', 'aæ1', 'aa'; # some test files,
my $pat = '[æ].$';
my $files = find_files( $pat );
say $_ for @$files;
sub decode {
return Encode::decode('UTF-8', $_[0], Encode::FB_CROAK | Encode::LEAVE_SRC );
}
sub find_files {
my ( $pat ) = @_;
my @files;
File::Find::find( sub { wanted( $pat, @files ) }, '.' );
return @files;
}
sub wanted {
my ( $pat, $files ) = @_;
my $name = decode( $_ );
my $full_name = decode( $File::Find::name );
push @$files, $full_name if $name =~ /$pat/;
}
现在输出正确:
./aæ1
更新:
事实上,File::Find::Rule
毕竟是可以使用的。只需使用exec
规则而不是name
规则:
my $pat = '[æ].$';
my $files = find_files( $pat );
say for @$files;
sub find_files {
my ( $pat ) = @_;
my @files = File::Find::Rule->new->exec( sub { wanted( $pat ) } )->in('.');
for (@files) {
$_ = decode( $_ );
}
return @files;
}
sub wanted {
my ( $pat ) = @_;
my $name = decode( $_ );
return ( $name =~ /$pat/ ) ? 1 : 0;
}
现在的输出是:
aæ1