BOM(Byte Order Mark)은 파일 맨 앞에 붙는 세 바이트 EF BB BF 다. UTF-8 은 바이트 순서가 고정이라 BOM 이 필요 없지만, 일부 도구가 인코딩을 판별하는 표시로 쓴다.
붙여야 하는 경우가 있다. Windows 의 메모장이나 Excel 이 CSV 를 열 때 BOM 이 없으면 시스템 기본 코드 페이지로 읽어 한글이 깨진다. Windows PowerShell 5.1 은 BOM 없는 .ps1 을 ANSI 코드 페이지로 읽어 한글 주석에서 파서 오류를 낸다.
떼야 하는 경우도 있다. 셸 스크립트 첫 줄의 shebang 앞에 BOM 이 있으면 #!/bin/bash 를 인식하지 못한다. JSON · YAML 파서 일부가 거부한다. diff 와 git 에서 첫 줄이 항상 다르게 보인다.
file app.csv
head -c 3 app.csv | od -An -tx1
ef bb bf 가 나오면 BOM 이 있다. 여러 파일을 한 번에 보려면 다음과 같다.
find . -type f -name '*.md' -exec sh -c '
head -c 3 "$1" | grep -q $'"'"'\xef\xbb\xbf'"'"' && echo "BOM: $1"
' _ {} \;
간단하게는 file 출력으로 거른다.
find . -type f -name '*.ps1' -exec file {} + | grep BOM
printf '\xEF\xBB\xBF' > with_bom.csv
cat without_bom.csv >> with_bom.csv
제자리에서 바꾸려면 다음과 같다.
add_bom() {
local f=$1
head -c 3 "$f" | grep -q $'\xef\xbb\xbf' && return 0
{ printf '\xEF\xBB\xBF'; cat "$f"; } > "${f}.tmp" && mv "${f}.tmp" "$f"
}
이미 BOM 이 있으면 건너뛰는 검사를 반드시 넣는다. 두 번 돌리면 BOM 이 두 개가 되어 앞의 것만 BOM 으로 인식되고 나머지는 본문 첫 글자가 된다.
sed -i '1s/^\xEF\xBB\xBF//' app.sh
GNU sed 기준이다. macOS 의 BSD sed 는 -i '' 형태와 이스케이프가 다르므로 perl 이 이식성이 낫다.
perl -i -pe 's/^\x{EF}\x{BB}\x{BF}// if $. == 1' app.sh
여러 파일에 적용한다.
find . -type f -name '*.md' -print0 | xargs -0 sed -i '1s/^\xEF\xBB\xBF//'
인코딩 이름으로 다룰 수 있어 가장 명확하다.
from pathlib import Path
def add_bom(path: Path) -> None:
data = path.read_bytes()
if data.startswith(b"\xef\xbb\xbf"):
return
path.write_bytes(b"\xef\xbb\xbf" + data)
def strip_bom(path: Path) -> None:
data = path.read_bytes()
if data.startswith(b"\xef\xbb\xbf"):
path.write_bytes(data[3:])
텍스트로 다룰 때는 인코딩 이름을 쓴다. utf-8-sig 는 읽을 때 BOM 을 떼고 쓸 때 붙인다.
text = path.read_text(encoding="utf-8-sig") # BOM 이 있어도 없어도 읽힌다
path.write_text(text, encoding="utf-8-sig") # BOM 을 붙여 쓴다
path.write_text(text, encoding="utf-8") # BOM 없이 쓴다
Jython 처럼 파이썬 2 계열에서는 바이트로 직접 다룬다.
BOM = "\xef\xbb\xbf"
with open(path, "rb") as f:
data = f.read()
if not data.startswith(BOM):
with open(path, "wb") as f:
f.write(BOM + data)
바이너리 파일에 돌리지 않는다. 확장자나 file 출력으로 텍스트인지 먼저 거른다.
줄 끝 문자와 헷갈리지 않는다. BOM 은 파일 맨 앞, CRLF 는 각 줄 끝의 문제다. 둘 다 정리해야 하는 경우가 많다.
sed -i -e '1s/^\xEF\xBB\xBF//' -e 's/\r$//' app.sh
일괄 변환 전에 대상 목록을 먼저 출력해 확인한다. 리포지터리 전체에 돌리면 의도하지 않은 파일까지 바뀐다.