SSD Nodes Learn 🎉 VPS $5.50/月〜
ガイド Matt Connor著者 Matt Connor

Bashのグロブ展開とワイルドカードの使い方

Bashのグロブ展開がコマンド実行前にファイル名へ変換される仕組みを解説します。nullglob、dotglob、globstar、extglobの否定まで、安全な確認例で学べます。

コマンドの実行前に Bash のグロブ展開で行われる処理

Bash のグロブ展開では、コマンドの実行前に、*.log のようなパターンを実在するファイル名のソート済みリストへ変換します。rm *.log はパターンを rm に渡しません。Bash はディレクトリを読み取り、パターンに一致する名前だけを残してソートし、その後、各名前を個別の引数として rm を実行します。rm プログラムからは、パターンが使われたことを判別できません。

グロブ展開で意外な結果になる現象のほとんどは、この事実から生じます。一致判定はシェルが、その時点で存在するファイルを対象に行います。コマンドが受け取るのは、完成したリストだけです。正式名称は pathname expansion です。日常的には globbing と呼びますが、どちらも同じ処理を指します。

練習用の小さなツリーを作成する

以下の例はすべて同じファイルを対象に実行します。気になるファイルが近くにないよう、一時ディレクトリ内に作成します。後で 1 つのコマンドで削除できます。

lab=$(mktemp -d)
cd "$lab"
mkdir -p logs archive/2025 archive/2026
touch app.log app.log.1 app.log.2 app.log.10 error.log debug.LOG
touch notes.txt notes.md README 'weekly report.log'
touch .env .apprc
touch logs/nginx.log logs/nginx.log.1
touch archive/2025/dec.log archive/2026/jan.log

mktemp -d は新しいディレクトリを作成してそのパスを返し、$(...) はそのパスを変数 lab に格納します。この構文に慣れていない場合は、コマンド置換でコマンドの出力を取得する方法で詳しく説明しています。

作成した内容を確認し、使用しているシェルも確認してください。最後の注記の 1 つはバージョンに依存します。

ls -a
bash --version

ここで扱う内容はすべて bash 4.0 以降で動作します。末尾付近の globskipdots に関する注記は bash 5.2 以降に適用されます。2026 年 8 月時点で、現在の Ubuntu と Debian のリリースにはこのバージョン以降が含まれています。

全体を通じて使用するプレビュー用コマンドは printf '%s\n' PATTERN です。一致した項目を 1 行に 1 件ずつ表示し、ディスク上の内容は変更しません。そのため、確信のないパターンに対しても安全に実行できます。

3 つのワイルドカード: star、question mark、brackets

  • * は、空文字列を含む任意の長さの文字列に一致します。
  • ? は、ちょうど 1 文字に一致します。
  • [...] は、brackets 内の集合から選ばれたちょうど 1 文字に一致します。
printf '%s\n' *.log
printf '%s\n' app.log.?
printf '%s\n' app.log.[0-9]
printf '%s\n' notes.*
printf '%s\n' *

2 行目を、作成したファイル一覧と比較します。? は 1 文字だけを表すため、2 桁の suffix はこのパターンに一致しません。app.log.[0-9]* に広げて、もう一度実行し、違いを確認してください。

3 つのワイルドカードすべてに適用されるルールが 2 つあります。ワイルドカードは / には一致しないため、パターンは 1 つのディレクトリ階層内にとどまります。また、先頭の . はリテラルとして入力する必要があるため、* で始まるパターンはすべての hidden name を除外します。この 2 つ目のルールにより、rm * は dotfile に影響しません。

Glob の結果は、現在の locale の照合順序を使用してソートされます。これは、文字ごとのテキストソートです。そのため、suffix が 10 のものは 2 のものより前にソートされます。比較時に、どちらも数値として解釈されないためです。printf '%s\n' app.log.*、続いて ls -v app.log.* を実行し、2 つを比較してください。ls -v は埋め込まれた数値を値としてソートし、パイプラインでは sort -V が同じ処理を行います。

文字クラスと範囲

