use utf8 環境下で => オペレータの左辺が UTF8 flag on になってしまう

use strict;
use warnings;

sub Dump {
    @_ = map {
             sprintf "'%s'(%s)", $_, utf8::is_utf8($_) ? 'utf8' : 'bytes'
         } @_;

    print {*STDERR} join(q{, }, @_), "\n";
}

no utf8;

Dump( foo => 'bar' );
#=> 'foo'(bytes), 'bar'(bytes)

no utf8; だもんで,両者とも bytes なのは,まぁあたりまえ。


ところが,これを use utf8; で動かすと……

use utf8;

Dump( foo => 'bar' );
#=> 'foo'(utf8), 'bar'(bytes)

foo の UTF8 flag が on になってる!


use utf8 にしてたので,任意の文字列リテラルが UTF8 flag on になっても文句はいえないです。しかし,一般の文字列リテラルでは,Latin-1 の character set に収まっている場合,UTF8 flag は on になりません。実際 'bar' は Latin-1 に収まっているので UTF8 flag は off です。

ということで,回避方法?ですが,=> オペレータの左辺を,きちんとした文字列様式(つまりクォーテーションで囲うなど)にすると

use utf8;

Dump( 'foo' => 'bar' );
#=> 'foo'(bytes), 'bar'(bytes)

両者とも bytes になります。

にて確認しました。

仕様といえば仕様?バグ?

2009-01-29 追記

=> に限らなかったです。なんというか,生の(文字列とみなされる)リテラルならそうなる感じ?

use utf8;

my %h;
$h{foo} = 'bar';
$h{'baz'} = 'ban';

Dump(%h);
#=> 'baz'(bytes), 'ban'(bytes), 'foo'(utf8), 'bar'(bytes)

my $d = <<'END_DOC'
foo
END_DOC
chomp $d;

Dump($d)
#=> 'foo'(utf8)

ヒアドキュメントはまぁ仕方ないですけど,それ以外のケースは,なんというか直感に反するというかなんというか。

どこで困った?

URI::query_form() ではまりました。

use URI;

my $uri = URI->new('http://example.com/');

use utf8;

$uri->query_form( bytes => "\x{a4}" );
print $uri, "\n";
#=> http://example.com/?bytes=%C2%A4

use utf8 してたから "\x{a4}"U+00A4 としてみなされたんだろうなぁ((ほんとはここ誤解です。utf8::is_utf8("\x{a4}") しても off です。)),しかたねえなぁと思って別の API をたたいてみたら,

$uri->query( "bytes=\x{a4}" );
print $uri, "\n";
#=> http://example.com/?bytes=%A4

きちんとエンコードできるでないか。なぜだなぜだ。

と思って調べたら,先ほどの現象をみつけたわけです。

先ほどと同じように

$uri->query_form( 'bytes' => "\x{a4}" );
print $uri, "\n";
#=> http://example.com/?bytes=%A4

とすれば回避できます。


URI::query_form()

                push(@query, "$key=$val");

というところで,たとえ $val が UTF8 flag off だったとしても $key が(=> の左辺などで)UTF8 flag on だった場合,全体が UTF8 flag on に upgrade してしまうんですね。んでその場合,UTF8::Escape::escape_char() が UTF8 flag on 文字列を UTF8 octets に convert してしまうので上記のような変なエスケープになっているわけです。