大文字小文字を区別しないという不幸な世界のワイルドカード(macOS)

# この記事について

macOS のファイルシステムは、デフォルトでは殆どのディレクトリで大文字小文字を区別しない。
そのような世界で、ワイルドカードを用いると何がマッチするか、という話。

# 大文字小文字を区別しないということ

例えばこんなスクリプトを実行すると

```bash:bash

#!/bin/bash

set -eu

# ASCII の英数字
touch foo.txt FOO.txt
ls *.txt && rm *.txt

# 所謂全角文字
touch ｚｅｎ.txt ＺＥＮ.txt
ls *.txt && rm *.txt

# ギリシャ文字・キリル文字・ローマ数字
touch ωяⅶ.txt ΩЯⅦ.txt
ls *.txt && rm *.txt

# DZ, NJ
touch ǳ.txt # U+01F3	Latin Small Letter DZ
touch ǲ.txt # U+01F2	Latin Capital Letter D with Small Letter z
touch Ǳ.txt # U+01F1	Latin Capital Letter DZ
touch ǌ.txt # U+01CC	Latin Small Letter NJ
touch ǋ.txt # U+01CB	Latin Capital Letter N with Small Letter J
touch Ǌ.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

```

こんな結果になる

```text
foo.txt
ｚｅｎ.txt
ωяⅶ.txt
ǌ.txt	ǳ.txt
i.txt	İ.txt	ı.txt
ⓐ.txt
```

`FOO` と `foo` は簡単。大文字小文字を区別しないのでどちらかしか生き残れない。

`ｚｅｎ`、 `ωяⅶ` や `ⓐ` のあたりを見て分かる通り、7bit-ASCII の範囲外の文字にも大文字小文字があり、それらは区別されない。

`ǲ` は「Titlecase Letter」というカテゴリの文字で、大文字でも小文字でもない。
対応する小文字は `ǳ` で、対応する大文字は `Ǳ` になる。
この文字も大文字小文字を区別しないので、 `touch ǳ.txt ǲ.txt Ǳ.txt` としても、ひとつしか生き残れない。

`ı`, `İ`, `i`, `I` の件は下表のとおり。

|言語|`I`の小文字|`i`の大文字|
|:--|:--|:--|
|英語|`i`|`I`|
|トルコ語|`ı`|`İ`|

`ı.txt`, `İ.txt`, `i.txt`, `I.txt` を touch すると、
`i.txt`, `İ.txt`, `ı.txt` が生き残る。

[unicode.org](https://unicode.org/cldr/utility/character.jsp?a=0131) を見ると、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|❌|❌|❌|❌|

### Ǳ / ǲ / ǳ 

|wildcard|Ǳ-uu.txt|ǲ-ul.txt|ǳ-ll.txt|
|:--|:--|:--|:--|
|Ǳ*.txt|✅|❌|❌|
|ǲ*.txt|❌|✅|❌|
|ǳ*.txt|❌|❌|✅|
|ǳ-uu.txt|✅|❌|❌|
|ǳ-ul.txt|❌|✅|❌|
|ǲ-uu.txt|✅|❌|❌|

## ruby(Dir.glob)

ruby の動作は POSIX glob とはだいぶ異なる。

基本的には「大文字と小文字を区別しない」という動作で一貫しているような感じ。
`Foo.txt` だと `foo.txt` のみがマッチして、 `F*.txt` だと `foo.txt` と `fred.txt` がマッチする。わかりやすい。

しかし、

```ruby:ruby
Dir.glob("ǳ-u*.txt") #=> []
Dir.glob("ǳ-uu.txt") #=> ["files/Ǳ-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|❌|❌|❌|❌|

### Ǳ / ǲ / ǳ 

|wildcard|Ǳ-uu.txt|ǲ-ul.txt|ǳ-ll.txt|
|:--|:--|:--|:--|
|Ǳ*.txt|✅|❌|❌|
|ǲ*.txt|❌|✅|❌|
|ǳ*.txt|❌|❌|✅|
|ǳ-uu.txt|✅|❌|❌|
|ǳ-ul.txt|❌|✅|❌|
|ǲ-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|❌|❌|❌|❌|

### Ǳ / ǲ / ǳ 

|wildcard|Ǳ-uu.txt|ǲ-ul.txt|ǳ-ll.txt|
|:--|:--|:--|:--|
|Ǳ*.txt|✅|❌|❌|
|ǲ*.txt|❌|✅|❌|
|ǳ*.txt|❌|❌|✅|
|ǳ-uu.txt|❌|❌|❌|
|ǳ-ul.txt|❌|❌|❌|
|ǲ-uu.txt|❌|❌|❌|

## C#(.NET Core / Directory.GetFiles)

ruby の動きと似ている。
ruby と異なり、 `Ǳ*.txt` で `ǳ-ll.txt` にちゃんと(?)マッチする。
しかし逆に、`ǳ-uu.txt` では `Ǳ-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|❌|❌|❌|❌|

### Ǳ / ǲ / ǳ 

|wildcard|Ǳ-uu.txt|ǲ-ul.txt|ǳ-ll.txt|
|:--|:--|:--|:--|
|Ǳ*.txt|✅|✅|✅|
|ǲ*.txt|✅|✅|✅|
|ǳ*.txt|✅|✅|✅|
|ǳ-uu.txt|❌|❌|❌|
|ǳ-ul.txt|❌|❌|❌|
|ǲ-uu.txt|❌|❌|❌|

## C#(Mono / Directory.GetFiles)

意外にも、 .NET Core と Mono で動作が異なる。
大文字でも小文字でもない `ǲ` という文字に負けている感じがする。

### 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|❌|❌|❌|❌|

### Ǳ / ǲ / ǳ 

|wildcard|Ǳ-uu.txt|ǲ-ul.txt|ǳ-ll.txt|
|:--|:--|:--|:--|
|Ǳ*.txt|✅|❌|✅|
|ǲ*.txt|❌|✅|❌|
|ǳ*.txt|✅|❌|✅|
|ǳ-uu.txt|✅|❌|❌|
|ǳ-ul.txt|❌|❌|❌|
|ǲ-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|❌|✅|❌|❌|

### Ǳ / ǲ / ǳ 

|wildcard|Ǳ-uu.txt|ǲ-ul.txt|ǳ-ll.txt|
|:--|:--|:--|:--|
|Ǳ*.txt|✅|❌|❌|
|ǲ*.txt|❌|✅|❌|
|ǳ*.txt|❌|❌|✅|
|ǳ-uu.txt|✅|❌|❌|
|ǳ-ul.txt|❌|✅|❌|
|ǲ-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からドットを除いたもの」のあたりで不穏な動作になりやすい。