角括弧の中には、リスト、範囲、否定を記述できます。

  • [ch] は、c または h のいずれか1文字に一致します。
  • [a-f] は、a から f の範囲に含まれる1文字に一致します。
  • [!0-9] は、数字以外の1文字に一致します。[^0-9] も同じ意味です。
printf '%s\n' [dn]*
printf '%s\n' *.[A-Z]*
printf '%s\n' [A-Z]*

範囲は、環境によって動作が変わる部分です。範囲は単純な ASCII 順ではなく、現在のロケールの照合順序で解決されます。そのため、一部のシステムでは [a-z] が大文字にも一致します。Bash には ASCII 順を強制する globasciiranges オプションがあります。最近のビルドではデフォルトで有効になっているため、思い込まずに shopt globasciiranges で確認してください。どの環境でも同じように一致させる必要があるスクリプトでは、独立した行に LC_ALL=C を設定し、順序を明示できます。これにより、すべての範囲が単純なバイト順に戻ります。

この代入は、それ自体がコマンドでなければなりません。接頭辞として記述すると、LC_ALL=C ls [a-z]*ls に対してだけロケールを設定します。シェルは ls が開始する前に、独自のロケールでパターンをすでに展開しています。これは、最初のセクションで説明した規則が再び現れる例です。

ブレース展開は glob ではありません

ブレースはワイルドカードのように見えますが、動作はまったく異なります。ブレース展開は glob よりはるか前に実行され、ディスクの内容を確認することもありません。

echo file{1,2,3}.txt
echo {01..10}
echo {a..e}
echo {0..20..5}

これらのファイルが存在しなくても、展開は実行されます。ここが大きな違いです。glob はファイルシステムに問い合わせますが、ブレースはテキストを生成するだけです。そのため、ブレースはファイルなどを検索するのではなく、作成するときに適しています。

mkdir -p site/{css,js,img}
cp app.log{,.bak}
touch report-{2025,2026}-{01,02}.csv

cp app.log{,.bak}app.logapp.log.bak の 2 つの単語に展開されます。これは、コンマの前にある空の項目によって元のテキストが再生成されるためです。{01..10} ではゼロ埋めが維持されます。両端のいずれかが先頭に 0 を持つ場合、bash は生成するすべての数を同じ幅にそろえるためです。

展開の順序を理解すると、よく発生する問題の理由も分かります。Bash は最初にブレースを展開し、次に変数を展開し、最後にパス名展開を実行します。

n=5
echo {1..$n}

$n が数値になる時点では、ブレース展開はすでに完了しています。そのため、範囲は生成されません。seq 1 "$n" を使用するか、C 形式のループを for ((i=1; i<=n; i++)) として記述してください。

パターンが何にも一致しない場合の動作

これは最初に遭遇する失敗パターンで、誰でも一度は経験します。デフォルトでは、bash は一致しないパターンをそのまま残します。単語は入力したとおりにコマンドへ渡されるため、コマンドは *.tmp の文字を受け取り、ファイル名として扱おうとします。

printf '%s\n' *.tmp

printf では問題になりません。しかし、何かを作成するコマンドでは問題になります。.tmp ファイルがないディレクトリで touch *.tmp を実行すると、名前が文字どおり *.tmp のファイルが作成されます。このファイルを削除するには、シェルが再び展開しないように引用符で囲む必要があります: rm -- '*.tmp'

この動作を変える shell option は 2 つあります。それぞれを、何にも一致しないパターンに対して試し、その後で unset してください。

shopt -s nullglob
printf '%s\n' *.tmp
shopt -u nullglob

shopt -s failglob
printf '%s\n' *.tmp
shopt -u failglob

nullglob を有効にすると、一致しないパターンは完全に何も展開されないため、コマンドの引数が入力時より少なくなります。ループでは通常これが必要です。nullglob が無効な場合、for f in *.tmpf にリテラルのパターンを保持したまま、body を 1 回実行します。この動作を利用するほぼすべての script でバグになります。nullglob が有効な場合、body は 0 回だけ実行されます。

