viで日本語の文字コードを自動判別

vim


今日は以前の「Ubuntuのvimを快適にする」の続きとして、今日は文字コードの自動判別の設定を紹介します。

最近のディストリビューションでは、デフォルトのロケールUTF-8 になっているので、CJKフォント(China, Japan, Korea……要はアジア系文字のフォント)さえ入っていれば、いちいちロケールを切り替えなくても、日本語でも韓国語でも表示できてしまいます。なので、コマンドの出力などでは、特に意識しなくとも、文字化けすることは殆どありません。

vimも標準で文字コードの自動判別や変換に対応していますので、どのような文字コードで書かれたファイルでもきちんと読み書きできるのですが、自動判別を上手く効かせるためには、多少設定してやる必要があります。

.vimrc の設定

私が使っている自動判別の設定は以下の通りです。~/.vimrc ファイルの末尾に追加してみてください。

:set encoding=utf-8
:set fileencodings=ucs-bom,iso-2022-jp-3,iso-2022-jp,eucjp-ms,euc-jisx0213,euc-jp,sjis,cp932,utf-8

encoding は、デフォルトで使用する文字コードで、自動判別に失敗した場合や、新規ファイルを作成する場合に使用されます。 vimの内部で使用する文字コードで、複数の文字コードを扱う人は、特に事情が無い限り、端末のLocaleと共にUTF-8に設定しておくことをお勧めします。ちなみに、自動判別に失敗した場合はこの文字コードとして開かれます。また、新規ファイルを作成する場合の文字コードのデフォルト値を指定したい場合は fileencoding に設定しますが、それを指定しなかった場合、このencodingの値が使われます。(2008-09-04 修正: 詳しくは文末の追記をご参照下さい。)

fileencodingsは、自動判別の設定で、ここに列挙された文字コードの順番で、対象ファイルを開いてみて、最初に上手くいった文字コードが採用されます。

この設定例は単に私の好みで設定しているので、参考にされる方はご自由に変更して頂いて構いませんが、設定項目には、以下に挙げるように幾つかの罠がありますのでご注意下さい。

  • 使える文字コードは、vim に内蔵された iconv 次第。(外部のコマンドを使用する手段もあるので、使いたい人は options.txt を熟読!)
  • その文字コードだとみなして開いてみて、不整合が無ければそれを採用してしまう
    • 例えば、アスキー文字だけのファイルは、ucsなど以外の大抵の文字コードにマッチしてしまうので、fileencodingsの最初に書かれた文字コードが採用されてしまう。
    • また、iso-2022系(JISなど)は、「エスケープシーケンス付きアスキーテキスト」とみなせるので、asciiにマッチする文字コード全てにマッチしてしまう。そのため、iso-2022系はそれらの文字コードよりも前に書かなければならない。
      • これらの事情から、JISファイルを自動判別に含めると、どうしてもアスキーファイルはJISファイルとして扱われるような設定になってしまう。
  • encoding が無指定の場合、端末のロケールに依存してデフォルト設定がかわる。UTF-8端末ならutf-8
  • fileencodings に encoding と同一の文字コードを含めると、fileencodings中の、それ以降に書かれていた文字コードは全て無視され、強制的にそれが採用される。文字化けしようとお構い無し。

任意の文字コードで保存する

上の罠の説明中にも書きましたが、前述の設定例では、純アスキー文字だけのファイルを開くと、JISエンコーディングとして開かれます。

この状態で日本語の文字を追記して保存すると、文字コードはJISで保存されますが、vimのコマンドモード上で以下のようにコマンドを打って変更してやれば、指定した文字コードで保存できます。

:set fenc=(文字コード)


(2008-09-08 訂正: ↑よりは↓のほうが適切。詳しくは後述)

:setl fenc=(文字コード)

このコマンドを使えば、「既存のファイルを別の文字コードに変換して保存」なんて事もできますね。

文字化けしたファイルを開きなおす

大抵のファイルは自動判別で正しく開けるのですが、日本語の文字が少なすぎたりすると判別できないこともあります。(例えば、"e3 81 82 e3 81 84"という6バイトのバイト列は、UTF-8で読むと"あい"、Shift-JISだと"縺ゅ>"になります。人間だと「前者が正しそうだ」という想像はつくと思いますが、後者が間違いだという絶対の保証はありませんので、このバイト列の文字コードは判別不能です。)

そんな場合はおもむろに、以下のコマンドを使って正しい文字コードを指定して開きなおしましょう。

:e ++enc=(文字コード)

もっと詳しく知りたい方は

設定の詳細については、 vim のヘルプ内の options.txt や mbyte.txt などを参照してください。

2008-09-08 追記

きちんと頭を整理せずに、適当に上記の説明を書いていたら、速攻で id:ka-nacht さんにツッコミをくらってしまいました。

というわけで、反省して、嘘が無い程度に以下に追記しました。

encoding, fileencoding, fileencodings の本当のところ

エントリ中に出てきた 3つのオプションを、もう少し真面目に解説すると、このようになります。

encoding(enc)
vimの内部で使用されるエンコーディングを指定する。編集するファイル内の全ての文字を表せるエンコーディングを指定するべき。(日本語を扱うのに latin1 を指定してみたり、Unicodeでしか表せない文字も扱うのにEUC-JPを指定したりしちゃダメ)
fileencoding(fenc)
そのバッファのファイルのエンコーディングを指定する。バッファにローカルなオプション。これに encoding と異なる値が設定されていた場合、ファイルの読み書き時に文字コードの変換が行なわれる。fencが空の場合、encodingと同じ値が指定されているものとみなされる。(つまり、変換は行なわれない。)
fileencodings(fencs)
既存のファイルを編集する際に想定すべき文字コードのリストをカンマ区切りで列挙したものを指定する。編集するファイルを読み込む際には、「指定された文字コード」→「encodingの文字コード」の変換が試行され、最初にエラー無く変換できたものがそのバッファの fenc に設定される。fencsに列挙された全ての文字コードでエラーが出た場合、fencは空に設定され、その結果、文字コードの変換は行われないことになる。fencsにencodingと同じ文字コードを途中に含めると、その文字コードを試行した時点で、「encoding と同じ」→「文字コード変換の必要無し」→「常に変換成功」→「fencに採用」となる。

