Windows環境でShift_JISなファイル名をPath::Class使って問題なく操作する方法

ちょっとした野暮プログラムで、あるフォルダのファイル名の抽出を行おうと思ったんだ。

ほんと軽い気持ちで、そう軽い気持ちでちょこちょこっとさ。

use Path::Class;
use Perl6::Say;

for my $file ( dir('./hoge')->children ) {
    say $file->basename;
}

こんなのね。すごいシンプル。ちょうシンプル。コレでhogeフォルダ以下のファイルを取得できるのね。

で、動かしてみたの。もちろん云わずもかなWindows環境ね。Windows環境。

するとね。うまくファイル名が抽出できなかったのさ。

そう、よくあることだ。Shift_JISだよShift_JISShift_JISのせいなんだ。ファイル名にShift_JISが含まれてるの。

Shift_JISの「予」や「表」とかの文字コードは2バイト目がバックスラッシュと同じなのでパスの解析をするときに誤作動するんだよね。いわゆる「予定表問題」ってやつね。まーよくあること。

いつもはこういう捨てコード書くときはPath::Classとか使わずにglob関数とかで取得して正規表現でファイル名を取ってきたりしてたんだけど、今日はなんとなくPath::Classでやりたかったのでやってみたらコレだよ。

もー、知らなければ知らないまま別になんとも思わないんだけど知ってしまったら知ってしまったで調べないと気がすまないのでしょうがなく調べたわけなのでメモとして残しておく次第です。

んでまあPath::Classなんだけどコレって結局のところ、最終的にFile::Specを使ってパスの解析とかやってるのでとどのつまりFile::SpecがShift_JISに対応してないってことになりますのです。ね。

ということで良い解法が無いかググレカスしてみたらずばり良さげモジュールを発見しました。

おおおお、まさにコレジャン。CPANモジュールではないけども、こっちとしても所詮捨てコードなのでコレ使ってさっさと終わらせようと思ったのですよ。

さてさてではこのFile::Spec::Win32JをどうやってPath::Classで使うのかっていうことですが、それはとても簡単なのです。

use Path::Class qw/foreign_dir/;
use Perl6::Say;
use File::Spec::Win32J;

for my $file ( foreign_dir('Win32J','./hoge')->children ) {
    say $file->basename;
}

いつも使ってるdir関数の代わりにforeign_dirというのを使ってその第一引数に明示的にFile::Specのサブクラスを指定してあげるだけで良いのです。

よっしゃー1!!これでいけたぞ!!!・・・・・と思いきや誤作動。またも「予定表問題」


え????なじぇなじぇ????なじぇにゃの?予定表問題に対応したモジュールのはずなのに予定表問題が発動するなんて神様ひどいよ。

ということで今度は泣く泣くFile::Spec::Win32Jを見ることに。

・・・はい、わかりました。

splitpathがバグっちょります。いや、splitpathというかそこで使ってるto_slashがバグってます。

my $sjis = undef;
{
	my $single_sjis = '[\x00-\x2E\x30-\x5B\x5D-\x7F\xA1-\xDF]';
	my $double_sjis = '(?:[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
	$sjis = qr/(?:$single_sjis|$double_sjis)/; # SJIS文字
}

# 〜中略〜

sub to_slash {
	my $self = shift;
	my $path = shift;
	return '' unless (defined($path) and length($path));
	$path =~ s/(^|(?:$sjis)+)\\/$1\//go;
	$path =~ s/\/\\/\/\//go;
	return $path;
}

バックスラッシュ「\」をスラッシュ「/」に変換してる処理ですね。ここが曲者でした。

最初の正規表現ですが、これは

$path =~ s{\G($sjis*)\x5C}{$1\/}g;

こうすべきですね。コレで直ります。

で二つ目の正規表現ですがなんでこんなことをしているかというと

hoge/\muge.txt

こういうケースのパスの場合に「\」が変換されないからですね。何故変換されないかというと、$single_sjisというところで「/」の文字コードを意図的に外しているからです。

何故意図的に外しているのかはちょっと不明ですね。なので対応としてはメソッド内部で$sjisを再定義するといった感じでしょうか。

ということで最終的なコードは以下のような感じになりました。

use Path::Class qw/foreign_dir/;
use Perl6::Say;
use File::Spec::Win32J;

no warnings 'redefine';
local *File::Spec::Win32J::to_slash = sub {
    my $self = shift;
    my $path = shift;
    return '' unless (defined($path) and length($path));
    my $sjis = '(?:[\x00-\x5B\x5D-\x7F\xA1-\xDF]|[\x81-\x9F\xE0-\xFC][\x40-\x7E\x80-\xFC])';
    $path =~ s{\G($sjis*)\x5C}{$1\/}g;
    return $path;
};

for my $file ( foreign_dir('Win32J','./hoge')->children ) {
    say $file->basename;
}

いやー、動いた動いた。なんとメンドウくさい作業だったのだろう。ほんの、ほんの軽い気持ちの捨てコードの予定だったのに。

ってかさ、もうto_slashを一時的に上書きするようなコードになるんだったらさ。なんつーかCPANモジュールでもないFile::Spec::Win32Jなんて使わずにIO::Dir::readを上書きしてdecodeしたほうがいいよね。

use Path::Class;
use Perl6::Say;
use Encode;

no warnings 'redefine';
local *IO::Dir::read = sub { decode 'shiftjis', readdir shift };

for my $file ( dir('./hoge')->children ) {
    say encode 'shiftjis', $file->basename;
}

ぐだぐだな感じですが終わりです。なんか他に良い方法があれば・・・うれしいな。