この記事について
macOS のファイルシステムは、デフォルトでは殆どのディレクトリで大文字小文字を区別しない。
そのような世界で、ワイルドカードを用いると何がマッチするか、という話。
大文字小文字を区別しないということ
例えばこんなスクリプトを実行すると
# !/bin/bash
set -eu
# ASCII の英数字
touch foo.txt FOO.txt
ls *.txt && rm *.txt
# 所謂全角文字
touch zen.txt ZEN.txt
ls *.txt && rm *.txt
# ギリシャ文字・キリル文字・ローマ数字
touch ωяⅶ.txt ΩЯⅦ.txt
ls *.txt && rm *.txt
# DZ, NJ
touch dz.txt # U+01F3 Latin Small Letter DZ
touch Dz.txt # U+01F2 Latin Capital Letter D with Small Letter z
touch DZ.txt # U+01F1 Latin Capital Letter DZ
touch nj.txt # U+01CC Latin Small Letter NJ
touch Nj.txt # U+01CB Latin Capital Letter N with Small Letter J
touch NJ.txt # U+01CA Latin Capital Letter NJ
ls *.txt && rm *.txt
# i witout dot, etc.
touch ı.txt # U+0131 Small I without dot
touch İ.txt # U+0130 Capital I with dot
touch i.txt # U+0069 Small I
touch I.txt # U+0049 Capital I
ls *.txt && rm *.txt
# 囲み文字
touch ⓐ.txt # U+24D0
touch Ⓐ.txt # U+24B6
ls *.txt && rm *.txt
こんな結果になる
foo.txt
zen.txt
ωяⅶ.txt
nj.txt dz.txt
i.txt İ.txt ı.txt
ⓐ.txt
FOO
と foo
は簡単。大文字小文字を区別しないのでどちらかしか生き残れない。
zen
、 ωяⅶ
や ⓐ
のあたりを見て分かる通り、7bit-ASCII の範囲外の文字にも大文字小文字があり、それらは区別されない。
Dz
は「Titlecase Letter」というカテゴリの文字で、大文字でも小文字でもない。
対応する小文字は dz
で、対応する大文字は DZ
になる。
この文字も大文字小文字を区別しないので、 touch dz.txt Dz.txt DZ.txt
としても、ひとつしか生き残れない。
ı
, İ
, i
, I
の件は下表のとおり。
言語 |
I の小文字 |
i の大文字 |
---|---|---|
英語 | i |
I |
トルコ語 | ı |
İ |
ı.txt
, İ.txt
, i.txt
, I.txt
を touch すると、
i.txt
, İ.txt
, ı.txt
が生き残る。
unicode.org を見ると、dot のない小文字ı
は、大文字にすると普通のI
になる。
にも関わらず、APFS 上では ı.txt
と I.txt
は別の名前だとみなされる。
様々な環境での対応
いくつかの環境で何が起こるのか試してみた。
shell script(bash) 他
以下の環境でこの結果になる:
- shell script(bash)
- C(POSIX glob)
- Go(filepath.Glob)
- Python3(glob.glob)
- PHP7(glob)
POSIX glob や bash と同じなのでこれが基本っぽいんだけど、わりと気持ち悪い動作になっている。
基本的には、
- ワイルドカードがない場合は大文字小文字を区別しない。
- ワイルドカードがある場合は大文字小文字を区別する。
となっている。Foo.txt
だと1件マッチして F*.txt
だと 0件マッチするというのはかなり意外だった。
ワイルドカードがない部分については、(気付いた範囲では)ファイルシステムと同じ意見になっている。
F*/f*/Foo
wildcard | foo.txt | fred.txt |
---|---|---|
F*.txt | ❌ | ❌ |
f*.txt | ✅ | ✅ |
Foo.txt | ✅ | ❌ |
i / I / ı / İ
wildcard | i-lat-lo.txt | I-lat-up.txt | ı-tur-lo.txt | İ-tur-up.txt |
---|---|---|---|---|
i*.txt | ✅ | ❌ | ❌ | ❌ |
I*.txt | ❌ | ✅ | ❌ | ❌ |
ı*.txt | ❌ | ❌ | ✅ | ❌ |
İ*.txt | ❌ | ❌ | ❌ | ✅ |
İ-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
I-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
ı-lat-up.txt | ❌ | ❌ | ❌ | ❌ |
DZ / Dz / dz
wildcard | DZ-uu.txt | Dz-ul.txt | dz-ll.txt |
---|---|---|---|
DZ*.txt | ✅ | ❌ | ❌ |
Dz*.txt | ❌ | ✅ | ❌ |
dz*.txt | ❌ | ❌ | ✅ |
dz-uu.txt | ✅ | ❌ | ❌ |
dz-ul.txt | ❌ | ✅ | ❌ |
Dz-uu.txt | ✅ | ❌ | ❌ |
ruby(Dir.glob)
ruby の動作は POSIX glob とはだいぶ異なる。
基本的には「大文字と小文字を区別しない」という動作で一貫しているような感じ。
Foo.txt
だと foo.txt
のみがマッチして、 F*.txt
だと foo.txt
と fred.txt
がマッチする。わかりやすい。
しかし、
Dir.glob("dz-u*.txt") #=> []
Dir.glob("dz-uu.txt") #=> ["files/DZ-uu.txt"]
と、ワイルドカードを入れるとマッチする件数が減るというパターンもある。
バグ?
F*/f*/Foo
wildcard | foo.txt | fred.txt |
---|---|---|
F*.txt | ✅ | ✅ |
f*.txt | ✅ | ✅ |
Foo.txt | ✅ | ❌ |
i / I / ı / İ
wildcard | i-lat-lo.txt | I-lat-up.txt | ı-tur-lo.txt | İ-tur-up.txt |
---|---|---|---|---|
i*.txt | ✅ | ✅ | ❌ | ❌ |
I*.txt | ✅ | ✅ | ❌ | ❌ |
ı*.txt | ❌ | ❌ | ✅ | ❌ |
İ*.txt | ❌ | ❌ | ❌ | ✅ |
İ-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
I-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
ı-lat-up.txt | ❌ | ❌ | ❌ | ❌ |
DZ / Dz / dz
wildcard | DZ-uu.txt | Dz-ul.txt | dz-ll.txt |
---|---|---|---|
DZ*.txt | ✅ | ❌ | ❌ |
Dz*.txt | ❌ | ✅ | ❌ |
dz*.txt | ❌ | ❌ | ✅ |
dz-uu.txt | ✅ | ❌ | ❌ |
dz-ul.txt | ❌ | ✅ | ❌ |
Dz-uu.txt | ✅ | ❌ | ❌ |
Java(PathMatcher)
java.nio.file
に PathMatcher
という インターフェイスがあるので、それを使ってみた。
これも POSIX glob とはだいぶ異なる。
常に大文字小文字を区別するように見える。
ファイルシステムのファイル名とは異なる動作だが、一貫性はある。
F*/f*/Foo
wildcard | foo.txt | fred.txt |
---|---|---|
F*.txt | ❌ | ❌ |
f*.txt | ✅ | ✅ |
Foo.txt | ❌ | ❌ |
i / I / ı / İ
wildcard | i-lat-lo.txt | I-lat-up.txt | ı-tur-lo.txt | İ-tur-up.txt |
---|---|---|---|---|
i*.txt | ✅ | ❌ | ❌ | ❌ |
I*.txt | ❌ | ✅ | ❌ | ❌ |
ı*.txt | ❌ | ❌ | ✅ | ❌ |
İ*.txt | ❌ | ❌ | ❌ | ✅ |
İ-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
I-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
ı-lat-up.txt | ❌ | ❌ | ❌ | ❌ |
DZ / Dz / dz
wildcard | DZ-uu.txt | Dz-ul.txt | dz-ll.txt |
---|---|---|---|
DZ*.txt | ✅ | ❌ | ❌ |
Dz*.txt | ❌ | ✅ | ❌ |
dz*.txt | ❌ | ❌ | ✅ |
dz-uu.txt | ❌ | ❌ | ❌ |
dz-ul.txt | ❌ | ❌ | ❌ |
Dz-uu.txt | ❌ | ❌ | ❌ |
C#(.NET Core / Directory.GetFiles)
ruby の動きと似ている。
ruby と異なり、 DZ*.txt
で dz-ll.txt
にちゃんと(?)マッチする。
しかし逆に、dz-uu.txt
では DZ-uu.txt
が取れない。
F*/f*/Foo
wildcard | foo.txt | fred.txt |
---|---|---|
F*.txt | ✅ | ✅ |
f*.txt | ✅ | ✅ |
Foo.txt | ✅ | ❌ |
i / I / ı / İ
wildcard | i-lat-lo.txt | I-lat-up.txt | ı-tur-lo.txt | İ-tur-up.txt |
---|---|---|---|---|
i*.txt | ✅ | ✅ | ❌ | ❌ |
I*.txt | ✅ | ✅ | ❌ | ❌ |
ı*.txt | ❌ | ❌ | ✅ | ❌ |
İ*.txt | ❌ | ❌ | ❌ | ✅ |
İ-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
I-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
ı-lat-up.txt | ❌ | ❌ | ❌ | ❌ |
DZ / Dz / dz
wildcard | DZ-uu.txt | Dz-ul.txt | dz-ll.txt |
---|---|---|---|
DZ*.txt | ✅ | ✅ | ✅ |
Dz*.txt | ✅ | ✅ | ✅ |
dz*.txt | ✅ | ✅ | ✅ |
dz-uu.txt | ❌ | ❌ | ❌ |
dz-ul.txt | ❌ | ❌ | ❌ |
Dz-uu.txt | ❌ | ❌ | ❌ |
C#(Mono / Directory.GetFiles)
意外にも、 .NET Core と Mono で動作が異なる。
大文字でも小文字でもない Dz
という文字に負けている感じがする。
F*/f*/Foo
wildcard | foo.txt | fred.txt |
---|---|---|
F*.txt | ✅ | ✅ |
f*.txt | ✅ | ✅ |
Foo.txt | ✅ | ❌ |
i / I / ı / İ
wildcard | i-lat-lo.txt | I-lat-up.txt | ı-tur-lo.txt | İ-tur-up.txt |
---|---|---|---|---|
i*.txt | ✅ | ✅ | ❌ | ❌ |
I*.txt | ✅ | ✅ | ❌ | ❌ |
ı*.txt | ❌ | ❌ | ✅ | ❌ |
İ*.txt | ❌ | ❌ | ❌ | ✅ |
İ-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
I-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
ı-lat-up.txt | ❌ | ❌ | ❌ | ❌ |
DZ / Dz / dz
wildcard | DZ-uu.txt | Dz-ul.txt | dz-ll.txt |
---|---|---|---|
DZ*.txt | ✅ | ❌ | ✅ |
Dz*.txt | ❌ | ✅ | ❌ |
dz*.txt | ✅ | ❌ | ✅ |
dz-uu.txt | ✅ | ❌ | ❌ |
dz-ul.txt | ❌ | ❌ | ❌ |
Dz-uu.txt | ❌ | ❌ | ❌ |
Perl(glob)
POSIX glob とよく似た動作になっているが、ドットなしの小文字 i の扱いが異なる。
F*/f*/Foo
wildcard | foo.txt | fred.txt |
---|---|---|
F*.txt | ❌ | ❌ |
f*.txt | ✅ | ✅ |
Foo.txt | ✅ | ❌ |
i / I / ı / İ
wildcard | i-lat-lo.txt | I-lat-up.txt | ı-tur-lo.txt | İ-tur-up.txt |
---|---|---|---|---|
i*.txt | ✅ | ❌ | ❌ | ❌ |
I*.txt | ❌ | ✅ | ❌ | ❌ |
ı*.txt | ❌ | ❌ | ✅ | ❌ |
İ*.txt | ❌ | ❌ | ❌ | ✅ |
İ-tur-lo.txt | ❌ | ❌ | ❌ | ❌ |
I-tur-lo.txt | ❌ | ❌ | ✅ | ❌ |
ı-lat-up.txt | ❌ | ✅ | ❌ | ❌ |
DZ / Dz / dz
wildcard | DZ-uu.txt | Dz-ul.txt | dz-ll.txt |
---|---|---|---|
DZ*.txt | ✅ | ❌ | ❌ |
Dz*.txt | ❌ | ✅ | ❌ |
dz*.txt | ❌ | ❌ | ✅ |
dz-uu.txt | ✅ | ❌ | ❌ |
dz-ul.txt | ❌ | ✅ | ❌ |
Dz-uu.txt | ✅ | ❌ | ❌ |
まとめ
POSIX glob は、ワイルドカードがない部分についてはファイルシステムと同じ意見になっているものの、ワイルドカードが入ると大文字小文字を区別するようになるところがわかりにくい。
- Go(filepath.Glob)
- Python3(glob.glob)
- PHP7(glob)
は、POSIX glob と同じ意見になっている。
一方
- ruby(Dir.glob)
- Java(PathMatcher)
- C#(.NET Core GetFiles)
- C#(Mono Directory.GetFiles)
- Perl(glob)
は独自のアルゴリズムで処理しているらしく、POSIX glob とは異なる結果を返す。
「アルファベット2文字をセットにした文字で一文字目だけ大文字に出来るもの」や「小文字のiからドットを除いたもの」のあたりで不穏な動作になりやすい。