UTF-8 BOMを確認・追加・削除するコマンド

MacOSX環境にて、コマンドでBOMの確認や追加・削除の必要がありましたので、方法を整理します。
bashとhead, sedコマンドを利用した方法なので、LinuxやAlpine Linuxなどのコンテナ環境、その他Unix、WindowsのGit Bash, MinGW環境でも動かしやすいはず。

## 背景
Microsoft Excelでは UTF-8エンコードのCSVファイルを読み込んだり、書き出したりできますが、BOMが付きます。
データ移行のため、顧客から受領したExcelファイルを元に、CSV出力したものをベースに、移行用のデータ加工を行う必要がありました。

`file`コマンドを使う簡単な確認方法があります。
しかし、csvファイルを判定しようとしたとき、手元のMacOSX環境のfileコマンドでは は判定結果がBOM有無に関わらず`CSV text`と出力され、その後のエンコード情報は出力してくれませんでした。
fileコマンドは、BSD系、System-V系の差異に加え、環境のmagicデータベースにも依存しますので、どの環境でも同じ出力が得られないのが難点です。

```bash
# よくある BOMなし、BOM付き UTF-8 テキストファイル
% file foo-no-bom.txt foo-with-bom.txt
foo-no-bom.txt:   UTF-8 Unicode text
foo-with-bom.txt: UTF-8 Unicode (with BOM) text

# しかし、CSVファイルではBOM有無を判別できない
% file data-no-bom.csv data-with-bom.csv
data-no-bom.csv:   CSV text
data-with-bom.csv: CSV text
```

##　確認方法

### UTF-8 BOM 確認その1 (fileコマンド encoding指定)

fileコマンドの`-e`オプションを使い、判定をencodingのみにします。
MacOSX環境でも、これでCSVファイルの判定ができます。

```bash
# -e encodingオプション指定でfileのエンコードを確認
% file -e encoding data-no-bom.csv data-with-bom.csv
data-no-bom.csv:   UTF-8 Unicode text, with CRLF line terminators
data-with-bom.csv: UTF-8 Unicode (with BOM) text, with CRLF line terminators
```

### UTF-8 BOM 確認その2 (先頭byteを見る)

BOMはファイル先頭に付く数バイトの識別用バイナリなので、直接ファイル先頭を調べて判定ができるはずです。
UTF-8 BOM の場合は `0xEF 0xBB 0xBF` ですね。
参考: [バイト順マーク \- Wikipedia](https://ja.wikipedia.org/wiki/%E3%83%90%E3%82%A4%E3%83%88%E9%A0%86%E3%83%9E%E3%83%BC%E3%82%AF)

以下は headコマンドで先頭3byteを切り出して判定しています。
※比較にシェルの拡張書式`$''`を使っているので、対応したシェル環境(bash, zsh, ashなど)で使ってください。

もちろん、perlでもrubyでもpythonでも何でもお好きなスクリプト言語を用いても良いでしょう。

```bash
# ファイルfile.txtがUTF-8 BOMを持つか確認する
[[ $(head -c 3 file.txt) == $'\xef\xbb\xbf' ]] && echo 'has utf8 bom!!'
```

### UTF-8 BOM 追加

```bash
# BOMなしファイルfile.txtにUTF-8 BOMを追加する
LC_ALL=C sed -e $'1s/^/\xef\xbb\xbf/' file.txt > file-with-bom.txt
```

上記ではsedコマンドを使って、1行目の先頭にBOMを追加しています。

もちろん、わざわざsedコマンドを使わなくても、以下のように単純にBOMを出力した後に元のファイル内容を出力するという方法も考えられます。この方法の場合は、元ファイルが空(ファイルサイズが0byte)の場合にBOMの3byteだけが入った空のテキストファイルが生成されます。

```bash
# 単純な方法 (※ファイルが空の場合はBOMのみの3byteファイルが生成される)
(echo $'\xef\xbb\xbf'; cat file.txt) > file-with-bom.txt
```

前述のsedを使った方法では、元ファイルが空(ファイルサイズが0byte)の場合は、出力ファイルも0byteのままとなります。
私は空の場合にファイルサイズだけで判定ができる0byteのままにしたい場合が多いので、sedを使う方法を主に使っていますが、空テキストファイルのBOMをどうするか要件によって使い分けてくださいね。
ちなみに、Windowsのメモ帳(notepad.exe)で空のテキスト文書を保存したときは3byteのBOMのみのファイルが作成されます。


### UTF-8 BOM 削除

```bash
# BOM付きファイルfile.txtからUTF-8 BOMを削除する
LC_ALL=C sed -e $'1s/^\xef\xbb\xbf//' file.txt > file-no-bom.txt
```

## コマンド化

よく使うなら、シェルスクリプト or シェル関数にしてコマンドとして使えるようにしておくと良いでしょう。

```bash:has-utf8-bom
#!/bin/bash
# UTF-8 BOM 確認
[[ $(LC_ALL=C head -c 3 -- "$@") == $'\xef\xbb\xbf' ]]
```

```bash:add-utf8-bom
#!/bin/bash
# UTF-8 BOM 追加
LC_ALL=C sed -e $'1s/^/\xef\xbb\xbf/' -- "$@"
```

```bash:remove-utf8-bom
#!/bin/bash
# UTF-8 BOM 削除
LC_ALL=C sed -e $'1s/^\xef\xbb\xbf//' -- "$@"
```

### 使い方例

例として、あるディレクトリ以下の UTF-8 BOM付きファイルを探して
元ファイルは .bak をつけてリネームし、BOMを削除したファイルを作成する、
というケース↓


```
# ディレクトリ ./src 以下のファイルを探して、UTF-8 BOMがついていれば削除する
find ./src -type f | while read F; do has-utf8-bom "$F" && mv "$F" "$F.bak" && remove-utf8-bom "$F.bak" > "$F" ; done
```

### Gistにも掲載しておきましたので、ご参考に。

* [Check if a file \(or stdin\) has UTF\-8 BOM](https://gist.github.com/htaketani/99bacc7410dff25105ef7b47fbf2b007)
* [Remove UTF\-8 BOM from a file \(or stdin\)](https://gist.github.com/htaketani/a69613e42fa941c8e7f4dca2478a0f55)
* [Add UTF\-8 BOM to a file \(or stdin\)](https://gist.github.com/htaketani/e8c2978aeb443d6de3ee3b038e8c232a)

## ライセンス

本記事に記載したコードは [CC0](https://creativecommons.org/publicdomain/zero/1.0/) (Public Domain)です。

## 参考

* [【メモ】コマンドでのBOMの追加・削除・確認 \- Qiita](https://qiita.com/tamanugi/items/63fe5cf8e709565777a5)