土曜日, 6月 14, 2025
- Advertisment -
ホームニューステックニュース誤解の多い「NFD問題とUTF-8-MAC問題」を解説する - macOSの濁点を含むファイル名の扱い #Unicode - Qiita

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



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

macOS では濁点や半濁点が含まれるファイル名でたびたび問題が発生しています。この問題は NFD 問題と言われたり UTF-8-MAC 問題と言われることがありますが、必要な情報が正確に書かれているところは少なく、正しく解説してある所でも情報が古くなっており、読むと逆に混乱してしまう場合があります。macOS 標準アプリや誰かが作ったアプリであればバグが修正されるまで待つだけですが、自分が作ったアプリやシェルスクリプトなどではこれがどういう問題なのかを理解しなければバグが修正できません。この記事ではそれらを整理し直して、(可能な限り正確に)解説したいと思います。検証は macOS 15.3(補助的に 15.5)で行っています。

要約

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

  • 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フレームワーク)は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 では「Finder はファイル名を NFD 正規化して作成」したり後述の「ランタイム正規化で NFD 正規化が利用されている」ので、内容が間違っているのか正しいのか、ややこしい状況になっています。

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

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

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

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

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

NFD 正規化について

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

❌ このような考え方ではない(双方向の変換ではない)
NFC  NFD 
NFKC  NFKD

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

文字列 → NFD → NFC は元の文字列に戻る保証がない

$ echo 羽 | uconv -x NFD | uconv -x NFC
羽     👈️ 「羽」に戻せない

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

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

日本語では NFD は濁点や半濁点を分解する変換として知られていますが、英語では「Å」は「A」と「 ゚」に分解するような変換で英語圏にも関係のある話です。今どきは英語圏だからといって ASCII 文字しか使わないというわけではありません。正規化のルールに「㍉ → ミリ」「羽 → 羽」の逆変換はなく、「羽 → 羽」は全ての正規化で行われます。「羽」の仲間の文字は互換漢字(CJK互換漢字CJK互換漢字補助)と呼ばれています。

「ガ → カ ゛」のような、見た目が大きく変わらない分解を正規分解、「㍉ → ミリ」のような見た目が変わる分解を互換分解と呼びます(厳密な定義は調べてください)。NFD/NFC は見た目が変わらない正規分解・正規合成を行うもので、 NFKD/NFKC は見た目が変わる互換分解・互換合成を行います。「羽 → 羽」は見た目が変わっていますが、互換漢字は普通は使わない文字という扱いらしく NFC/NFD でも似たような文字に正規化されるルールになっています。

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

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

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

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

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

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 によると、厳密には U+2000-U+2FFF(記号など)、U+F900-U+FAFF(CJK互換漢字付近)、U+2F800-U+2FAFF(CJK互換漢字補助付近)の間は正規化しないことになっています。

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

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)")
}

「ガ」は分解されるが「羽」は変換されないので 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 由来のプログラムでは行われません。

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移植版(未検証)を試してみると良いでしょう。

【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

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 に基づく正規化が行われていたようです(参考)。

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 年の Unicode 3.0 で、Unicode 2.1 には策定途中の近い仕様はあったもの厳密な定義ではなかったようです。

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

ランタイム正規化の導入

APFS(Apple File System)の導入は、ベータ版として 2017 年の macOS Sierra (10.12.4) から始まりました。このときに正規化に伴うファイル名の食い違いのために互換性上の問題が発生し、ファイルにアクセスできないなどの問題が発生したため、mac OS 10.12.6 で「ランタイム正規化」と呼ばれる回避策が実装されました。ランタイム正規化では(UTF-8-MAC ではなく)NFD 正規化が使われます。

APFS ではファイルシステムレベルでの正規化を行わなくなりました。Foundation フレームワークを使う macOS アプリなどではアプリケーションレベルで UTF-8-MAC 正規化が行われますが、Unix 由来のプログラムは UTF-8-MAC 正規化が行われません。これが問題になるのは、例えば macOS 専用の GUI インターフェースを備えながらも、コアのプログラムは移植性のために Unix 由来の C 言語関数を使用しているような場合です。GUI からのファイルの作成時では、入力したファイル名とは異なる UTF-8-MAC 正規化が行われたファイル名で記録されます。しかし Unix 由来のプログラムが入力したファイル名で記録されている前提でいると、ファイル名が異なるためにアクセスできません。

ランタイム正規化はこのような問題を回避するための仕組みで、ファイルが参照できない場合に、NFD 正規化を行った名前でリトライすることで、Unix 由来のプログラムでもファイルが参照できるようにします。

$ 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

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 との互換性のためにデフォルトで区別するように設定されています。

macOS での動作

$ echo test > File.txt

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

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

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

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 正規化された名前のファイルを作成します。

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 上ではこの変換は行われません。

$ 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(細かいバージョンは未調査)ではファイル名の補完機能で正規化が行われるようです。

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

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

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

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

【ファイルシステムには 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 にはすでに説明したようにバグがあるため注意してください。

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

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

ファイル名の変換プログラム(正規化は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コマンドのソート順がまともに修正されました!」を参照してください。

【照合順序がない場合(= バイナリ順)では自然な並びとならない】
$ 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 Å 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





Source link

Views: 0

RELATED ARTICLES

返事を書く

あなたのコメントを入力してください。
ここにあなたの名前を入力してください

- Advertisment -