マルチバイト環境でのソースファイルの文字コードを管理する
日本と海外(特に英語圏)のプログラミングを比較すると、もちろん違いは沢山あると思いますが、一番根本的な違いはソースファイルの文字コードだと思います。
プログラムで扱う文字列の文字コードの話ではないです。そこは世界中でUnicode
を使うようにしていると思いたいですが、ソースファイル自体の文字コードの話です。
日本では日本語のコメントが書けるように一般的にUTF-8
のような文字コードを使うと思いますが、英語圏では使っているツールによりますが、昔からよっぽどの理由がない限り、ASCII
にした方が移植性が高いと言われます。
今は殆どのツールがデフォルトでUTF-8
で保存するようになっていると思いますが、普通はソースコードの文字コードを意識することはあんまりないです。
UTF-8 BOM
付きとは?
UTF-8
には、普通のUTF-8
とBOM
付きUTF-8
という2択になります。
BOM
はByte Order Mark
という名前の略で、U+FEFF ZERO WIDTH NO-BREAK SPACE
というUnicode
の文字のことを言っていて、BOM
付きってことは、その文字をそのファイルの文字コードでファイルの頭に付けてあるということになります。
UTF-8
ファイルの場合は、最初の3つのバイトがEF BB BF
になっていて、それ以外は、普通のUTF-8
のファイルと完全に同じになります。
Windows
ではBOM
付きUTF-8
、Linux
ではBOM
無しのUTF-8
が多いと思います。
現在はWindows
のプロジェクトでBOM
付きUTF-8
ファイルを使う決まりになっています。
どちらを使うにしても、プロジェクト全体としては統一されていた方が混乱は少ないです。
もちろん人間が頑張って全ソースファイルの文字コードを統一することもできますが、ここはもう少し便利な方法を考えてみたいところです。
そのために、まずは指定されたディレクトリの中の全ファイルの文字コードをチェックして、UTF-8 BOM
付きではないファイルをリストアップしてくれるPowershell
のスクリプトを作ってみました。
UTF-8 BOM
付きではないファイルをチェックするスクリプト
以下のPowershell
スクリプトは、指定されたディレクトリにあるBOM
付きUTF-8
ではないファイルを全部リストアップしてくれます。
param (
[parameter(mandatory=$true)]
[ValidateScript({Test-Path $_})]
$RootDirectory
)
Get-ChildItem -Path $RootDirectory -Recurse -Include *.cpp, *.c, *.hpp, *.h -ErrorAction SilentlyContinue | % {
try {
$Bytes = Get-Content -Path $_.FullName -Encoding Byte -Raw -ReadCount 3
if($Bytes[0] -ne 239 -or $Bytes[1] -ne 187 -or $Bytes[2] -ne 191) {
$msg="BOM not found: $_.FullName"
Write-Output $msg
continue
}
}
catch {
$msg="Check failed: $_.FullName"
Write-Output $msg
}
}
Powershell の中から以下のように実行できます。
./check_encoding -RootDirectory "path/to/your/workspace"
初めて Powershell スクリプトを実行する人は、まずは Powershell を管理者として実行して、以下のコマンドを実行しないといけないかもしれません。
set-executionpolicy remotesigned
コードを見ると分かると思いますが、このスクリプトはBOM
の存在のみをチェックしているため、UTF-8
ではないのに、頭にBOM
が付いているファイルは引っかからないです。
でも普通にエディタを使ってファイルを保存している限り、意図的にBOM
付きUTF-8
で保存しない限り、頭にBOM
が付くことはまずないです。
BOM
付きUTF-16
の場合は、BOM
もUTF-16
で保存されるため、違うバイトになり、このチェックで引っかかります。
最初はBOM
以外も、ファイルの内容がちゃんとUTF-8
になっているかもチェックしたかったのですが、文字コードをチェックするのはかなり難しいです。
BOM
が付いている場合でも、最初の数バイトが本当にBOM
なのか、偶然そういうバイトになっているだけなのか、という問題もありますが、BOM
が付いていない場合は、ファイルの内容だけで文字コードを読み取ることはできないです。ただの推測になります。
文字コードはファイルのメタデータに保存すればいいのですが、Windows
もLinux
もファイルの文字コードをプロパティとして保存していないようです。
そのため、各エディタが各々でファイルの文字コードを推測したり、キャッシュしたりします。
極端の例では、ASCII
の文字しか使ってないUTF-8
のファイルは同じ内容のASCII
のファイルと互換性のために完全一致するようになっています。
こういう問題があって、文字コードをもう少し簡単に分かるようにするために、BOM
を追加したのだと思います。
話を戻しますね。
上記のスクリプトを使って、現在BOM
付きUTF-8
ではないファイルを全部洗いだして、統一化したとしても、いずれ誰かがまた違う文字コードのファイルを追加してしまうかもしれないので、それを阻止する必要があります。
UTF-8 BOM
付きではないソースファイルのコミットの阻止
SVN
SVN
には、コミット前やコミット後、アップデート前などのタイミングでスクリプトを実行できるHook
という機能があります。
その機能を使って、コミット前に以下の文字コードをチェックするPowershell
スクリプトを実行すれば、期待しない文字コードのファイルをコミットしようとすると、コミットをエラーにできます。
$errors = 0
Get-Content -Path $args[0] | %{
if(Test-Path $_) {
$Bytes = Get-Content -Path $_ -Encoding Byte -Raw -ReadCount 3
if($Bytes[0] -ne 239 -or $Bytes[1] -ne 187 -or $Bytes[2] -ne 191) {
$msg="BOM not found: $_"
Write-Error $msg
$errors += 1
}
}
}
exit $errors
TortoiseSVN
ならリポジトリに右クリックして、TortoiseSVN
→ Settings
→ Hook Scripts
を選んで、Add...
をクリックして、以下の設定でスクリプトを追加します。
- Hook Type: Pre-Commit Hook
- Working Copy Path: リポジトリへのパス。
- Command Line To Execute: powershell -executionpolicy bypass -file path/to/file/ForceUTF8.ps1
- Wait for the script to finish と Hide the script while running checkboxes を有効にする。
Git
Git
の場合は、Git
リポジトリの中の .git\hooks
にスクリプトを置けば、名前によって様々のタイミングでそのスクリプトを実行してくれます。例えば、pre-commit
というファイルを置くと、コミット前に実行されます。
残念ながらPowershell
のスクリプトを直接実行できないので、Powershell
のスクリプトを実行するだけのスクリプトを置きます。
#!/bin/sh
c:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy RemoteSigned -Command '.git\hooks\pre-commit.ps1'
実際のPowershell
のスクリプトはSVN
とはそんなに変わらないです。Git
はLinux
のパスを使うため、Windows
のパスに変える必要があります。
$errors = 0
# コミットする予定のファイルのリストを取得する
$changes = git diff --cached --name-only
foreach ($change in $changes)
{
# GitはLinuxのパスを返すので、Windowsのパスに変える
$winPath = $change.replace("/", "\")
$winPath = ".\$winPath"
if(Test-Path $winPath) {
$Bytes = Get-Content -Path $winPath -Encoding Byte -Raw -ReadCount 3
if($Bytes[0] -ne 239 -or $Bytes[1] -ne 187 -or $Bytes[2] -ne 191) {
$msg="BOM not found: $winPath"
Write-Error $msg
$errors += 1
}
}
}
exit $errors
結論
プロジェクトとして期待しない文字コードのファイルをリストアップしてくれるスクリプトと、コミットしようとしているファイルの文字コードを判定してエラーを返すスクリプトを作ることに成功しました。
まずは前者のスクリプトを使って、現在のファイルを全部整えてから、SVN
とGit
でも設定できるコミット前Hook
として、後者のスクリプトを設定すれば、もう二度と文字コードの問題が発生しなくなります。
もしサーバー側でこういうのを設定できると、チームの全員がローカルで設定する手間を省けるのでさらに楽になりますね。
Discussion
一応前提として、git(あるいはsvn)について書くのであれば、OSを限定しないで読む人がいると思います。UTF-8にBOMを付けたがったり付いてないと異なる挙動を示す文化はWindowsにしかありません。なので、Windows以外のOSを使ってる人にはBOMが付いていると難色を示す人が多いことは書いておいた方がいいです。
昔はともかく最近はBOMが付いていることを要求する環境も少なくなってきているので、まるで推奨するかのような記述、は控えた方がいいと思いますよ。
あとはgit bashを使うのであれば、fileコマンドなどで分類は比較的容易に行えます。最初の3バイトであれば
とかで判定出来ます。エンコーディングの妥当性検証にはiconvコマンドが使えると思います。
PowerShellを使う場合、5.1ではBOM無しUTF-8を扱えませんが、6以降デフォルトでBOMなしになったり、.NETを使ってエンコーディングを調べることも比較的簡単に出来ますが、同様に環境によるバージョンの違いで頭を悩ませることになるかもしれません。svnはもう保守用でしか使われてないと思うので、環境をいじるのはあまり得策ではないかもしれません。