맥에서 한글 파일명이 find·grep·셸 글롭에 안 잡힐 때: NFC와 NFD가 갈리는 자리와 이름을 맞추는 방법을 직접 시험한 결과When a Korean filename on a Mac is not found by find, grep, or shell globs: where NFC and NFD diverge and how to make the names match, tested directly
맥에서 한글 이름 파일이 화면에는 보이는데 find·grep·셸 글롭·파이썬·git 에서 안 잡히거나 압축·저장소에서 이름이 어긋난 독자가, 자기 파일이 NFC 인지 NFD 인지 가리고 도구마다 어느 쪽을 못 찾는지 알아 이름을 맞추게 돕는다.
이 맥(26.6.2, APFS)에서 NFD 로 저장된 한글.txt 는 [ -e ]·ls·파이썬 exists 에서는 NFC 로 쳐도 찾히지만 find -name, grep -F, bash 글롭, 파이썬 목록 비교에서는 안 잡힌다. bash 3.2 의 `touch` 등과 파이썬이 만든 파일은 준 이름 그대로 저장되고, `NSFileManager` 한 가지 호출과 bsdtar 풀기는 NFD 로 저장했다. git 은 core.precomposeunicode 가 켜져 있으면 NFC 로 기록한다. 이름은 mv 나 os.rename 으로 NFC 로 바꿀 수 있고, 비교할 땐 양쪽을 같은 정규형으로 맞춘다.
문제 폴더에서 글의 `isnfd` 루프로 `NFD:` 줄이 찍히는 이름이 있는지 확인하고, 있으면 글에 실은 파이썬 한 방으로 NFC 로 바꾼 뒤 다시 찾아 보라. 압축이나 저장소가 문제라면 해당 절의 명령으로 이름 바이트부터 비교하라.
목차
한 줄 요약: 맥에서 NFD 로 저장된 한글 파일명은 존재 확인에는 NFC 로 쳐도 잡히지만 find -name·grep -F·bash 글롭에는 안 잡히므로, 이름을 NFC 로 바꾸거나 찾는 말을 NFD 로 맞춘다.
ls 로 보면 한글.txt 가 분명히 있는데 find . -name '한글*' 은 0개를 내고 ls | grep -F 한글.txt 는 빈 출력에 종료 코드 1이다. 스크립트는 파일이 없다고 하고 눈은 있다고 한다. 이 글은 그 파일이 NFD 라는 방식으로 저장돼 있을 때 어떤 도구가 어떻게 달라지는지를 같은 파일 하나로 재고, 어떤 방법이 그런 파일을 만드는지, 압축과 git 은 이름을 어떻게 남기는지, 이름을 어떻게 맞추면 되는지를 16진수 바이트로 보인다.
확인한 환경은 macOS 26.6.2 arm64 의 APFS(대소문자를 구분하지 않는 기본 볼륨), /bin/bash 3.2.57, zsh 5.9, 파이썬 3.9.6, /usr/bin 의 BSD grep·find, bsdtar 3.5.3, 맥 기본 rsync(openrsync, protocol version 29), Info-ZIP zip 3.0, Apple Git 2.54.0 이고 날짜는 2026-11-05 이다. 시험 스크립트 다섯 개를 두 번씩 돌렸고 두 번째 출력은 첫 번째와 글자까지 같았다. 한글 이름은 한글.txt 와 폴더 몇 개만 썼다.
용어 몇 개. 같은 글자 한 을 한 덩어리로 적는 방식이 NFC(완성형)이고, 자음과 모음 조각 ㅎ+ㅏ+ㄴ 으로 나눠 적는 방식이 NFD(분해형)다. 화면에는 똑같이 보이지만 저장되는 바이트가 다르다. 이름을 16진수로 찍으면 한글.txt 는 NFC 로 ed959c eab880 2e747874 10바이트이고 NFD 로 e18492 e185a1 e186ab e18480 e185b3 e186af 2e747874 22바이트다. 정규화는 이 둘 사이를 오가며 바꾸는 일이다. 글롭은 * 나 ? 로 이름 묶음을 적는 패턴이다.
같은 이름을 두 가지로 칠 수 있어도 파일은 하나다
APFS 에 대해 애플의 FAQ 는 이렇게 적는다. “preserves both case and normalization of the filename on disk in all variants”, 그리고 macOS High Sierra 의 APFS 는 “normalization-insensitive” 라서 “a filename can be found with any of its normalization variants” 라고 한다. 같은 폴더에 정규형만 다른 이름을 둘 만들 수 없고, 어느 쪽으로 쳐도 파일을 찾는다는 뜻이다. 저장은 만든 그대로 보존한다.
이 맥에서 그대로 나왔다. NFD 이름으로 touch 한 파일을 NFC 로 친 이름으로 확인하면 다음과 같았다.
| 시도 | 결과 |
|---|---|
[ -e "NFC이름" ] |
종료 코드 0 (있음) |
파이썬 open(NFC이름, "x") |
FileExistsError: [Errno 17] File exists |
touch "NFC이름" |
종료 코드 0, 폴더 항목은 계속 1개, 저장된 바이트는 NFD 그대로 |
rm "NFC이름" |
종료 코드 0, 항목 0개 |
이 FAQ 는 High Sierra 시절 기준의 보관 문서이고 macOS 26 에서도 같은지는 이 맥의 시험으로만 확인했다. FAQ 는 이어서 “any additional work to ensure correct normalization behavior” 가 필요 없다고 쓴다. 파일을 여는 쪽에서는 맞는 말이다. 문제는 이름을 열지 않고 글자로 비교하는 도구에서 생긴다.
어떤 방법이 NFD 이름을 만드는가
맥이 한글 파일명을 NFD 로 저장한다는 설명은 공개 글 세 건이 다른 표현으로 적고 있다(조합형으로 저장된다, NFD 로 표기된다, NFD 방식을 쓴다). 이름을 NFC 로 주고 파일을 만든 뒤 저장된 바이트를 확인했다.
| 만든 방법 | NFC 로 준 이름의 저장 형태 |
|---|---|
bash touch, mkdir, > 리다이렉트, cp |
NFC 그대로 |
파이썬 open(이름, "w") |
NFC 그대로 |
osascript -l JavaScript 로 부른 NSFileManager createFileAtPath |
NFD (39바이트, 이름 뒤쪽이 e18492 e185a1 …) |
tar -xf nfc.tar (아카이브 안 이름은 NFC) |
NFD |
tar -cf - src | tar -xf - |
NFD |
cp -R, ditto 폴더 복사, rsync -a, unzip, ditto -x -k |
NFC 원본은 NFC 그대로 (unzip 은 NFD 로 담긴 이름도 NFD 그대로 풀었다) |
NFD 를 준 이름은 touch 와 파이썬 둘 다 NFD 로 저장했다. 정리하면 NFC 로 준 이름은 이 맥의 touch·mkdir·cp, bash 의 > 리다이렉트, 파이썬이 바꾸지 않았고(NFD 로 준 이름은 touch 와 파이썬만 시험했다), Cocoa 파일 API 한 가지와 이 맥의 bsdtar 풀기가 NFC 를 NFD 로 바꿔 저장했다. 이 시험은 NSFileManager 한 가지 호출만 봤다. Finder 에서 이름을 바꾸거나 파일을 만드는 것, AirDrop, 브라우저 다운로드, 메신저로 받은 파일, 앱의 저장 대화상자는 시험하지 않았으므로 그쪽이 어느 형태로 저장하는지는 이 글로 말할 수 없다. bash 3.2 명령과 파이썬으로 만든 파일은 NFC 가 그대로 남는다는 점, 그리고 압축을 tar 로 풀면 NFD 가 된다는 점이 확인된 전부다. 같은 tar 파이프에서 이름이 바뀐 일은 앞서 올린 cp –parents 시험 글에도 한 줄 있었는데, 여기서는 풀기만 해도 바뀐다는 데까지 넓혀 확인했다.
NFC 로 친 말이 NFD 이름을 못 찾는 도구
한글.txt 를 NFD 로 만들고 옆에 other.txt 를 뒀다. 찾는 말은 키보드로 친 것과 같은 NFC 다. LC_ALL=en_US.UTF-8 이다.
| 도구 | NFC 로 찾으면 |
|---|---|
[ -e 이름 ], [ -f 이름 ], ls 이름 |
찾는다 (종료 코드 0) |
ls | grep -F "한글.txt" |
빈 출력, 종료 코드 1 |
ls | grep -c 한글 |
0줄 (NFD 로 친 한글 은 1줄) |
find . -name '한글.txt', find . -name '한글*' |
0개 (NFD 로 친 이름은 1개) |
find . -regex '.*한글.*' |
0개 |
bash 3.2 글롭 한글*, *한글.txt |
0개 (글롭이 안 맞으면 글자 그대로 남고 ls 한글* 은 No such file or directory 와 종료 코드 1) |
bash case 패턴, [ "$f" = "한글.txt" ] |
맞지 않음 |
파이썬 '한글.txt' in os.listdir('.') |
False |
파이썬 os.path.exists('한글.txt') |
True |
파이썬 glob.glob('한글.txt') / glob.glob('한글*') |
1개 / 0개 |
zsh 5.9 글롭 한글* |
1개 (돌려준 이름의 바이트가 NFC ed959c… 였다) |
파일 내용 속 NFD 한글 문서 를 grep -c 로 NFC 검색 |
0줄 |
같은 파일이 [ -e ] 에는 있고 os.listdir 에는 없다. 파이썬 쪽은 스크립트가 흔히 하는 if name in os.listdir(...) 가 조용히 거짓이 되는 자리다. zsh 만 글롭이 NFC 로 돌려줘서 같은 패턴이 맞았는데, ls 로 읽은 이름의 바이트는 여전히 NFD 였다. 스크립트가 bash 인지 zsh 인지에 따라 같은 줄의 결과가 달라진다.
목록 두 개를 맞춰 보는 도구는 로캘에 따라 갈렸다. 파일시스템 목록(NFD)과 사람이 타자로 친 목록(NFC)에 같은 파일 한 개가 들어 있을 때, 같은 파일이면 중복 1줄이거나 차이 0줄이어야 한다.
| 도구 | en_US.UTF-8 |
C |
|---|---|---|
합쳐서 sort -u 한 줄 수 (같은 파일이면 1) |
2 | 2 |
sort | uniq -d 중복 줄 수 (같은 파일이면 1) |
1 | 0 |
comm -23 로 목록에만 있다고 나온 줄 (같은 파일이면 0) |
0 | 1 |
grep -x -F -f 목록파일 맞은 줄 (같은 파일이면 1) |
0 | 0 |
en_US.UTF-8 에서는 uniq 와 comm 이 둘을 같은 이름으로 봤고 sort -u 는 둘을 다 남겼다. C 에서는 모두 다른 이름으로 봤다. 같은 목록 대조인데 어떤 명령이냐, 어떤 로캘이냐에 따라 맞기도 틀리기도 하니, 이름을 대조하는 스크립트에서 결과가 뒤죽박죽이라면 로캘부터 의심하게 된다.
찾는 말을 이름 쪽에 맞추기
이름이 NFD 로 저장돼 있다면 찾는 말을 NFD 로 바꾸면 된다. 맥의 iconv 가 UTF-8-MAC 라는 이름으로 NFD 변환을 한다.
tonfd() { printf %s "$1" | iconv -f UTF-8 -t UTF-8-MAC; } # NFC -> NFD
tonfc() { iconv -f UTF-8-MAC -t UTF-8; } # NFD -> NFC (파이프 입력)
find . -name "$(tonfd '한글*')" # 1개
ls | grep -c -F "$(tonfd 한글)" # 1줄
같은 시험에서 bash 글롭도 패턴을 tonfd 로 바꿔 쓰면 1개가 맞았다. 두 목록을 대조할 때는 양쪽을 NFC 로 통일한 뒤 LC_ALL=C 로 비교했다.
export LC_ALL=C
comm -23 <(tonfc < 파일목록.txt | sort) <(tonfc < 손목록.txt | sort) # 같은 파일이면 0줄
이 시험에서 양쪽을 통일하고 comm -23 을 하자 목록에만 있는 줄이 0줄이었고, sort -u 는 1줄이었다. 파이썬이라면 비교하는 두 쪽에 unicodedata.normalize("NFC", 이름) 을 거는 것이 같은 일이다.
자기 이름이 어느 쪽인지 가리는 함수도 iconv 로 만들 수 있다. 변환 결과가 원래 이름과 다르면 NFD 이다.
isnfd() { [ "$(printf %s "$1" | iconv -f UTF-8-MAC -t UTF-8)" != "$1" ]; }
# NFD 이름은 종료 코드 0, NFC 이름과 ASCII 이름은 종료 코드 1
이 함수에 넣을 이름 목록은 ls, find, 파이썬 os.listdir 의 출력으로 뽑아야 한다. zsh 에서 for f in * 로 돌린 $f 는 글롭이 돌려준 NFC 이름이라, 같은 시험에서 NFD 파일인데도 [ "$f" = "한글.txt" ] 가 맞았다. 폴더의 NFD 이름만 골라내려면 이렇게 ls 나 find 출력을 읽어 돌린다. 두 방법 모두 bash 와 zsh 에서 NFD 이름을 골랐고, zsh 의 for f in * 는 아무것도 못 골랐다.
ls | while IFS= read -r f; do isnfd "$f" && echo "NFD: $f"; done
find . | while IFS= read -r f; do isnfd "$f" && echo "NFD: $f"; done # 하위 폴더와 폴더 이름까지
zip·tar·git 은 이름을 어떻게 남기는가
압축은 이름 바이트를 바꾸지 않고 그대로 담았다. NFC 이름과 NFD 이름 파일을 각각 zip -r, ditto -c -k, tar -cf 로 묶고 파이썬으로 항목 이름을 읽어 16진수로 비교했다. 세 도구 모두 NFC 는 NFC, NFD 는 NFD 로 담았다. 풀 때만 도구별로 갈렸다. unzip 은 담긴 그대로 풀었고(ditto -x -k 는 NFC 로 담긴 zip 만 풀어 NFC 가 그대로 나왔고), tar -xf 는 NFC 로 담긴 이름도 NFD 로 풀었다.
zip 이 담은 항목에는 두 가지가 눈에 띄었다. zip 과 ditto 가 만든 zip 에서 한글 이름이 든 항목의 UTF-8 플래그(NFC 로 담은 zip 에서도 같았다)(일반 목적 비트 11)가 꺼져 있었고, 유니코드 경로를 따로 담는 추가 필드(0x7075)도 없었다(추가 필드는 NFD 이름을 넣은 zip 두 개에서만 찍어 봤다). zip 3.0 의 항목은 추가 필드가 0x5455·0x7875, ditto 의 항목은 0x5855 뿐이었다. 이 플래그를 보고 이름 글자 집합을 판단하는 풀기 도구가 있는지, 그 도구가 윈도우에서 자모가 갈라져 보이는 일과 관계가 있는지는 이 맥에서 시험하지 못했다. 또 파이프로 받은 unzip -Z1 과 unzip -l 출력은 NFD 이름의 바이트 일부(84·85·86·80·92)를 ?(3f)로 바꿔 찍었다. 화면에 찍힌 목록으로 이름 바이트를 판단하면 안 된다.
같은 이름이 NFC 와 NFD 로 둘 다 든 아카이브를 가정하고, 파이썬 tarfile 로 그런 시험용 tar 를 합성해 풀었다. 순서를 바꿔 만든 두 아카이브 모두 종료 코드 0에 경고 없이 파일 하나만 남았고, 이름은 둘 다 NFD 였으며 내용은 아카이브의 마지막 항목 것이었다. 두 항목은 조용히 하나로 합쳐졌다.
git 은 설정 하나가 갈랐다(시험은 사용자·시스템 git 설정 파일을 끄고 돌렸다). git init 직후 .git/config 에 precomposeunicode = true 가 적혀 있었다(git config --show-scope --show-origin 이 local file:.git/config true 를 냈다). git 설명서는 이렇게 적는다. “When core.precomposeUnicode=true, Git reverts the unicode decomposition of filenames done by Mac OS.” 그리고 거짓이면 “file names are handled fully transparent by Git” 다. NFD 로 만든 한글.txt 를 git add 하고 커밋했을 때 이렇게 나왔다.
precomposeunicode=true |
=false |
|
|---|---|---|
git ls-files -z 에 기록된 바이트 |
NFC (ed959c…) |
NFD (e18492…) |
git ls-files -- NFC이름 |
1줄 | 0줄 |
git log --oneline -- NFC이름 |
1줄 | 0줄 |
git clone 한 작업 폴더의 파일 이름(clone 도 해당 열의 설정으로 받았다) |
NFC | NFD |
=false 로 커밋한 저장소를 false 설정으로 읽으며 NFC 로 치고 git log -- 한글.txt 하면 아무것도 출력하지 않는다. 한 저장소를 두 설정으로 번갈아 읽으면 git status 가 같은 파일을 추적 안 되는 파일(??)로 보였다. true 로 커밋한 저장소를 false 로 읽으면 NFD 이름이, false 로 커밋한 저장소를 true 로 읽으면 NFC 이름이 그랬다. 또 git ls-files | grep 한글 은 이름이 같아도 0줄이 나올 수 있다(true 저장소에서 기본 설정은 0줄, core.quotePath=false 와 -z 는 1줄이었다). git 이 기본으로 한글 바이트를 "\355\225\234…" 처럼 8진수로 감싸 찍기 때문이고(core.quotePath, 설명서: “The default value is true”), -z 를 쓰거나 git -c core.quotePath=false 로 읽으면 맞았다.
폴더째 NFC 로 바꾸기
NFD 이름을 NFC 로 바꾸는 방법은 바로 되는 것과 조용히 안 되는 것이 있었다. 각각 새 폴더에서 NFD 파일 하나를 한글.txt 로 바꿔 봤다.
| 방법 | 결과 |
|---|---|
mv NFD이름 NFC이름 |
종료 코드 0, 이름이 NFC 로 바뀜 |
파이썬 os.rename(NFD이름, NFC이름) |
종료 코드 0, 이름이 NFC 로 바뀜 |
임시 이름을 거쳐 두 번 os.rename |
같은 결과 |
mv -n NFD이름 NFC이름 |
종료 코드 0인데 아무것도 안 바뀜 (NFD 그대로) |
cp -p NFD이름 NFC이름 |
are identical (not copied) 와 종료 코드 1 |
mv -n 은 “이미 있는 이름이면 덮어쓰지 않는다” 는 옵션이라 NFC 이름이 이미 있는 것으로 보고 조용히 넘어간 것으로 보인다(짐작이다). 안전하게 하려고 -n 을 붙였다가 이름이 하나도 안 바뀌는 스크립트가 종료 코드 0으로 끝날 수 있다.
폴더 안 전부를 바꾸는 파이썬 한 방은 다음이다. nfc-fix.py 로 저장해 python3 nfc-fix.py 폴더경로 로 돌리고, 같은 걸 두 번 돌려도 해가 없다.
import os, sys, unicodedata as u
changed = 0
for root, dirs, files in os.walk(sys.argv[1], topdown=False): # 깊은 것부터
for n in files + dirs:
c = u.normalize("NFC", n)
if c != n:
os.rename(os.path.join(root, n), os.path.join(root, c)); changed += 1
print("바꾼 항목 수:", changed)
폴더 두 개(폴더 와 그 아래 하위)와 파일 셋(최상위, 폴더 아래, 하위 아래에 하나씩)으로 시험했더니 처음에 5개를 바꾸고 남은 NFD 항목이 0개였으며, 한 번 더 돌리면 0개를 바꿨다. 위에서 아래로(topdown=True) 돌린 시험은 폴더 하나와 파일 하나의 더 작은 트리였고 오류 없이 NFC 로 바뀌었고 순서를 지켜야 하는 경우는 이 시험에서 만나지 못했다. 이름만 바꾸므로 파일 내용은 그대로였다.
바꾸기 전에 tar 로 풀어서 생긴 이름이라면 풀고 난 다음에 한 번 돌리면 된다. NFC 와 NFD 가 한 폴더에 따로 있는 일은 APFS 에서 생기지 않으므로, 이름을 바꿀 때 두 이름이 충돌하는 경우는 이 맥에서 만나지 못했다.
시험하지 못한 것
이미 어긋난 git 저장소를 바로잡는 방법과 어느 설정을 권할지, Finder·AirDrop·브라우저로 만든 파일, 윈도우와 리눅스 쪽에서 이 파일들이 어떻게 보이는지, exFAT 같은 외장 디스크와 네트워크 볼륨, 대소문자를 구분하는 APFS 볼륨, Homebrew 의 GNU find·grep·tar 와 ugrep, bash 4 이상, 한글 외의 글자(일본어 탁점 등)는 시험하지 않았다. 한글 이름도 한글.txt 와 폴더·하위·문서 몇 개뿐이다. 크리에이터매거진 포럼, SK DevOcean, velog 에서 내가 연 본문은 윈도우로 옮길 때 자모가 갈라져 보이는 증상과 변환 방법을 다루었고, 이 맥 안의 find·grep·git·압축 도구가 어떻게 다른지는 거기서 보지 못했다.
세 줄 요약
- 파일이 NFD 로 저장돼 있으면
[ -e ]·ls·파이썬 exists 는 NFC 로 쳐도 찾지만find -name,grep -F, bash 글롭,os.listdir비교는 못 찾는다. - 이 맥에서 bash 3.2 와 파이썬은 받은 이름 그대로 저장하고,
NSFileManager한 가지 호출과tar -xf풀기는 NFC 를 NFD 로 바꿔 저장했으며, git 은core.precomposeunicode가 켜지면 NFC 로 기록한다. - 이름은
mv·os.rename으로 NFC 로 바꾸고(mv -n은 조용히 안 바뀐다), 비교할 때는iconv -f UTF-8-MAC -t UTF-8이나unicodedata.normalize로 양쪽을 맞춘다.
To help a reader whose Korean-named files appear on screen but are not found by find, grep, shell globs, Python, or git, or whose names drift in archives and repositories, tell whether a file is NFC or NFD, learn which tool misses which form, and make the names match.
On this Mac (26.6.2, APFS), a Korean-named file stored in NFD is found when typed in NFC by [ -e ], ls, and Python exists, but not by find -name, grep -F, bash globs, or Python list comparison. Files made by bash 3.2 `touch` and similar commands and by Python keep the name as given, while one `NSFileManager` call and bsdtar extraction stored NFD. git records NFC when core.precomposeunicode is on. Rename to NFC with mv or os.rename, and when comparing, normalize both sides to the same form.
In the problem folder, run the `isnfd` loop from this article and check whether any `NFD:` line is printed. If so, convert it to NFC with the short Python script in this article and search again. If the trouble is an archive or a repository, compare the name bytes first using the commands in the matching section.
Contents
One-line summary: On a Mac, a Korean filename stored in NFD is found by existence checks even when typed in NFC but not by find -name, grep -F, or bash globs, so rename the file to NFC or convert the search term to NFD.
ls shows 한글.txt plainly, yet find . -name '한글*' prints zero matches and ls | grep -F 한글.txt prints nothing with exit code 1. The script says the file is missing while your eyes say it is there. This article measures, with a single file, how each tool behaves when that file is stored in the NFD form, which methods create such files, how archives and git keep the name, and how to make the names match, all shown as hexadecimal bytes.
The environment was macOS 26.6.2 arm64 on APFS (the default volume, which ignores case), /bin/bash 3.2.57, zsh 5.9, Python 3.9.6, the BSD grep and find in /usr/bin, bsdtar 3.5.3, the default Mac rsync (openrsync, protocol version 29), Info-ZIP zip 3.0, and Apple Git 2.54.0, on 2026-11-05. I ran five test scripts twice each, and the second output matched the first character for character. The only Korean names I used were 한글.txt and a few folders.
A few terms. NFC (precomposed) writes the single syllable 한 as one block, while NFD (decomposed) writes it as separate consonant and vowel pieces. On screen they look identical, but the stored bytes differ. Printed in hexadecimal, 한글.txt is ed959c eab880 2e747874 (10 bytes) in NFC and e18492 e185a1 e186ab e18480 e185b3 e186af 2e747874 (22 bytes) in NFD. Normalization means converting between the two. A glob is a pattern such as * or ? that names a group of files.
You can type the name two ways, but there is one file
Apple’s FAQ says this about APFS: it “preserves both case and normalization of the filename on disk in all variants”, and APFS in macOS High Sierra is “normalization-insensitive”, so “a filename can be found with any of its normalization variants”. In other words, you cannot create two names in one folder that differ only in normalization, and either spelling finds the file, while storage keeps the name exactly as it was created.
That is what happened on this Mac. I ran touch with an NFD name and then checked the file by typing the NFC name.
| Attempt | Result |
|---|---|
[ -e "NFC name" ] |
exit code 0 (exists) |
Python open(NFC name, "x") |
FileExistsError: [Errno 17] File exists |
touch "NFC name" |
exit code 0, the folder still has 1 entry, stored bytes stay NFD |
rm "NFC name" |
exit code 0, 0 entries left |
This FAQ is an archived document written for High Sierra, and I confirmed that it holds on macOS 26 only through the tests on this Mac. The FAQ goes on to say that “any additional work to ensure correct normalization behavior” is unnecessary. That holds when you open the file. The trouble starts in tools that compare names as text without opening them.
Which methods create NFD names
Three public articles describe in different words that a Mac stores Korean filenames in NFD (stored in precomposed form, written as NFD, uses the NFD method). I gave each method an NFC name, created the file, and checked the stored bytes.
| Method used to create | Stored form of the name given in NFC |
|---|---|
bash touch, mkdir, > redirect, cp |
NFC unchanged |
Python open(name, "w") |
NFC unchanged |
NSFileManager createFileAtPath called through osascript -l JavaScript |
NFD (39 bytes, the tail of the name reads e18492 e185a1 …) |
tar -xf nfc.tar (names inside the archive are NFC) |
NFD |
tar -cf - src | tar -xf - |
NFD |
cp -R, ditto folder copy, rsync -a, unzip, ditto -x -k |
NFC source stays NFC (unzip also extracted a name stored as NFD unchanged as NFD) |
Names given in NFD were stored as NFD by both touch and Python. In short, names given in NFC were not altered by this Mac’s touch, mkdir, and cp, the bash > redirect, or Python (names given in NFD were tested only with touch and Python), while one Cocoa file API and this Mac’s bsdtar extraction stored NFC names as NFD. I tested a single NSFileManager call. I did not test renaming or creating files in Finder, AirDrop, browser downloads, files received through messengers, or the save dialogs of apps, so this article cannot say which form they store. What is confirmed is that files made by bash 3.2 commands and Python keep NFC, and that extracting with tar yields NFD. The same tar pipe changing names was mentioned in one line in the earlier cp –parents test article; here I extended it to show that extraction alone changes them.
Tools that miss an NFD name when searching in NFC
I created 한글.txt in NFD and put other.txt next to it. The search term is NFC, the same as typed on a keyboard, with LC_ALL=en_US.UTF-8.
| Tool | Searching in NFC |
|---|---|
[ -e name ], [ -f name ], ls name |
finds it (exit code 0) |
ls | grep -F "한글.txt" |
empty output, exit code 1 |
ls | grep -c 한글 |
0 lines (typed in NFD, 1 line) |
find . -name '한글.txt', find . -name '한글*' |
0 matches (typed in NFD, 1 match) |
find . -regex '.*한글.*' |
0 matches |
bash 3.2 glob 한글*, *한글.txt |
0 matches (an unmatched glob stays as literal text, and ls 한글* prints No such file or directory with exit code 1) |
bash case pattern, [ "$f" = "한글.txt" ] |
no match |
Python '한글.txt' in os.listdir('.') |
False |
Python os.path.exists('한글.txt') |
True |
Python glob.glob('한글.txt') / glob.glob('한글*') |
1 match / 0 matches |
zsh 5.9 glob 한글* |
1 match (the bytes of the returned name were NFC, ed959c…) |
grep -c in NFC for an NFD 한글 문서 inside the file contents |
0 lines |
The same file is present for [ -e ] and absent from os.listdir. On the Python side, the common if name in os.listdir(...) quietly turns false. Only zsh returns the glob result in NFC, so the same pattern matched there, yet the bytes of the name read with ls were still NFD. The result of the same line changes depending on whether the script runs under bash or zsh.
Tools that compare two lists differed by locale. I put one file into both a filesystem list (NFD) and a hand-typed list (NFC); for the same file, the duplicate count should be 1 or the difference should be 0.
| Tool | en_US.UTF-8 |
C |
|---|---|---|
lines after sort -u on both lists combined (1 if the same file) |
2 | 2 |
duplicate lines from sort | uniq -d (1 if the same file) |
1 | 0 |
lines comm -23 reports only in the first list (0 if the same file) |
0 | 1 |
matching lines from grep -x -F -f listfile (1 if the same file) |
0 | 0 |
Under en_US.UTF-8, uniq and comm treated the two as the same name while sort -u kept both. Under C, all of them treated the two as different. For the same list comparison, the answer is right or wrong depending on the command and the locale, so if a script that compares names gives confusing results, the locale is the first suspect.
Matching the search term to the stored name
If the name is stored in NFD, convert the search term to NFD. The Mac’s iconv performs the NFD conversion under the name UTF-8-MAC.
tonfd() { printf %s "$1" | iconv -f UTF-8 -t UTF-8-MAC; } # NFC -> NFD
tonfc() { iconv -f UTF-8-MAC -t UTF-8; } # NFD -> NFC (piped input)
find . -name "$(tonfd '한글*')" # 1 match
ls | grep -c -F "$(tonfd 한글)" # 1 line
In the same test, a bash glob also matched 1 file once the pattern went through tonfd. To compare two lists, I normalized both sides to NFC and compared under LC_ALL=C.
export LC_ALL=C
comm -23 <(tonfc < filelist.txt | sort) <(tonfc < handlist.txt | sort) # 0 lines if the same file
After normalizing both sides in this test, comm -23 reported 0 lines only in the first list and sort -u left 1 line. In Python, applying unicodedata.normalize("NFC", name) to both sides does the same job.
You can also build a function that tells which form a name uses, again with iconv. If the converted result differs from the original name, it is NFD.
isnfd() { [ "$(printf %s "$1" | iconv -f UTF-8-MAC -t UTF-8)" != "$1" ]; }
# exit code 0 for an NFD name, exit code 1 for an NFC name and for an ASCII name
Get the list of names to feed this function from the output of ls, find, or Python os.listdir. In zsh, the $f of a for f in * loop is the NFC name returned by the glob, so in the same test [ "$f" = "한글.txt" ] matched even though the file was NFD. To pick out only the NFD names in a folder, loop over the output of ls or find like this. Both methods picked out the NFD names in bash and zsh, while zsh for f in * picked out nothing.
ls | while IFS= read -r f; do isnfd "$f" && echo "NFD: $f"; done
find . | while IFS= read -r f; do isnfd "$f" && echo "NFD: $f"; done # including subfolders and folder names
How zip, tar, and git keep the name
Archives carried the name bytes as they were. I packed one file with an NFC name and one with an NFD name using zip -r, ditto -c -k, and tar -cf, then read the entry names with Python and compared them in hexadecimal. All three tools stored NFC as NFC and NFD as NFD. They differed only when extracting. unzip extracted exactly what was stored (ditto -x -k was run only on a zip that stored NFC, and gave NFC), while tar -xf extracted even a name stored in NFC as NFD.
Two things stood out in the zip entries. On the entries holding Korean names in the zips made by zip and ditto, the UTF-8 flag (general purpose bit 11) was off, and there was no extra field that carries a Unicode path (0x7075) (I printed the extra fields only for the two zips that held NFD names). The entries from zip 3.0 had only the extra fields 0x5455 and 0x7875, and the entries from ditto had only 0x5855. I could not test on this Mac whether any extraction tool judges the character set from that flag, or whether that has anything to do with Korean jamo looking split on Windows. Also, unzip -Z1 and unzip -l output received through a pipe printed some bytes of an NFD name (84, 85, 86, 80, 92) replaced with ? (3f). Do not judge name bytes from a printed listing.
I assumed an archive that holds the same name in both NFC and NFD, and synthesized such a test tar with Python tarfile and extracted it. For both archives I built in opposite orders, extraction ended with exit code 0, no warning, and only one file, whose name was NFD and whose content came from the last entry in the archive. The two entries were quietly merged into one.
For git, one setting made the difference (the test ran with the user and system git config files turned off). Right after git init, .git/config contained precomposeunicode = true (git config --show-scope --show-origin printed local file:.git/config true). The git manual says: “When core.precomposeUnicode=true, Git reverts the unicode decomposition of filenames done by Mac OS.” and when false, “file names are handled fully transparent by Git”. Here is what happened when I ran git add and committed the 한글.txt created in NFD.
precomposeunicode=true |
=false |
|
|---|---|---|
bytes recorded by git ls-files -z |
NFC (ed959c…) |
NFD (e18492…) |
git ls-files -- NFC name |
1 line | 0 lines |
git log --oneline -- NFC name |
1 line | 0 lines |
file name in a git clone working folder (the clone also used that column’s setting) |
NFC | NFD |
If you read a repository committed with =false under the false setting, type the NFC name, and run git log -- 한글.txt, it prints nothing. Reading one repository under the two settings in turn made git status show the same file as untracked (??): an NFD name when a true repository was read with false, and an NFC name when a false repository was read with true. Also, git ls-files | grep 한글 can print 0 lines even when the names match (in the true repository the default setting gave 0 lines, while core.quotePath=false and -z gave 1). By default git wraps Korean bytes in octal escapes such as "\355\225\234…" (core.quotePath; the manual says “The default value is true”), and reading with -z or git -c core.quotePath=false matched.
Converting a whole folder to NFC
Some ways of renaming an NFD name to NFC work right away, and some silently do nothing. In each case I renamed an NFD file to 한글.txt in a fresh folder.
| Method | Result |
|---|---|
mv NFD-name NFC-name |
exit code 0, the name became NFC |
Python os.rename(NFD-name, NFC-name) |
exit code 0, the name became NFC |
two os.rename calls through a temporary name |
same result |
mv -n NFD-name NFC-name |
exit code 0 but nothing changed (stays NFD) |
cp -p NFD-name NFC-name |
are identical (not copied) and exit code 1 |
mv -n means “do not overwrite an existing name”, and it seems to have treated the NFC name as already existing and moved on quietly (this is a guess). A script that added -n for safety can finish with exit code 0 without renaming a single file.
This Python snippet converts everything in a folder. Save it as nfc-fix.py, run it as python3 nfc-fix.py folder-path, and running it twice does no harm.
import os, sys, unicodedata as u
changed = 0
for root, dirs, files in os.walk(sys.argv[1], topdown=False): # deepest first
for n in files + dirs:
c = u.normalize("NFC", n)
if c != n:
os.rename(os.path.join(root, n), os.path.join(root, c)); changed += 1
print("items renamed:", changed)
I tested with two folders (폴더 and 하위 under it) and three files (one at the top, one under 폴더, one under 하위). The first run renamed 5 items and left 0 NFD items, and a second run renamed 0. Running top-down (topdown=True), on a smaller tree of one folder and one file, also ended with NFC and no error, and I met no case in this test where the order mattered. Because only names change, the file contents stayed the same.
If the names came from extracting with tar, run this once after extracting. NFC and NFD names cannot sit side by side in one folder on APFS, so I did not meet a case on this Mac where two names collide during renaming.
What I did not test
I did not test how to repair an already diverged git repository or which setting to recommend, nor files made by Finder, AirDrop, or browsers, how these files look on Windows and Linux, external disks such as exFAT and network volumes, case-sensitive APFS volumes, Homebrew’s GNU find, grep, and tar or ugrep, bash 4 and later, or characters other than Korean (such as Japanese dakuten). The Korean names are only 한글.txt and a few of 폴더, 하위, and 문서. The body I opened in the Creator Magazine forum, SK DevOcean, and velog dealt with the symptom of Korean jamo appearing split when files move to Windows and with how to convert them, and I did not see there how find, grep, git, and archive tools on the Mac differ.
Three-line summary
- When a file is stored in NFD,
[ -e ],ls, and Python exists find it even when typed in NFC, butfind -name,grep -F, bash globs, andos.listdircomparison do not. - On this Mac, bash 3.2 and Python store the name as given, one
NSFileManagercall andtar -xfextraction stored NFC as NFD, and git records NFC whencore.precomposeunicodeis on. - Rename to NFC with
mvoros.rename(mv -nquietly changes nothing), and when comparing, normalize both sides withiconv -f UTF-8-MAC -t UTF-8orunicodedata.normalize.