nullglob には独自の危険性があります。引数がすべて取り除かれたコマンドでも、実行自体は継続されるためです。ls *.nope は引数のない ls になり、ディレクトリ全体を一覧表示します。grep -l needle *.nopegrep -l needle になり、読み取るファイルがないため標準入力を待ち続け、停止したように見えます。必要なループの周囲で nullglob を有効にし、終了後に再び無効にしてください。

failglob は別の動作をします。一致しないパターンをエラーとして扱い、bash がエラーを報告してコマンドを実行しません。対話型 shell では、この設定が適しています。入力を誤ったパターンがリテラルの名前として rm に渡されるのを防げるためです。

隠しファイル: dotglob

shopt -s dotglob
printf '%s\n' *
shopt -u dotglob

そのブロックを実行し、先ほどの通常の printf '%s\n' * と一覧を比較します。dotglob は先頭のドットに関する規則を解除するため、* は隠しファイル名にも一致します。... のエントリは、dotglob を有効にしても常に除外されます。

dotglob を使用しない場合、隠しファイルにアクセスする通常の方法は .* です。ただし、以前は危険でした。5.2 より前の bash では、.*... にも一致したため、chmod -R 755 .* のような再帰コマンドは親ディレクトリにそのまま移動していました。Bash 5.2 では globskipdots オプションが追加され、デフォルトで有効になっています。これにより、... はすべての展開から除外されます。利用する前に shopt globskipdots を実行して確認してください。古い環境にはこの機能がないためです。

globstar: ツリーの末端まで一括してマッチング

shopt -s globstar
printf '%s\n' **/*.log
printf '%s\n' **/
shopt -u globstar

globstar を設定すると、パスの1つの構成要素全体を形成する ** は、0階層下を含む任意の深さにあるファイルとディレクトリにマッチします。そのため、**/*.log はカレントディレクトリだけでなく、logs/archive/ 以下のすべても対象にします。末尾にスラッシュを付けた **/ はディレクトリだけにマッチするため、ツリー構造をすばやく確認できます。

globstar を設定しない場合、** は通常の * として動作し、1つのディレクトリ内にとどまります。同じパターンをオプション無効の状態で実行して比較してください。別の場所からコピーした再帰パターンが、予想よりはるかに少ない結果しか返さない場合は、通常これが原因です。すべての bash では、globstar はデフォルトで無効です。

知っておくべき制限が1つあります。** はディレクトリへのシンボリックリンクをたどりません。シンボリックリンクでつないだツリーには、代わりに find -L が必要です。

nocaseglob(大文字と小文字が異なる名前用)

shopt -s nocaseglob
printf '%s\n' *.log
shopt -u nocaseglob

nocaseglobを有効にするとファイル名の照合で大文字と小文字が区別されなくなるため、大文字の.LOGサフィックスが小文字の.logパターンに一致します。最初のセクションの一覧と比較してください。このシェルで後続するすべてのコマンドの照合結果が変わるため、オプションはすぐに無効に戻します。1時間後に原因を調べる際、設定が残っていると切り分けが難しくなります。

nocaseglobが適用されるのはファイル名展開だけです。case文または二重角括弧テスト内のパターン照合に使用する関連オプションはnocasematchであり、両者は個別に設定します。

extglob: 「これ以外」を指定できるパターン

拡張パターンはデフォルトで無効です。単独の行で有効にします。

shopt -s extglob

形式は5つあり、それぞれ | で区切ったパターンのリストを受け取ります。

  • ?(list) は0回または1回の出現に一致します。
  • *(list) は0回以上に一致します。
  • +(list) は1回以上に一致します。
  • @(list) は選択肢のいずれか1つに正確に一致します。
  • !(list) はどの選択肢にも一致しないものに一致します。

拡張パターンは、オプションがすでに有効になっている shell で読み込む必要があります。そのため、以下の一連の処理は bash -O extglob で起動する小さなスクリプトに入れます。-O フラグは、新しい shell がファイルの1行目を読み込む前に shopt オプションを設定します。これにより、ファイル内のすべてのパターンを正しく解析できます。このセクションの最後の段落で、順序が重要な理由を説明します。

