彷徨えるフジワラ

年がら年中さまよってます

MBCS 文字列の折り返し - その 1

Mercurial の 1.4 版から、端末表示幅にあわせてヘルプテキストが折り返されるようになったのだけど、案の定というか、言語設定を日本語にしてある(LANGUAGE=ja 等)場合、折り返された行末で表示が乱れるケースがちらほらと。

ぱっと見、複数バイトで構成される文字の途中で改行されているんだろうなぁ、と原因の目星は付いていたのだけど、何やかにやでついつい後回しになっていたパッチ作成作業に、ようやく着手することに。

ソースを追いかけていくと結局のところ、Python 標準の行折り返しライブラリである textwrap モジュールが、明らかに1文字=1バイトな文字コードしか念頭に置いていないことが原因っぽいことが判明。
そんな textwrap を Mercurial で使ってしまっていること自体は、まぁ良しとしましょう。でも、21 世紀ももうすぐ 10 年が経とうとしているこのご時勢に、MBCS(Multiple Byte Character Set) に対応した textwrap ライブラリが無いなんてことは無いよね? > Python、と思っていたのだけど、ちょいと探した限りでは、少なくとも標準ライブラリとしては提供されてないみたい。

MBCS なテキストデータを Python で行折り返そうとする人が居ないのか、それとも本家にマージされるような働きかけが無いのか。はたまた、今時は HTML 化してブラウザで参照するのが主流になっていて、tty 的な「行折り返し」なんてものは必要性が低いのか。GUI 系だと text + font metrics で画面上のサイズを算出するのが一般的だろうから、確かに「行折り返し」のニーズは低いのかも....

とは言うものの、Google で検索したら、textwrap モジュールの MBCS 対応化を行っている人が居た。しかも、動機がまさに Mercurial での行折り返し問題。世間は狭い(笑)。なので、これを流用させてもらうことに。

dayflower 氏による元々の実装は独立したモジュールとしても機能するようなものだけど、まずは Mercurial に取り込むことを前提とすることに。そうすることで、入力テキストがバイト列であることを仮定できるので:

  • unicode 判定/符号化変換を除外できる
  • 既にバイト列なので _mb_len() が不要 ⇒ _wrap_chunks() の override が不要

という具合に随分簡略化できる。何時の世も「パッチは極力小さく」が鉄則。

Python 標準の textwrap 機能を使用してる箇所を問答無用で置き換えて、諸々の辻褄を合わせたら、早速動作確認。

COLUMNS 環境変数設定により端末表示サイズを任意に設定できることを利用して、奇数桁に指定した状態で help 表示をさせてみる。

あれ?古い版でもちゃんと表示されるぞ?おかしいなぁ.... あ、ファイルにリダイレクトした奴を Emacs で開くと確かに行末データがおかしくなっている。Emacs の shell モードで実行した場合も、同様に行末が変になっている。

そうか!確認に使用している環境では、Windows 上で Cygterm と TeraTerm の組み合わせを使用しているんだけど、どうやら壊れた行末データを TeraTerm が頑張って表示してたっぽい!やるなぁ、TeraTerm。でも今回に限っては完全に余計なお世話だよ....

ということで、動作確認を Emacs の shell モード上に移動。

旧版では壊れていた行末部分のデータが、修正版では適切になっていることを確認。

ここまでの動作確認は、COLUMNS 指定を長くすることで、ヘルプドキュメントのメイン部分の行折り返しについて確認したけど、Mercurial はオプションリスト表示でも textwrap を使っているので、今度は COLUMNS 指定を短くしての動作確認。

....あれ? unicode 変換で例外が浮揚されるぞ?呼び出し元の util.py を見てみると:

    # To avoid corrupting multi-byte characters in line, we must wrap
    # a Unicode string instead of a bytestring.
    try:
        u = line.decode(encoding.encoding)
        w = padding.join(textwrap.wrap(u, width=width - hangindent))
        return w.encode(encoding.encoding)
    except UnicodeDecodeError:
        return padding.join(textwrap.wrap(line, width=width - hangindent))

ぐは!ここだけ MBCS 対応してる!なんて中途半端な....

そうか、COLUMNS 指定を短くしても旧版ではオプションリスト表示部分が全然折り返されてないのが不思議だったけど、unicode 変換されていることで 1 文字=1 桁とみなされてしまう、つまり日本語メッセージの場合は相当画面幅を狭めないと桁溢れとみなされなかったから折り返しがされてなかったのか。

更に更に、dayflower 氏の元実装で入っていた unicode 判定処理は、これに対応するものだったのではなかろうかと勝手に推測。確かにこれだと _wrap_chunks() を override しないと駄目だわ。

っつーことで、呼び出し側の unicode 変換をざっくり削除することに。そして動作確認。

....
オプション:

 -A --after    手動で削除済みのファイ
               ルに対して、登録除外の
               旨を記録
 -f --force    追加登録/変更対象であ
               っても登録除外(ファイル
               は削除)
 -I --include  パターンに合致したファ
               イルを処理対象に追加
 -X --exclude  パターンに合致したファ
....

よし、折り返し成功。

あと一箇所、template 機能の fillXXX 系フィルタが textwrap を使っているので、こちらは patch の commit log を適当な長い日本語にすることで動作確認&期待通りの動作であることを確認。

良い感じのパッチを作れそうな目処が付いたかな?