これらのオプションが、実際にどのように作用しているかについて解説する前に、fileencoding の説明でさらっと書いている「バッファにローカル」の説明を加えておきましょう。

global と local

オプションには、1つの vim プロセス中で global な単一の値のみを保持するオプションだけでなく、バッファごとやウィンドウごとに異なる値を保持できるオプションもあります。

前述の enc と fencs は global なオプションなので、例えば vim で複数のファイルを開いていても、それらの enc (つまりvim内部の文字コード)は常に同一です。

一方、fenc は、バッファに local なオプションなので、vim内で共通のglobal値の他に、各バッファに固有のlocal値も持っています。オプションの効力が実際に及ぶのはlocalな値の方で、global値の方は新規のバッファの既定値でしかありません。このように「バッファにlocal」となっているおかげで、文字コードの違う複数のファイルを同時に開いても、問題が起こらないようになっているのです。

これらのオプションを操作するには set や setlocal(setl) といったコマンドを使用します。globalなオプションを操作する場合は、これらのコマンドに差はありませんが、localなオプションに対しては効果が違います。setコマンドをバッファにlocalなオプションに対して使用すると、global値も、そのバッファのlocal値も同時に変更されます。それに対し、setlocal を使うと、global値はそのままに、local値だけを変更することが出来ます。

具体的にどう作用するか

ここからは、~/.vimrc が以下のようになっている場合を例に、vim内部でどのような事が起きているかを解説します。

:set encoding=utf-8
:set fileencodings=euc-jp,sjis

起動時はオプションは以下のようになっています。

global
encfencsfenc
utf-8euc-jp,sjis(空)

さて、ここで、新規ファイル用にバッファを開いてみましょう。新規のバッファでは、localオプションの値はglobal値からコピーされますので、以下のようになります。

global新規ファイルのバッファ
encfencsfenc(メモリ上のデータ)
utf-8euc-jp,sjis(空)(空)(空)

バッファにテキストを書き込むと、encの値に従って、utf-8で扱われます。

global新規ファイルのバッファ
encfencsfenc(メモリ上のデータ)
utf-8euc-jp,sjis(空)(空)(UTF-8のテキスト)

このバッファがファイルに保存される際には、fencのlocal値は"空"なので、一切の変換は行なわれず、結果として正しくUTF-8のファイルが出来上がります。

さて、今度は、既存のsjis のファイル(sjis.txt)を開く場合を考えてみます。ファイルを開く場合は fencs の指定に従って変換が試行されます。

  • sjis.txt を euc-jp とみなして、utf-8 へ変換 → 失敗
  • sjis.txt を sjis とみなして、utf-8 へ変換 → 成功

この結果、sjis.txt の fenc に sjis が設定され、適切にファイルが開かれます。(この際、fencのglobal値は変更されません。)

globalsjis.txt のバッファ
encfencsfenc(メモリ上のデータ)
utf-8euc-jp,sjis(空)sjisutf-8に変換されたsjis.txt

ここで、このsjis.txtをeuc-jpに変換して保存するため、localのfencを変更してみましょう。私が最初に書いていたように、setコマンドを用いて変更すると、こうなります。

globalsjis.txt のバッファ
encfencsfenc(メモリ上のデータ)
utf-8euc-jp,sjiseuc-jpeuc-jputf-8に変換されたsjis.txt

この状態で sjis.txt のバッファをファイルに書き出すと、メモリ上のデータ(UTF-8のバイト列)は utf-8 (encの値)から euc-jp (fencのlocal値)に変換されるため、euc-jp のファイルとして適切に書き出されます。めでたしめでたし。

ところが、これでは fenc の global 値まで変更されてしまっています。

global
encfencsfenc
utf-8euc-jp,sjiseuc-jp

なので、ここで新しいバッファを開くと最初の例とは事情が違ってしまいます。新しいバッファに fencのglobal値が引き継がれますので、こうなります。

global新規ファイルのバッファ
encfencsfenc(メモリ上のデータ)
utf-8euc-jp,sjiseuc-jpeuc-jp(空)

テキストの編集は相変わらず UTF-8 で行なわれます。

global新規ファイルのバッファ
encfencsfenc(メモリ上のデータ)
utf-8euc-jp,sjiseuc-jpeuc-jp(UTF-8のテキスト)

ファイルを保存する時は、 UTF-8euc-jp の変換を通じて、euc-jpで保存されます。

こんな変な副作用を起こしてしまうのも何なので、文字コードを変換して保存したい場合には set ではなく、setlocal を使いましょう。ということですね。

fencのglobal値は、新規のファイルのデフォルトの文字コードになるわけなので、常にそれとして扱ってやるべきです。

何故、私は間違えたのか

理屈はうっすらとは理解しているのに、なんでこんな間違いを書いてしまったのか。

理由は簡単です。私は似非vim使いで、1プロセスのvimで何個もファイルを開いたり閉じたりしないからに他成りません(笑)

そんな使い方をしている限りは、最初に書いた通りのやり方や理解で、運用上は全く問題無いので、間違ったやりかたが染み付いちゃってたんですな。

もうちょっとちゃんと調べてから書いてれば……、せっかく気付くチャンスだったのに、と、反省です。