cat > patterns.sh <<'EOF'
printf '%s\n' @(app|error).log
printf '%s\n' app.log.+([0-9])
printf '%s\n' *.@(md|txt)
printf '%s\n' !(*.log)
printf '%s\n' !(*.log|*.md|README)
EOF
bash -O extglob patterns.sh

+([0-9]) は1桁以上の数字を表すため、任意の長さの番号付きサフィックスに対応します。これにより、先ほどの ? の制限を解消できます。両方のパターンを続けて実行すると、その違いを確認できます。

否定形式は「これ以外のすべて」を指定する方法です。extglob を有効にする主な理由でもあります。この形式には、意外に思われやすい点が2つあります。

ファイルだけでなくディレクトリにも一致します。そのため、ls !(*.log) は結果にディレクトリ名ではなく、対象ディレクトリの内容を一覧表示します。名前そのものを確認するには ls -d を使用するか、printf で確認用の結果を表示します。

また、他の glob と同じくドットファイルの規則にも従います。そのため、!(*.log).log で終わらないすべての表示可能な名前に一致します。隠し名を対象にするには、dotglob も設定する必要があります。

!(...) 内の ! は glob の否定です。履歴展開と bang 文字 とは関係ありません。これは別の機能で、異なる段階で実行されます。

もう1つ注意点があります。これは解析時に発生します。Bash はコマンド全体を実行前に解析するため、同じ行で extglob を有効にして拡張パターンを使用すると失敗します。オプションがまだ無効な状態でパターンが解析されるためです。shopt -s extglob はパターンより前の単独の行に置き、スクリプトの先頭付近で有効にしてください。関数本体は関数定義時に解析されるため、呼び出し前ではなく定義前にオプションを有効にする必要があります。上の一連の処理のように bash -O extglob で shell を起動すれば、解析が始まる前に順序を確定できます。

シェルがパターンを展開し、コマンドは展開しません

これは 2 つ目の失敗モードで、多くのバグの原因になります。

grep -l needle *.log
find . -name '*.log'
find . -name *.log

1 行目では bash がマッチングを実行し、grep はファイル名のリストを受け取ります。2 行目ではパターンをクォートしているため、find は 5 文字の *.log を受け取り、開始地点より下のすべての階層で独自にマッチングを実行します。3 行目がバグです。bash が現在のディレクトリだけを対象にパターンを先に展開するため、find には特定の 1 つの名前を探すよう指示されます。マッチする名前が 1 つだけのディレクトリでは、誤った対象を黙って検索します。2 つ以上マッチすると、余分な名前が式を指定する位置に渡されるため、find は使用方法のエラーを報告します。

ルールは簡単です。パターンをコマンドに渡す場合はクォートします。シェルに処理させる場合は、そのまま記述します。同じ区別が tar 内の --exclude パターンと rsync にも適用され、grep 内の --include フィルターにも適用されます。

Globs は文字を共有していても、正規表現ではありません。Glob では * は任意の文字列を意味します。正規表現では * は直前の要素の 0 回以上の繰り返しを意味するため、grep '*.log' が求めるものは見た目から想像するものとは大きく異なります。

変数も同じ順序で処理されます。グロブ展開は変数展開の後に実行されるため、パターンを保持するクォートされていない変数は、ディレクトリを対象に展開されます。

pat='*.log'
printf '%s\n' $pat
printf '%s\n' "$pat"

クォートしない場合、bash は変数の内容をグロブ展開します。クォートすると、パターンはリテラルのままになります。関連する重要な点として、グロブ展開で生成された単語がスペースで再分割されることはありません。そのため for f in *.log はスペースを含むファイル名を 1 つの名前として処理しますが、for f in $(ls *.log) はその名前を 2 つの単語に分割します。グロブを直接ループ処理してください。

破壊的なパターンを実行する前にプレビューする

新しいパターンを試すとき、最初のコマンドに rm を使わないでください。まず安全なコマンドでパターンを実行して一覧を確認し、その後、行の先頭にあるコマンドだけを変更します。

target='!(*.log)'
printf '%s\n' $target
ls -ld -- $target
rm -- $target

