🥗

マルチバイト環境でのソースファイルの文字コードを管理する

2025/04/12に公開1

日本と海外(特に英語圏)のプログラミングを比較すると、もちろん違いは沢山あると思いますが、一番根本的な違いはソースファイルの文字コードだと思います。
プログラムで扱う文字列の文字コードの話ではないです。そこは世界中でUnicodeを使うようにしていると思いたいですが、ソースファイル自体の文字コードの話です。
日本では日本語のコメントが書けるように一般的にUTF-8のような文字コードを使うと思いますが、英語圏では使っているツールによりますが、昔からよっぽどの理由がない限り、ASCIIにした方が移植性が高いと言われます。
今は殆どのツールがデフォルトでUTF-8で保存するようになっていると思いますが、普通はソースコードの文字コードを意識することはあんまりないです。

UTF-8 BOM付きとは?

UTF-8には、普通のUTF-8BOM付きUTF-8という2択になります。
BOMByte Order Markという名前の略で、U+FEFF ZERO WIDTH NO-BREAK SPACEというUnicodeの文字のことを言っていて、BOM付きってことは、その文字をそのファイルの文字コードでファイルの頭に付けてあるということになります。
UTF-8ファイルの場合は、最初の3つのバイトがEF BB BFになっていて、それ以外は、普通のUTF-8のファイルと完全に同じになります。
WindowsではBOM付きUTF-8LinuxではBOM無しのUTF-8が多いと思います。
現在はWindowsのプロジェクトでBOM付きUTF-8ファイルを使う決まりになっています。
どちらを使うにしても、プロジェクト全体としては統一されていた方が混乱は少ないです。

もちろん人間が頑張って全ソースファイルの文字コードを統一することもできますが、ここはもう少し便利な方法を考えてみたいところです。
そのために、まずは指定されたディレクトリの中の全ファイルの文字コードをチェックして、UTF-8 BOM付きではないファイルをリストアップしてくれるPowershellのスクリプトを作ってみました。

UTF-8 BOM付きではないファイルをチェックするスクリプト

以下のPowershellスクリプトは、指定されたディレクトリにあるBOM付きUTF-8ではないファイルを全部リストアップしてくれます。

check_bom.ps1
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の場合は、BOMUTF-16で保存されるため、違うバイトになり、このチェックで引っかかります。

最初はBOM以外も、ファイルの内容がちゃんとUTF-8になっているかもチェックしたかったのですが、文字コードをチェックするのはかなり難しいです。
BOMが付いている場合でも、最初の数バイトが本当にBOMなのか、偶然そういうバイトになっているだけなのか、という問題もありますが、BOMが付いていない場合は、ファイルの内容だけで文字コードを読み取ることはできないです。ただの推測になります。
文字コードはファイルのメタデータに保存すればいいのですが、WindowsLinuxもファイルの文字コードをプロパティとして保存していないようです。
そのため、各エディタが各々でファイルの文字コードを推測したり、キャッシュしたりします。
極端の例では、ASCIIの文字しか使ってないUTF-8のファイルは同じ内容のASCIIのファイルと互換性のために完全一致するようになっています。
こういう問題があって、文字コードをもう少し簡単に分かるようにするために、BOMを追加したのだと思います。

話を戻しますね。
上記のスクリプトを使って、現在BOM付きUTF-8ではないファイルを全部洗いだして、統一化したとしても、いずれ誰かがまた違う文字コードのファイルを追加してしまうかもしれないので、それを阻止する必要があります。

UTF-8 BOM付きではないソースファイルのコミットの阻止

SVN

SVNには、コミット前やコミット後、アップデート前などのタイミングでスクリプトを実行できるHookという機能があります。
その機能を使って、コミット前に以下の文字コードをチェックするPowershellスクリプトを実行すれば、期待しない文字コードのファイルをコミットしようとすると、コミットをエラーにできます。

ForceUTF8.ps1
$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ならリポジトリに右クリックして、TortoiseSVNSettingsHook 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 finishHide the script while running checkboxes を有効にする。

Git

Gitの場合は、Gitリポジトリの中の .git\hooksにスクリプトを置けば、名前によって様々のタイミングでそのスクリプトを実行してくれます。例えば、pre-commitというファイルを置くと、コミット前に実行されます。
残念ながらPowershellのスクリプトを直接実行できないので、Powershellのスクリプトを実行するだけのスクリプトを置きます。

call_powershell
#!/bin/sh
c:/Windows/System32/WindowsPowerShell/v1.0/powershell.exe -ExecutionPolicy RemoteSigned -Command '.git\hooks\pre-commit.ps1'

実際のPowershellのスクリプトはSVNとはそんなに変わらないです。GitLinuxのパスを使うため、Windowsのパスに変える必要があります。

pre-commit.ps1
$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

結論

プロジェクトとして期待しない文字コードのファイルをリストアップしてくれるスクリプトと、コミットしようとしているファイルの文字コードを判定してエラーを返すスクリプトを作ることに成功しました。
まずは前者のスクリプトを使って、現在のファイルを全部整えてから、SVNGitでも設定できるコミット前Hookとして、後者のスクリプトを設定すれば、もう二度と文字コードの問題が発生しなくなります。
もしサーバー側でこういうのを設定できると、チームの全員がローカルで設定する手間を省けるのでさらに楽になりますね。


|cpp記事一覧へのリンク|

Discussion

dameyodamedamedameyodamedame

一応前提として、git(あるいはsvn)について書くのであれば、OSを限定しないで読む人がいると思います。UTF-8にBOMを付けたがったり付いてないと異なる挙動を示す文化はWindowsにしかありません。なので、Windows以外のOSを使ってる人にはBOMが付いていると難色を示す人が多いことは書いておいた方がいいです。

昔はともかく最近はBOMが付いていることを要求する環境も少なくなってきているので、まるで推奨するかのような記述、は控えた方がいいと思いますよ。

あとはgit bashを使うのであれば、fileコマンドなどで分類は比較的容易に行えます。最初の3バイトであれば

if [ "$(head -c 3 $1)" = $'\xEF\xBB\xBF' ]; then
        echo "UTF-8 with BOM!"
fi

とかで判定出来ます。エンコーディングの妥当性検証にはiconvコマンドが使えると思います。

PowerShellを使う場合、5.1ではBOM無しUTF-8を扱えませんが、6以降デフォルトでBOMなしになったり、.NETを使ってエンコーディングを調べることも比較的簡単に出来ますが、同様に環境によるバージョンの違いで頭を悩ませることになるかもしれません。svnはもう保守用でしか使われてないと思うので、環境をいじるのはあまり得策ではないかもしれません。