誤解の多い「NFD問題とUTF-8-MAC問題」を解説する - macOSの濁点を含むファイル名の扱い

# はじめに

macOS では濁点や半濁点が含まれるファイル名でたびたび問題が発生しています。この問題は NFD 問題と言われたり UTF-8-MAC 問題と言われることがありますが、必要な情報が正確に書かれているところは少なく、正しく解説してある所でも情報が古く（主に HFS+ 時代の話に）なっており、読むと逆に混乱してしまう場合があります。

macOS 標準アプリや誰かが作ったアプリであればバグが修正されるまで待つだけですが、自分が作ったアプリやシェルスクリプトなどではこれがどういう問題なのかを理解しなければバグが修正できません。この記事ではそれらを整理し直して、（可能な限り正確に）解説したいと思います。検証は macOS 15.3（補助的に 15.5）で行っています。

:::note
**2025-06-14** 記事公開時に抜けていた APFS の「ネイティブ正規化」の説明を追加し、全体的に見直して関連する間違いなどを修正しています。
:::

この問題は、Mac で作成した zip ファイルを Windows で展開したときに、濁点や半濁点を含むファイルに Windows でアクセスできないなどの問題を引き起こすことがあります。

https://qiita.com/ko1nksm/items/0cce942c5a77574e68b7

## 要約

細かい説明の前に、すでにある程度知っている人のための思い込みを捨てるための要約です。念の為ですがこれらの正規化はファイル名の話であって、ファイルの中身が正規化されるわけではありません。

- UTF-8-MAC 正規化と NFD 正規化は別のもの（UTF-8-MAC は古い仕様）
- UTF-8-MAC は主に濁点・半濁点周りの正規化、NFD は他にも正規化される文字がある
- NFD/UTF-8-MAC 問題は英語圏でも発生する（例:「Å」など）
- 現在の APFS ファイルシステムは作成時に正規化を行わない（正規化するのは HFS+）
- APFS になったからと言って NFD/UTF-8-MAC 問題がなくなったわけではない
- APFS はファイルの参照時に NFD 正規化されたファイル名で参照する
- （exFATの場合の動作がよくわからない。要調査）
- テキストエディットアプリなど (Foundation API)はUTF-8-MAC正規化を行う
- Finder はフォルダ作成時や名前変更時に NFD 正規化を行う
- Finder でファイルをコピーすると UTF-8-MAC 正規化が行われる
- 多くの Unix 由来のプログラム（CLI コマンドなど）はファイル名を正規化しない
- Windows や Linuxのファイルシステムは NFC に正規化するわけではない
- （UTF-8-MAC 正規化からの逆変換で元に戻る保証はない。何が問題になるか要調査）
- 一部のシェル（zshなど）は UTF-8-MAC 正規化の逆変換を行うことがある

ややこしいですね？ 何がどの正規化を行うか、もう少し簡略化しましょう。

- ファイルシステム
  - HFS+ （UTF-8-MAC ファイルシステム正規化）※作成時の正規化
  - APFS （NFD 正規化）※参照時の正規化
- アプリケーション 
  - 主な macOS アプリ （UTF-8-MAC 正規化して作成）
  - Finder （NFD 正規化して作成、ただしコピー時は UTF-8-MAC）
  - 多くの Unix 由来のプログラム （正規化せずに作成）

ややこしいはずですね。

## NFD問題とUTF-8-MAC問題の違い

NFD 正規化と UTF-8-MAC 正規化は別のものです。困ったことにファイル名が 「UTF-8-MAC に正規化される」話を「NFD に正規化される」と説明しているページが非常に多いです。正しく説明しているページは「UTF-8-MAC は NFD ではない」と書かれているのですが、困ったことに現在の macOS では UTF-8-MAC が使われている場面もありながら、Finder はファイル名を NFD 正規化して作成したり、後述の APFS の正規化では、参照時に NFD 正規化が利用されているので、内容が間違っているのか正しいのか、ややこしい状況になっています。

以前の macOS ではデフォルトのファイルシステムとして HFS+ が使用されており、2017年の macOS 10.13 から APFS に変更されました。変更されたと言っても現在の macOS でも外部ストレージは HFS+ でフォーマットすれば使えるので HFS+ に関する話は過去の話というわけではありません。HFS+ は自動的に APFS に変換されるというような事が書かれているページがありますが、これはシステムボリュームの話であって、HFS+ でフォーマットされた外付けの SSD/HDD などが自動的に APFS に変換されるわけではありません。HFS+ ではファイルの作成時に UTF-8-MAC 正規化が行われていましが、新しい APFS では正規化を行わなくなりました。

| ファイルシステム | ファイル作成時に行うファイル名の正規化 |
| - | - |
| HFS+ （以前のデフォルト）| UTF-8-MAC で正規化を行う
| APFS （現在のデフォルト）| 正規化を行わない |

APFS では**ファイルを作成するとき**にファイルシステムがファイル名の正規化を行いません。ただしアプリケーションレベルで正規化が行われています。つまり、主に maOS 用アプリ（テキストエディットなど）や Finder からファイルやフォルダを作成したときにファイル名やフォルダ名が正規化されます。一方で CLI コマンドなど Unix 由来のプログラムの多くは正規化を行いません。

| プログラム | ファイル作成時のファイル名の正規化 | 
| - | - |
| 主な macOS 用アプリ | UTF-8-MAC 正規化を行って作成する |
| Finder | NFD 正規化を行って作成する |
| 多くの Unix 由来のプログラム | 正規化を行わずに作成する |