パターンを一度 target に記述すると、3 行の内容がずれません。$target を引用符で囲んでいないのは意図的です。これにより bash がパターンを展開できます。printf '%s\n' は名前を 1 行に 1 つずつ表示するだけで、何も変更しません。ls -ld はディレクトリの内容を一覧表示せず、各エントリ自体を表示します。モード列にはディレクトリかどうかが示されるため、再帰的な削除の前に確認する項目です。drwxr-xr-x のパーミッションビットを読むで、この列について説明しています。

上矢印で前の行を呼び出し、コマンドだけを編集してください。これでパターンは同じままになります。パターンを記憶だけで入力し直すと、ミスが入り込みます。

パターンの前に -- を置きます。これによりオプションの終わりが示され、- で始まるファイル名もフラグではなく名前として扱われます。

スクリプトの一部区間で globbing を無効にする場合は、set -f で無効にし、set +f で再び有効にします。

この習慣が重要なのは、rm に undo 機能がないためです。rm -rf で削除したファイルを復元する作業には時間がかかり、通常は完全には復元できません。したがって、プレビューにかかる手間は、実行後の復旧に比べてはるかに小さいものです。

終わったら練習用ディレクトリを削除してください。パスを表示して、まず内容を確認します。間違った変数を指す rm -rf は、このセクションで扱っている事故そのものだからです。

cd ~
echo "$lab"
rm -rf -- "$lab"

FAQ

find . -name *.logが誤ったファイルを見つけるのはなぜですか?

bash が find の実行を開始する前に *.log を展開するためです。シェルは現在のディレクトリでパターンに一致するファイルを探し、その結果のファイル名を find に渡します。すると find は、その1つの名前をすべての階層で検索します。2つ以上のファイルが一致すると、find は式を指定すべき位置に余分な引数を受け取り、使用方法のエラーを報告します。パターンを find . -name '*.log' のように引用してください。これにより、find 自身が独自の規則と再帰処理で一致検索を行います。

パターンがリテラル文字列として表示されるのはどういう意味ですか?

一致するものがなかったという意味です。デフォルトでは、bash は一致しないパターンをそのまま残し、未加工の文字列をコマンドに渡します。コマンドはそれをファイル名として扱います。shopt -s failglob を設定すると、一致しないパターンがエラーになり、コマンドが停止します。shopt -s nullglob を設定すると、パターンは空文字列に展開されます。nullglob を有効にする場合は、引数がまったくなくてもコマンドが正しく動作するか確認してください。ls *.nope は裸の ls になり、ディレクトリ全体を一覧表示するためです。

bash で1つのパターン以外のすべてのファイルに一致させるにはどうすればよいですか?

shopt -s extglob を単独の行で有効にしてから、否定形式を使用します。!(*.log).log で終わらない名前に一致し、!(*.log|*.md) は両方を除外します。dotglob も設定しない限り、隠しファイルは結果に含まれません。また、ディレクトリも含まれます。そのため、rm に渡す前に printf '%s\n' !(*.log) でパターンの結果を確認してください。

*が隠しファイルを除外するのはなぜですか?

先頭のドットはリテラルとして一致させる必要があるため、* はドットで始まる名前に一致しません。隠しファイルを対象にするには .* を使用します。または、shopt -s dotglob を設定して、通常のすべてのパターンに隠しファイルを含めます。... のエントリは dotglob の有効時も常に除外されます。さらに bash 5.2 以降では、デフォルトで有効な globskipdots オプションにより、.* からも除外されます。

**を動作させるには globstar が必要ですか?

はい。shopt -s globstar がない場合、bash は ** を通常の * として扱います。これは1つのディレクトリ内だけで一致し、そこで停止します。globstar を有効にすると、パスの1つの構成要素全体を形成する ** は任意の深さで一致します。そのため、**/*.log は現在のディレクトリと、その下にあるすべてのサブディレクトリを対象にできます。また、単独の **/ はディレクトリだけに一致します。** はディレクトリへのシンボリックリンクを追跡しない点に注意してください。シンボリックリンクで構成されたツリーには find -L を使用します。

#bash#globbing#shell#linux#scripting