[플밍노트] FreeBSD에서 BOM 제거를 위한 삽질기

각 메이저 진영은 모두 마치 약속이나 한 것처럼 미묘한 차이로 우리를 괴롭힌다. 마이크로소프트와 오픈소스 진영 간의 차이는 역사적으로 더 골이 깊다. UTF16과 UTF32, 파라미터를 UTF16으로 변경하는 것과 기존 함수에 UTF8 변환 기능을 추가하는 것, 64비트 지원을 위해서 LLP64냐 LP64냐 등의 차이는 유명하다. 여기에 또 하나의 골치아픈 차이가 있다. UTF8 소스에 대한 BOM이다. msvc는 BOM이 있는 것만 컴파일을 해주고, gcc는 BOM이 없는 것만 컴파일을 해준다. 둘이서 짜고 우리를 괴롭히는 것 같다. 어떤걸 선택해도 우리는 양쪽 다 만족시킬 수가 없다. 결국 양쪽 다 만족시키기 위해서는 한쪽에다 변환기를 설치해야 한다. 우리는 gcc를 위해서 BOM을 제거하기로 결정했다.

#0

find . -type f -exec sed -i '1 s/^\xef\xbb\xbf//' {} \;

우리는 이 명령어를 이용해서 UTF8 BOM을 제거했다. 한동안 잘 사용했는데 이게 유닉스 진영에서도 문제가 될지는 몰랐다. FreeBSD를 만나기 전까지는 말이다. FreeBSD에서는 위 명령어가 제대로 동작하지 않았고 그 미묘한 차이로 인해 결국 나는 몇 시간을 허비했다.

#1

find . -type f -exec sed -i .bak '1 s/^\xef\xbb\xbf//' {} \;

처음 구글 신이 나에게 알려준 것은 sed의 -i 옵션이 리눅스와 BSD가 다르다는 것이었다. BSD는 -i의 옵션 다음에 원본을 저장할 확장자를 지정해야 한다는 것이었다. 위의 커맨드는 그렇게 변경한 것이다. .bak 파일에 원본을 백업하고 편집된 파일을 원본에다 덮어씌우는 기능을 한다. 명령어 오류가 없이 잘 돌아가는 것 같았다.

#2

find . -type f -exec sed -i .bak '1 s/^\xef\xbb\xbf//' {} \; -exec rm {}.bak \;

1차 시도에 백업 파일을 제거하는 기능을 덧붙인 것이다. 여기까지 하고는 이제는 빌드가 잘 되겠지 하고는 돌렸다. 하지만 빌드는 여전히 되지 않았다. 파일을 열어보니 BOM은 그 자리에 그대로 존재했다. 여기서 가장 많은 시간을 허비했다. 도대체 왜 BOM 제거가 안 되는 것이지? 구글님도 딱히 뾰족한 답을 주지 않았다. 삽질 끝에 알아낸 사실은 FreeBSD에 포함된 sed가 \xef등의 16진수 표현을 지원하지 않는다는 것이었다. \xef등이 아닌 멀쩡한 문자를 넣으면 잘 제거됐다. 구글님은 너가 8진수를 쓰면 될지도 모를껄 이라고 알려줘서 \oNNN을 사용한 표기로 변경해 보았으나 소용이 없었다.

#3

find . -type f -exec sed -i .bak $'1 s/^\xef\xbb\xbf//' {} \; -exec rm {}.bak \;

bash핵을 사용한 변칙 방법이다. bash에서 $'' 안에 포함된 문자를 변환시킨다는 것을 이용한 방법이다. 여기서 깔끔하게 끝낼 수 있었는데 나는 이때까지 이 명령어를 makefile에 결합해서 사용하는 방법을 몰랐다. 그래서 16진수 표현을 지원하는 awk로 돌아섰다.

#4

find . -type f -exec awk 'NR==1{sub(/^\xef\xbb\xbf/, "")}{print}' {} \;

위 명령은 앞선 우리의 sed와 동일한 기능을 한다. 단지 파일에 저장하는 것이 아니라 변경된 내용을 화면에 출력할 뿐이다. awk에서 제자리 편집(in-place edit)를 하는 방법을 찾아 보았다. -i 옵션이 있었지만 FreeBSD에서는 응당 지원하지 않았다. 출력물을 결국 리다이렉션 시켜서 저장하는 방법 밖에는 없었다.

#5

find . -type f -exec awk 'NR==1{sub(/^\xef\xbb\xbf/, "")}{print > {}}' {} \;

find가 엮여 있어서 그냥 리다이렉션 시키기는 쉽지 않았다. 그러다 발견한 것이 awk의 위와 같은 문법이다. print 안에서 리다이렉션 구문을 쓸 수 있었다. 하지만 이렇게 하자 뭔가 잘 동작하지 않았다.

#6

find . -type f -exec awk -v out={} 'NR==1{sub(/^\xef\xbb\xbf/, "")}{print > out}' {} \;

그러다 -v를 사용해서 변수 값을 지정해서 사용하면 정상 동작한다는 것을 알아냈다. 여기서 FreeBSD를 정복하는 줄 알았지만 여전히 문제가 있었다. 정확한 원인은 모르겠지만 이렇게 저장된 파일은 원본의 전체가 아니라 일정한 길이 이후로 파일이 잘렸다. 전체 버퍼가 다 저장되지 않는 것처럼 보였다.

#7

find . -type f -exec sh -c "awk 'NR==1{sub(/^\xef\xbb\xbf/, \"\")}{print}' {} > {}" \;

결국 다시 쉴이다. 진작 알았으면 좋을 뻔 했다. "sh -c"로 특정 명령을 쉴로 실행할 수 있다는 사실이었다. 그래서 위와 같은 것을 생각해냈지만 결과는 참혹했다. 빈 파일만 덩그러니 남았다.

#8

find . -type f -exec sh -c "awk 'NR==1{sub(/^\xef\xbb\xbf/, \"\")}{print}' {} > {}.tmp" \;

임시 파일로 리다이렉션 시킬 수 밖에 없었다. 이 명령은 정확하게 동작했다.

#9

find . -type f -exec sh -c "awk 'NR==1{sub(/^\xef\xbb\xbf/, \"\")}{print}' {} > {}.tmp; mv {}.tmp {}" \;

임시 파일을 원본으로 덮어씌우는 것을 추가한 이 명령은 완벽했다. 이제 빌드 할 수 있게 되었다.

#10

몇 시간의 삽질 끝에 동작하는 두 가지 명령어를 알아냈다.

find . -type f -exec sh -c "awk 'NR==1{sub(/^\xef\xbb\xbf/, \"\")}{print}' {} > {}.tmp; mv {}.tmp {}" \;

이 명령은 bash가 필요하지는 않다. 다만 매칭된 파일 개수만큼 sh, awk, mv가 실행된다.

bash -c "find . -type f -exec sed -i .bak $'1 s/^\xef\xbb\xbf//' {} \; -exec rm {}.bak \;"

이 명령은 bash가 필요하다. 다만 sed, rm만 실행되기 때문에 sh가 추가로 실행되는 앞선 명령 보다는 효율적이다.