厳密に言えば NFD 正規化と UTF-8-MAC 正規化は別物です。とは言うものの、違いはほとんど使わないような珍しい漢字だけで、そんなものをファイル名に使うことがあるだろうかと考えれば、同じように扱っても実害はそれほどないだろうとは思います。

## NFD 正規化について

NFD とは Unicode で仕様が定義されている文字の正規化ルールの 1 つです。その他に NFC、NFD、NFKC、NFKD という正規化が定義されています。**これらの正規化は一方通行**です。つまり、ある文字列を NFD に正規化し、NFC 正規化を行っても、元の文字列に戻せる保証はありません。

```txt
❌ このような考え方ではない（双方向の変換ではない）
NFC <-> NFD 
NFKC <-> NFKD

⭕️ このような考え方（一方通行の変換）
元の文字列 -> NFC or NFD or NFKC or NFKD
```

```console:文字列 → NFD → NFC は元の文字列に戻る保証がない
$ echo 羽 | uconv -x NFD | uconv -x NFC
羽     👈️ 「羽」に戻せない
```

「NFC/NFD問題」などと書いてあるページが多く、この 2 つは対比しているように思えるかもしれませんが、Unicode には単に 4 種類の異なる正規化ルールが定義されているというだけです。

| 正規化の種類 | カ ゛→ ガ | ガ → カ ゛| ㍉ → ミリ | 羽 → 羽 |
| - | - | - | - | - |
| NFC |✅️ 行う | | | ✅️ 行う |
| NFD || ✅️ 行う | | ✅️ 行う |
| NFKC | ✅️ 行う | | ✅️ 行う | ✅️ 行う |
| NFKD | | ✅️ 行う  |✅️ 行う | ✅️ 行う |

日本語では NFD は「ガ → カ ゛」（フォントにもよるが**見た目では文字の違いがわからない**）のように濁点や半濁点を 1 文字に分解する変換として有名ですが、英語では「Å」は「A」と「 ﾟ」に分解するような変換で英語圏にも関係のある話です。今どきは英語圏だからといって ASCII 文字しか使わないというわけではありません。正規化のルールに「㍉ → ミリ」「羽 → 羽」の逆変換はなく、「羽 → 羽」は全ての正規化で行われます。「羽」の仲間の文字は互換漢字（[CJK 互換漢字](https://ja.wikipedia.org/wiki/CJK%E4%BA%92%E6%8F%9B%E6%BC%A2%E5%AD%97)や [CJK 互換漢字補助](https://ja.wikipedia.org/wiki/CJK%E4%BA%92%E6%8F%9B%E6%BC%A2%E5%AD%97%E8%A3%9C%E5%8A%A9)）と呼ばれています。

「ガ → カ ゛」のように見た目が変わらない分解を正規分解、「㍉ → ミリ」のような見た目が変わる分解を互換分解と呼びます（正確な定義は調べてください）。NFD/NFC は見た目が変わらない正規分解・正規合成を行うもので、 NFKD/NFKC は見た目が変わる互換分解・互換合成を行います。「羽 → 羽」は見た目が変わっていますが、[互換漢字は普通は使わない文字](http://nomenclator.la.coocan.jp/unicode/normalization.htm#ci)という扱いらしく NFC/NFD でも似たような文字に正規化されます。

個人的には、これらの正規化は文字列を検索する時に互換性がある文字にマッチしやすくするために使うべきと考えています。元の文字列という情報が失われてしまうため正規化した文字列はデータとして使用しづらくなってしまうからです。

補足ですが、CLI で Unicode の正規化を行いたい場合は Perl や Ruby を使用するのがおすすめです。どちらも標準インストールされており標準環境で動作するはずです。macOS Catalina 10.15 でスクリプト言語ランタイムは非推奨となっており、Perl、Ruby、Python などは[将来廃止予定という話](https://developer.apple.com/documentation/macos-release-notes/macos-catalina-10_15-release-notes)もありますが、互換性を維持するために私は無理だろうと思っています。

```console
【Perl で NFC 正規化を行う場合】
$ echo 羽 | perl -MUnicode::Normalize -CS -pe '$_=NFC($_)'
羽

【Ruby で NFC 正規化を行う場合】
$ echo 羽 | ruby -pe '$_.unicode_normalize!(:nfc)'
羽
```

ICU が提供している `uconv` コマンド（`brew install icu4c` でインストール可能）でも正規化できますが、現在のバージョンでは、バグで一部の文字（どばぱべぺガギグズゼゾダヷヾ）が正しく正規化できません。

```console:uconvコマンドはバグが有る
$ uconv --version
uconv v2.1  ICU 77.1

【「パ」は NFC、NFKC が正しく行われる】
$ printf パ | uconv -x NFD | uconv -x NFC | od -tx1
0000000 e3 83 91

【「ぱ」は NFC、NFKC が正しく行われない】
$ printf ぱ | uconv -x NFD | uconv -x NFC | od -tx1
0000000 e3 81 af e3 82 9a 0a
```

## UTF-8-MAC 正規化について

以前の macOS で使用されていた HFS+ ファイルシステムが内部で行っていた正規化が UTF-8-MAC です。ファイルシステムによる正規化なので、現在の macOS でも例えば昔から使っている外付け HDD が HFS+ でフォーマットされているような場合には使われます。UTF-8-MAC 正規化とは簡単に言えば NFD 正規化のうち濁点や半濁点などに限って正規化するもので、字形が変更するようなものは正規化しません。これは Unicode の正式な仕様には含まれない Apple 独自の正規化ルールです。

| 正規化の種類 | ガ → カ ゛ | 羽 → 羽  |
| - | - | - |
| NFD 正規化       | ✅️ 行う | ✅️ 行う |
| UTF-8-MAC 正規化 | ✅️ 行う | | 

https://developer.apple.com/library/archive/qa/qa1173/_index.html によると、UTF-8-MAC 正規化では U+2000-U+2FFF（記号など）、U+F900-U+FAFF（CJK 互換漢字付近）、U+2F800-U+2FAFF（CJK 互換漢字補助付近）の間は正規化しないことになっています。

**UTF-8-MAC 正規化は現在の macOS 環境でも Foundation フレームワークが行っており**、次のような Swift プログラムからファイルを作成すると UTF-8-MAC 正規化 が行われます。

```swift:createfile.swift
import Foundation

let fileManager = FileManager.default
let arguments = CommandLine.arguments

guard arguments.count > 1 else {
    print("ファイル名を引数で指定してください。")
    exit(1)
}

let path = arguments[1]

if !fileManager.fileExists(atPath: path) {
    let success = fileManager.createFile(atPath: path, contents: nil, attributes: nil)
    if success {
        print("ファイルを作成しました: \(path)")
    } else {
        print("ファイルの作成に失敗しました: \(path)")
    }
} else {
    print("ファイルはすでに存在しています: \(path)")
}
```

```console:「ガ」は分解されるが「羽」は変換されないので NFD ではなく UTF-8-MAC と分かる
$ swiftc createfile.swift # コンパイル

$ ./createfile ガ羽
ファイルを作成しました: ガ羽

$ ls | od -tx1c
0000000    63  72  65  61  74  65  66  69  6c  65  0a  63  72  65  61  74
           c   r   e   a   t   e   f   i   l   e  \n   c   r   e   a   t
0000020    65  66  69  6c  65  2e  73  77  69  66  74  0a  e3  82  ab  e3
           e   f   i   l   e   .   s   w   i   f   t  \n  カ  **  **    ゙
0000040    82  99  ef  a8  9e  0a
          **  **  羽  **  **  \n
0000046
```

このようなファイル作成時の UTF-8-MAC への正規化は、ファイルシステムで正規化を行う HFS+ では Unix 由来のプログラムでも行われましたが、APFS では Foundation フレームワークを経由しない Unix 由来のプログラムでは行われません。

```console:Unixコマンドで作成した場合は正規化が行われない
$ rm -f ガ羽 && touch ガ羽 # （再）作成

$ ls | od -tx1c
0000000    63  72  65  61  74  65  66  69  6c  65  0a  63  72  65  61  74
           c   r   e   a   t   e   f   i   l   e  \n   c   r   e   a   t
0000020    65  66  69  6c  65  2e  73  77  69  66  74  0a  e3  82  ac  ef
           e   f   i   l   e   .   s   w   i   f   t  \n  ガ  **  **  羽
0000040    a8  9e  0a
          **  **  \n
0000043
```

補足ですが UTF-8-MAC という名前は、macOS 版の `iconv` コマンドが持っている文字コード（エンコーディング）の名前です。ただし UTF-8-MAC は UTF-8 そのもので、単に一部の文字を別の文字へと変換（正規化）しているだけです。UTF-8-MAC の変換と逆変換は `iconv` コマンドで行えますが、一部バグが有るようなので安定した動作が欲しい場合は Ruby を使うかこちらの [GNU libiconv への UTF-8-MAC 移植版](https://fumiyas.github.io/2019/12/06/macos-libiconv-bug.utf8mac-curse.html)（未検証）を試してみると良いでしょう。

```console
【iconv コマンドを使った UTF-8-MAC 変換】
$ echo ガ羽 | iconv -t UTF-8-MAC | od -tx1c
0000000   e3  82  ab  e3  82  99  ef  a8  9e  0a
          カ  **  **    ゙   **  **  羽  **  **  \n

【UTF-8-MAC の逆変換（2番目の iconv）】
$ echo ガ羽 | iconv -t UTF-8-MAC | iconv -f UTF-8-MAC | od -tx1c
0000000   e3  82  ac  ef  a8  9e  0a
          ガ  **  **  羽  **  **  \n

【補足: Ruby を使って UTF-8 から UTF-8-MAC に正規化する方法】
$ echo ガ羽 | ruby -pe '$_.encode!("UTF-8-MAC", "UTF-8")' | od -tx1c
0000000   e3  82  ab  e3  82  99  ef  a8  9e  0a
          カ  **  **    ゙   **  **  羽  **  **  \n
0000012
```

```console:macOS 標準の iconv コマンドのバグ
$ while :; do echo あいう; done | iconv -t UTF-8-MAC > /dev/null
iconv: iconv(): Inappropriate ioctl for devic

$ echo 🍀 | iconv -t UTF-8-MAC

```

ところで、なぜ Apple は NFD とは異なる独自の UTF-8-MAC を考案したのでしょうか？ その理由は Apple が UTF-8-MAC を考案したときにはまだ Unicode 正規化は誕生していなかったからと考えられます。HFS+ が採用されたのは macOS が Unix になる前の 1998年の Mac OS 8.1 ですが、その頃から 1998 年の Unicode 2.1 に基づく正規化が行われていたようです（[参考](https://developer.apple.com/library/archive/technotes/tn/tn1150.html)）。

> Note:
Mac OS versions 8.1 through 10.2.x used decompositions based on Unicode 2.1. Mac OS X version 10.3 and later use decompositions based on Unicode 3.2. Most of the characters whose decomposition changed are not used by any Mac encoding, so they are unlikely to occur on an HFS Plus volume. The MacGreek encoding had the largest number of decomposition changes.

（間違っていなければ）Unicode に正規化 (UTR #15: Unicode Normalization Forms) が正式に追加されたのは 1999年9月の Unicode 3.0 で、1998年5月の Unicode 2.1 ではまだ策定途中のドラフト段階でした。

- https://www.unicode.org/versions/enumeratedversions.html
- https://www.unicode.org/reports/tr15/
- 1998-05-20: https://www.unicode.org/reports/tr15/pdtr15.html PROPOSED DRAFT 1.3
- 1998-08-20: https://www.unicode.org/reports/tr15/tr15-7.html 最初の DRAFT
- 1999-03-22: https://www.unicode.org/reports/tr15/tr15-14.html 最後の DRAFT
- 1999-07-23: https://www.unicode.org/reports/tr15/tr15-15.html 

つまり Apple は NFD に手を加えて UTF-8-MAC を考案したわけではなく、正規化の考えが曖昧だった時代に UTF-8-MAC（Unicode 2.1 と同一？）を考案し、その後に仕様が追加された NFD が策定されたという流れなのだと思われます。

## ネイティブ正規化とランタイム正規化

:::note 
**2025-06-14** 記事公開時に「ネイティブ正規化」と「ランタイム正規化」の 2 つを混同しており、ネイティブ正規化の説明が抜けていたため書き換えました。（この記事のコメントも参照）
:::

APFS（Apple File System）では（HFS+ とは異なり）ファイルシステムレベルでの正規化を行わなくなり、ファイルシステムには作成時に指定したファイル名がそのまま記録されるようになりました。ただしファイルの参照時には NFD 正規化を行った名前のハッシュ値で参照する仕組みとなっているため、記録されているファイル名がなんであろうと、正規化されていない名前でも正規化された名前でもファイルを参照できます。この仕組みは、ハッシュベースの「ネイティブ正規化」と呼ばれています。

```console
$ echo test > 羽.txt
$ ls | od -tx1c

$ ls | od -tx1c   👇️ 「羽.txt」で記録されている
0000000    ef  a8  9e  2e  74  78  74  0a
          羽  **  **   .   t   x   t  \n
0000010

$ cat 羽.txt      👈️ 当然「羽.txt」で参照できる
test

$ cat 羽.txt      👈️ 正規化された「羽.txt」でも参照できる
test
```

ただし注意が必要なのが、**Foundation フレームワークが行うアプリケーションレベルの UTF-8-MAC 正規化は別に行われる**ということです。作成時に指定したファイル名そのままの文字列で記録されるのは、Foundation フレームワークを使わない Unix 由来のプログラムなどです。これが問題になるのは、例えば macOS 専用の GUI インターフェースを備えながらも、コアのプログラムは移植性のために Unix 由来の C 言語関数を使用しているような場合です。GUI からのファイルの作成時では、指定したファイル名とは異なる UTF-8-MAC 正規化が行われたファイル名で記録される可能性があります。もし Unix 由来のプログラムが指定したファイル名で記録される前提でいると、記録されていると思っているファイル名と、実際に記録されているファイル名とで食い違いが発生します。

HFS+ から APFS へ移行が始まった時、初期の APFS の実装にはネイティブ正規化がありませんでした。正規化していないファイル名を記録し、そのファイル名で参照する、つまり他のファイルシステムと同じような仕様でしたが macOS では互換性上の問題が発生しました。そこでネイティブ正規化が実装されますが、その前段階としてすでに初期の APSF を導入してしまった環境のために「ランタイム正規化」と呼ばれる回避策が実装されました。ランタイム正規化ではファイルが参照できない場合に正規化を行った名前でリトライする仕組みです。ランタイム正規化は初期の APFS のための機能で、クリーンインストールの場合にはネイティブ正規化が使われます。詳細は Apple の[ドキュメント](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html)、[What’s New in APFS - #WWDC17](https://devstreaming-cdn.apple.com/videos/wwdc/2017/715gk347h3udl/715/715_whats_new_in_apple_file_system.pdf) 、および APFS 移行にまつわる紆余曲折については、この記事末尾の参考・関連リンクの eclecticlight.co の記事が参考になります。

## Windows/Linux は NFC で正規化しない

「正規化を行う」の反対は「正規化を行わない」です。macOS が NFD 正規化を行うのに対して、Windows や Linux などは正規化を行いません。NFD 正規化ではないからと、正規化しないことを NFC 正規化と説明している例を非常によく見かけますが、もし本当に NFC 正規化を行っているのであれば「羽」が「羽」に正規化されてしまうはずです。

Windows や Linux はファイル名を入力したままの文字列で保存します。一般的な日本語入力システム (IME) は、濁音を濁点が結合された 1 文字に日本語変換します。つまり**たまたま入力した文字が結合されているから結合したファイル名で記録される**というだけなんです。ちなみに濁音を 1 文字に日本語変換するのは macOS 標準の IME である「ことえり」でも同じです。

Linux の ext4 や Windows の NTFS、そして macOS の APFS は、ファイルシステム自体はファイル名は NFC でも NFD でも記録できます。NFC や NFD は Unicode の範囲で文字を正規化することであって、正規化されていても正規化されていなくても Unicode であることに変わりないからです。これらのファイルシステムは単に Unicode で記録されると言うだけです。

## macOS/Windowsはファイル名の大文字小文字を区別しない

macOS と Windows に昔から共通する仕様は、 Unix 系 OS とは異なりファイル名の大文字と小文字を区別しないことです。一応どちらも区別させることはできるのですが、デフォルトの動作では区別しません。なお、Windows の WSL では、Linux との互換性のためにデフォルトで区別するように設定されています。

```console:macOS での動作
$ echo test > File.txt

$ cat file.txt  👈️ 全部小文字でも参照できる
test

$ cat FILE.TXT  👈️ 全部大文字でも参照できる
test
```

この挙動はファイル名の正規化の話と似ていますが、大文字小文字はファイルシステムにそのまま記録されているという点で、正規化した名前で記録する場合と違いがあります。ネイティブ正規化に近い機能と言えるでしょう。

ちなみに APFS は macOS よりも先に 2017年3月の iOS 10.3 で導入されました。**iOS 版の APFS はデフォルトで大文字と小文字を区別します**。その後、macOS 用に HFS+ と互換性の高い大文字と小文字を区別しない APFS が開発され、2017年3月の macOS 10.12.4 でベータ機能として導入されます。しかしそれでも互換性問題が発生したため、ランタイム正規化が macOS 10.12.6、iOS 10.3.3 / 11に実装され、さらにネイティブ正規化が macOS 10.13 と iOS 11 に実装されたという流れのようです。

## NFD/UTF-8-MAC正規化はファイル名の取得で問題になる

Finder が行う NFD 正規化や、その他の macOS アプリが行う UTF-8-MAC 正規化は、ファイル名を指定するときにはあまり問題にならないはずです。問題になるのはファイルシステムに記録されたファイル名を参照するときです。正規化によって、読み取るときには作成時に指定したファイル名とは、一部の文字が別の文字に変わってしまうからです。

例えば、Finder やテキストエディットなどで「ガ」と入力してファイルやフォルダを作成した時、ファイルシステムには「カ ゛」で記録されます。そして **Finder からファイルのパスをコピーしてテキストファイルに貼り付けたりすると「カ ゛」で貼り付けられます**。本来、IME などでは「ガ」に日本語変換されるというのに、ファイル名のコピペを行うといつの間にか「カ ゛」になってしまうわけですから本当に困った話です。

その他にも Finder などで作成したファイル一覧を `ls` コマンドでテキストファイルにリスト化すると、ファイル名は「カ ゛」になっています。ファイルシステムに記録されたファイル名をデータとして使用するときに問題が発生します。

## 注意すべき CLI コマンドの挙動

多くの CLI コマンドは Unix 系 OS 間での移植性のために POSIX で標準化された関数を利用しています。macOS 標準の Foundation フレームワークを利用しないため、**CLI コマンドから作成したファイルのファイル名は APFS 上であれば正規化されません**。そのため完全にターミナル上だけで作業をし、一般的な Unix コマンドだけを使っていれば正規化のことなど気にせず作業ができます。

問題が発生するのは、主に GUI アプリ（macOS 標準アプリなど）で作成した正規化されたファイル名をターミナルやシェルスクリプトで扱うときです。このような問題を扱えるように、一部のプログラムは独自で正規化を実装しているのですが、その事に気づいていないと正規化の挙動が理解できなくなってしまいます。

### zshのパス名展開の正規化

知らなければ非常に混乱する（混乱した）のが、macOS 上の zsh は、パス名展開で UTF-8-MAC の逆変換の正規化（正確に UTF-8-MAC であるかは検証していません）を行うということです。検証としてまず UTF-8-MAC 正規化された名前のファイルを作成します。

```console:UTF-8-MACの「ガ羽.txt」の作成
$ touch $'\xe3\x82\xab\xe3\x82\x99\xef\xa8\x9e.txt'

$ ls | od -tx1c
0000000    e3  82  ab  e3  82  99  ef  a8  9e  2e  74  78  74  0a
          カ  **  **    ゙  **  **  羽  **  **   .   t   x   t  \n
0000016
```

この状態で zsh のパス名展開を実行すると、次のように UTF-8-MAC の逆変換行われているのがわかります。この変換が行われるのは macOS 標準の zsh だけではなく、Homebrew でインストールした zsh も同じですが、Linux 上ではこの変換は行われません。

```console
$ echo *.txt | od -tx1c
0000000    e3  82  ac  ef  a8  9e  2e  74  78  74  0a
          ガ  **  **  羽  **  **   .   t   x   t  \n
0000013

$ /bin/zsh -c 'echo *.txt | od -tx1c'
0000000    e3  82  ac  ef  a8  9e  2e  74  78  74  0a
          ガ  **  **  羽  **  **   .   t   x   t  \n
0000013

$ $(brew --prefix)/bin/zsh -c 'echo *.txt | od -tx1c'
0000000    e3  82  ac  ef  a8  9e  2e  74  78  74  0a
          ガ  **  **  羽  **  **   .   t   x   t  \n
0000013
```

UTF-8-MAC 正規化からの逆変換は zsh の機能なので bash や ksh などでは行われません。zsh がパス名展開で正規化を行うという事実は、macOS が zsh に乗り換えた理由の一つなのかもしれません（一番の理由はライセンス問題だと思いますが）。

### zshやbashの補完機能の正規化

macOS 標準の古い bash は行わないようですが、新しい bash（細かいバージョンは未調査）ではファイル名の補完機能で正規化が行われるようです。

```console:UTF-8-MACの「アップル.txt」を作成
$ touch $'\xe3\x82\xa2\xe3\x83\x83\xe3\x83\x95\xe3\x82\x9a\xe3\x83\xab.txt'

$ ls | od -tx1c
0000000    e3  82  a2  e3  83  83  e3  83  95  e3  82  9a  e3  83  ab  2e
          ア  **  **  ッ  **  **  フ  **  **    ゚  **  **  ル  **  **   .
0000020    74  78  74  0a
           t   x   t  \n
0000024
```

```console:bash 3.2の補完ではファイルシステムそのままのファイル名で補完される
$ /bin/bash
The default interactive shell is now zsh.
To update your account to use zsh, please run `chsh -s /bin/zsh`.
For more details, please visit https://support.apple.com/kb/HT208050.

bash-3.2$ echo ア 【ここでTABを押して補完する】
bash-3.2$ echo アップル.txt | od -tx1c
0000000    e3  82  a2  e3  83  83  e3  83  95  e3  82  9a  e3  83  ab  2e
          ア  **  **  ッ  **  **  フ  **  **    ゚  **  **  ル  **  **   .
0000020    74  78  74  0a
           t   x   t  \n
0000024
```

```console:zshやbash 5.2の補完ではUTF-8-MACの逆変換で正規化される
【zshの場合】
$ echo ア 【ここでTABを押して補完する】
$ echo アップル.txt | od -tx1c
0000000    e3  82  a2  e3  83  83  e3  83  97  e3  83  ab  2e  74  78  74
          ア  **  **  ッ  **  **  プ  **  **  ル  **  **   .   t   x   t
0000020    0a
          \n
0000021

【新しいbashの場合】
$ bash
bash-5.2$ echo ア 【ここでTABを押して補完する】
bash-5.2$ echo アップル.txt | od -tx1c
0000000    e3  82  a2  e3  83  83  e3  83  97  e3  83  ab  2e  74  78  74
          ア  **  **  ッ  **  **  プ  **  **  ル  **  **   .   t   x   t
0000020    0a
          \n
0000021
```

APFS の正規化の機能により、ファイルには UTF-8-MAC でもそうでなくてもどちらも参照できます。つまり、**ファイルシステムに UTF-8-MAC で記録されている場合に、補完機能で UTF-8-MAC の逆変換に正規化されたファイル名で参照すると、そのファイル名で記録されているかのような出力が行われる**ということです。

```console
【ファイルシステムには NFD で記録されいている】
$ ls | od -tx1c
0000000    e3  82  a2  e3  83  83  e3  83  95  e3  82  9a  e3  83  ab  2e
          ア  **  **  ッ  **  **  フ  **  **    ゚  **  **  ル  **  **   .
0000020    74  78  74  0a
           t   x   t  \n
0000024

【引数で指定したファイル名で記録されているかのように出力される】
$ ls アップル.txt | od -tx1c
0000000    e3  82  a2  e3  83  83  e3  83  97  e3  83  ab  2e  74  78  74
          ア  **  **  ッ  **  **  プ  **  **  ル  **  **   .   t   x   t
0000020    0a
          \n
0000021
```

### gitが行う正規化

git も zsh と同じように UTF-8-MAC の逆変換を行います（正確に UTF-8-MAC であるかは検証していません）。つまり Finder やテキストエディットなどからファイルを作成し、ファイルシステムに記録された濁音が 2 文字で表現されていたとしても、リポジトリには 1 文字に変換されて記録されます。

HFS+ の時代ではファイル名は必ず UTF-8-MAC で正規化されていたわけですから、macOS を含む様々な Unix 系 OS や Windows との間でファイル共有するには、結合された一般的な文字でなければ混乱が生じます。git は主にソースコードを管理するので、日本語ファイル名を保存するようなことは少ないと思いますが、それでもドキュメントなどではありえます。そのような場合に問題が起きにくくなるようになっています。

## 混沌とする「NFD/UTF-8-MAC問題」にどう対応するか？

おそらく macOS 標準アプリ、GUI アプリ、Foundation フレームワークを使うアプリ、だけを利用している分には大きな問題は発生しないのでしょう。またターミナルで CLI コマンドを使うなど、Unix 由来プログラムだけで作業している場合にも問題は発生しにくいはずです。**問題の多くは macOS の世界と Unix の世界を行き来するときに発生**します（APFS を使う場合、HFS+を使う場合は Unix の世界だけでも問題が発生する）。

macOS の世界ではファイル名は NFD（Finder の場合）または UTF-8-MAC で正規化されます。しかしターミナルからのコマンドライン引数は正規化されない文字列で指定するため、さまざまな状況でファイル名がマッチしません。`iconv` コマンドの使用は回避方法の一つですが、macOS 標準の `iconv` にはすでに説明したようにバグがあるため注意してください。

```console
【UTF-8-MAC正規化された「アップル.txt」を作成】
$ touch $'\xe3\x82\xa2\xe3\x83\x83\xe3\x83\x95\xe3\x82\x9a\xe3\x83\xab.txt'

【findコマンドで検索できない】
$ find . -name "アップル.*"

【lsコマンドの出力にマッチしない】
$ ls | grep アップル

【参考: lsコマンドの出力をUTF-8-MACの逆変換で正規化するとマッチする】
$ ls | iconv -f UTF-8-MAC | grep アップル
アップル.txt
```

Unix の世界では正規化されていないファイル名と正規化されたファイル名は別の名前です。したがって一律で正規化してしまうのは、厳密に言えばあまり良くありません。zsh のパス名展開を使えばファイル名は UTF-8-MAC の逆変換で正規化されるので、この機能を利用するのも一つの手かもしれませんが、移植性のあるファイル名の取り扱いは困難です。

tar アーカイブや zip アーカイブへのファイルの追加でも注意が必要です。APFS の正規化のおかげで、正規化されない文字でも正規化された文字でもどちらで指定してもアーカイブにファイルを追加できます。しかしアーカイブ内のファイル名は、コマンドライン引数で指定したファイル名です。ファイル名を zsh や新しい bash の補完機能を使って指定した場合は、UTF-8-MAC の逆変換に正規化されるかもしれませんが、ディレクトリ名を指定して複数のファイルをアーカイブへ追加した場合は UTF-8-MAC のまま格納されるでしょう。そしてそれを Linux で展開すると通常は使わない UTF-8-MAC でファイルが作成されてしまいます。

コマンド側が UTF-8-MAC への対応を追加するのが一番良いと思いますが、Unix コマンドはいくつもあるわけで、それらが個々に対応するのは難しいでしょう。現状で対応するのであれば、**事前にファイル名を UTF-8-MAC の逆変換に正規化する**のが一番シンプルではないかと思われます。そのために `convmv` コマンドなどが使えるかもしれません（展開したアーカイブのファイルの文字化け修正にも使えるかも）。もちろん HFS+ 上では使えなかったりファイル名を変更する権限がない場合には使えないなど完璧な方法ではありません。

```console:ファイル名の変換プログラム（正規化はNFC、NFDであることに注意）
【convmvのインストール】
$ brew install convmv

【カレントディレクトリ以下の全てのファイル名をNFCに変換】
$ convmv -r -f utf8 -t utf8 --nfc --notest .

【カレントディレクトリ以下の全てのファイル名をNFDに変換】
$ convmv -r -f utf8 -t utf8 --nfd --notest .
```

## さいごに

なぜ macOS はファイル名を NFD（または UTF-8-MAC）に正規化したのでしょうか？ 一番の理由は「ガ」と「カ ゛」のように見た目で区別できない 2 つのファイルが作れないようにするためでしょう。しかしそれならば NFC（または UTF-8-MAC の逆変換）に正規化しても良かったはずです。NFC ではなく NFD を選んだ理由として照合順序データがなくても、ある程度自然な形でデータが並ぶようにしたかったからではないかと考えています。照合順序データがない場合、コードポイント（またはバイナリ）順でソートするしかないので、例えば「A」と「Å」は遠く離れた位置に並んでしまいます。しかし NFD 正規化を行っておけば「Å」は「A」と「 ﾟ」に分解されるため「A」の近くに並びます。同じ話がコードポイントで「ウ」ではなく「ン」の次にある「ヴ」にも当てはまります。照合順序については「[macOS 15.4でsortコマンドのソート順がまともに修正されました！](https://qiita.com/ko1nksm/items/ce20e643bd83a72850b2)」を参照してください。

```console
【照合順序がない場合（= バイナリ順）では自然な並びとならない】
$ printf "%s\n" A Å B C D E | LC_ALL=C sort | xargs
A B C D E Å

【NFD（またはUTF-8-MAC）に正規化すれば自然な並びになる】
$ printf "%s\n" A Å B C D E | uconv -x NFD | LC_ALL=C sort | xargs
A Å B C D E

【NFC（またはUTF-8-MACの逆変換）に正規化した場合は自然な並びとならない】
$ printf "%s\n" A Å B C D E | uconv -x NFC | LC_ALL=C sort | xargs
A B C D E Å

【補足: 照合順序が存在する macOS 15.5 での実行結果】
$ printf "%s\n" A Å B C D E | LC_ALL=en_US.UTF-8 sort | xargs
A Å B C D E
```

Unicode に照合順序の考え方が追加されたのも（Unicode 正規化が追加されたのと同じ）1999 年の Unicode 3.0 です。したがって HFS+ が誕生した 1998 年の時点で照合順序はなかったため、文字の並び順を自然なものとするために HFS+ が UTF-8-MAC 正規化を採用したのではないかと考えています。ファイルシステムで正規化をすべきではないという意見はそのとおりだと思いますが、おそらく当時は他に良い選択肢がなかったのでしょう。後に Finder や APFS の正規化が UTF-8-MAC ではなく NFD を採用したのはおそらく、正規化の仕様が完成していた Unicode 標準に合わせるためでしょう。しかしファイル名の正規化に関しては字形が変わる文字がある NFD よりも UTF-8-MAC の方が適している気がします。

他の OS が正規化なしでうまくやっている（ように思える）ことを踏まえるとファイル名は正規化せずに入力したままの文字列で記憶し、正規化に関係なく参照できるようにするのが良いように思えます。APFS 自体はすでにそうなっているので、次にやるべきは Finder の NFD 正規化や Foundation フレームワークの UTF-8-MAC 正規化の廃止でしょうか。しかしそれを行うためには、先に Unix の世界に照合順序を導入しなければならないはずです。macOS 15.4 でそれが実現されたので、将来の macOS では NFD 正規化や UTF-8-MAC 正規化が廃止されるかもしれません。それが実現するまでは Unix 由来のプログラムからの扱いが面倒なため、NFD/UTF-8-MAC問題はこれからもたびたび発生し続けるような気がします。

## 参考・関連リンク

developer.apple.com

- [Apple File System Guide - Frequently Asked Questions
](https://developer.apple.com/library/archive/documentation/FileManagement/Conceptual/APFS_Guide/FAQ/FAQ.html)
- [Technical Note TN1150 HFS Plus Volume Format](https://developer.apple.com/library/archive/technotes/tn/tn1150.html)
- [Text Encodings in VFS](https://developer.apple.com/library/archive/qa/qa1173/_index.html)

eclecticlight.co: macOS と iOS に関するファイル名の正規化に詳しい

- 2017-01-27: [Apple File System: what it means, what it brings](https://eclecticlight.co/2017/01/27/apple-file-system-what-it-means-what-it-brings/)
- 2017-04-05: [File problems in iOS 10.3 and macOS 10.13: What’s in a name?](https://eclecticlight.co/2017/04/05/file-problems-in-ios-10-3-and-macos-10-13-whats-in-a-name/)
- 2017-04-06: [Untangling file names and normalisation with Apfelstrudel](https://eclecticlight.co/2017/04/06/untangling-file-names-and-normalisation-with-apfelstrudel/)
- 2017-04-06: [APFS is currently unusable with most non-English languages](https://eclecticlight.co/2017/04/06/apfs-is-currently-unusable-with-most-non-english-languages/)
- 2017-04-07: [Normalising strings in Swift: scripting Apfelstrudel and beyond](https://eclecticlight.co/2017/04/07/normalising-strings-in-swift-scripting-apfelstrudel-and-beyond/)
- 2017-04-07: [APFS and macOS 10.13: many apps and tools will need to be revised
](https://eclecticlight.co/2017/04/07/apfs-and-macos-10-13-many-apps-and-tools-will-need-to-be-revised/)
- 2017-04-10: [How to normalise strings, and a new command tool to help](https://eclecticlight.co/2017/04/10/how-to-normalise-strings-and-a-new-command-tool-to-help/)
- 2017-04-22: [Unicode spoofing: when an аррӏе isn’t an apple at all
](https://eclecticlight.co/2017/04/22/unicode-spoofing-when-an-%d0%b0%d1%80%d1%80%d3%8f%d0%b5-isnt-an-apple-at-all/)
- 2017-04-23: [Last Week on My Mac: New Macs and bad characters](https://eclecticlight.co/2017/04/23/last-week-on-my-mac-new-macs-and-bad-characters/)
- 2017-07-05: [High Sierra and filenames: Apple is relenting](https://eclecticlight.co/2017/07/05/high-sierra-and-filenames-apple-is-relenting/)
- 2017-07-17: [APFS: iOS 11 and High Sierra will fix normalisation problems](https://eclecticlight.co/2017/07/17/apfs-ios-11-and-high-sierra-will-fix-normalisation-problems/)
- 2017-07-24: [Sierra isn’t finished yet: APFS support is undone in 10.12.6](https://eclecticlight.co/2017/07/24/sierra-isnt-finished-yet-apfs-support-is-undone-in-10-12-6/)
- 2017-09-27: [High Sierra 10.13: supported disk formats and file systems](https://eclecticlight.co/2017/09/27/high-sierra-10-13-supported-disk-formats-and-file-systems/)
- 2018-02-17: [Is APFS fully supported yet?](https://eclecticlight.co/2018/02/17/is-apfs-fully-supported-yet/)
- 2019-03-17: [Last Week on My Mac: Happy birthday APFS](https://eclecticlight.co/2019/03/17/last-week-on-my-mac-happy-birthday-apfs/)
- 2020-08-21: [Fun and confuddling with Unicode](https://eclecticlight.co/2020/08/21/fun-and-confuddling-with-unicode/)
- 2021-05-08: [Explainer: Unicode, normalization and APFS](https://eclecticlight.co/2021/05/08/explainer-unicode-normalization-and-apfs/)
- 2022-04-01: [A brief history of APFS in honour of its fifth birthday](https://eclecticlight.co/2022/04/01/a-brief-history-of-apfs-in-honour-of-its-fifth-birthday/)
- 2025-04-19: [A brief history of text on the Mac](https://eclecticlight.co/2025/04/19/a-brief-history-of-text-on-the-mac/)

mjtsai.com

- 2017-03-24: [APFS’s “Bag of Bytes” Filenames](https://mjtsai.com/blog/2017/03/24/apfss-bag-of-bytes-filenames/)
- 2017-03-31: [APFS to Add Case-Insensitive Variant for Mac](https://mjtsai.com/blog/2017/03/31/apfs-to-add-case-insensitive-variant-for-mac/)
- 2017-06-27: [APFS Native Normalization](https://mjtsai.com/blog/2017/06/27/apfs-native-normalization/)

その他

- [WWDC 2017 Notes - Kevin Chen)](https://kevinchen.co/blog/wwdc-2017-notes/)
- [macOS iconv(3) UTF-8-MAC エンコーディングの問題 - OSSTech Advent Calendar 2019](https://fumiyas.github.io/2019/12/06/macos-libiconv-bug.utf8mac-curse.html)
- [普通のUnicodeはNFCなのか](https://tama-san.com/unicode-nfc/)
- [Unicode正規化とは](http://nomenclator.la.coocan.jp/unicode/normalization.htm)
- [Problems with iconv on macOS](https://blog.r-project.org/2024/12/11/problems-with-iconv-on-macos/)