こんにちは、おうどんです🍜
テキストファイルの名前だけ取り出したくて、ls | grep ‘*.txt’ を打ったんですけど、1件も出ません
ファイルが逃げたのかな。うどんの湯気と一緒に
逃げていません。* の意味が、bash と grep で違うだけです
(会話は説明用に作ったものです。コードの結果は今回の検証で実際に動かしたものです)
ファイルはちゃんとあります。ls *.txt なら出るのに、grep '*.txt' だと出ない。
同じ * なのに、扱う係が違うんです。
- ワイルドカード(グロブ。bash がファイル名を探すときの型)の
*は「何でもいい、おまかせ」 - 正規表現(grep などが文字の並びを探すときの書き方)の
*は「直前の1文字をもう0回以上、おかわり」
おまかせと、おかわり。1文字違いで、注文の中身がまるで別物です。
この記事では、2つの違いと、どちらをどこで使うかを、実際に動かしながら整理します。
検証は、今回の検証環境(Windows の Git Bash、bash 5.2.26、GNU grep 3.0、Python 3.13)で行いました。
まず覚えるのは、この3つです
- ファイルを選ぶのはワイルドカード、文字を探すのは正規表現
- 正規表現で「何でもいい」は
*ではなく.* - grep や find に渡すパターンは
'...'で囲む
ワイルドカードは「ファイル名の型」、正規表現は「文字の中の模様」
最初に答えを言うと、ワイルドカードはファイル名を選ぶための型で、正規表現は文字の並びの中から模様を探すための書き方です。
Linux の glob(7) のマニュアルにも、ワイルドカードのパターンは正規表現ではない、少し似ているだけ、と書いてあります。ワイルドカードは文章ではなくファイル名にマッチする、とも。
つまり双子じゃなくて、顔が似てるだけの他人
名字は同じ * なのに
その名字が、いちばんややこしいところです。
試しに使ったフォルダの中身はこうです(.secret.txt は先頭が . の隠しファイル、old はフォルダです)。
ls -A
.secret.txt
kitsune-udon.txt
old
soba.txt
tempura.TXT
udon.txt
udon10.txt
udontxt
udontxt は、ドットを打ち忘れたうどんです。このあと何度も事件に巻き込まれます。本人に悪気はありません(今回の検証用に、わざと作りました)。
ワイルドカードの記号:* ? [ ]
ワイルドカードの記号は、基本は3つです。bash のマニュアルのパターンマッチの節に書いてある意味をまとめます。
| 記号 | 意味 | 例 |
|---|---|---|
* | 何文字でも(0文字も含む) | udon*.txt |
? | ちょうど1文字 | ?don.txt |
[...] | かっこの中のどれか1文字 | [ks]*.txt |
[!...] | かっこの中にない1文字 | [!u]*.txt |
実際に echo で、何に広がるかを見ます。
echo *.txt
echo ?don.txt
echo udon*.txt
echo [ks]*.txt
echo [!u]*.txt
kitsune-udon.txt soba.txt udon.txt udon10.txt
udon.txt
udon.txt udon10.txt
kitsune-udon.txt soba.txt
kitsune-udon.txt soba.txt
*.txt に .secret.txt が入っていないこと、tempura.TXT が入っていないことに注目です。理由はあとの章で見ます。
udon*.txt で udon10.txt も来た。10 玉の注文まで拾ってくれる、気がきく店員さん
* は何文字でもいいので、10 でも 100 でも来ます。
100 玉が来たら、店の床が抜けます。
正規表現の記号:. * ^ $ [ ]
正規表現の記号は、もっと細かいです。GNU grep のマニュアルから、よく使うものをまとめます。
| 記号 | 意味 | 例 |
|---|---|---|
. | どれでも1文字 | udon.txt の . は1文字なら何でもいい |
* | 直前の1文字(かたまり)を0回以上くり返す | u*don |
.* | 何文字でも(0文字も含む) | ^udon.*\.txt$ |
^ | 行の先頭 | ^udon |
$ | 行の末尾 | \.txt$ |
[...] | かっこの中のどれか1文字 | [ks] |
[^...] | かっこの中にない1文字 | [^u] |
\. | 文字としての . | \.txt |
ワイルドカードの * に当たるのは、正規表現では .*(どれでも1文字、をおかわり)です。
ワイルドカードの ? に当たるのは、正規表現の . です。
じゃあ .* は、おまかせのおかわり?
何でもいい1文字を、好きなだけ。食べ放題の頼み方です。
さっきのファイル一覧を grep に流してみます。grep は、パターンに合う部分を含む行を出します。
ls -A | grep 'udon'
ls -A | grep '.txt'
ls -A | grep '\.txt$'
ls -A | grep '^udon'
ls -A | grep '^udon.*\.txt$'
それぞれの結果です(実行結果に、コマンドごとの区切りの行を入れています)。
kitsune-udon.txt
udon.txt
udon10.txt
udontxt
--- .txt
.secret.txt
kitsune-udon.txt
soba.txt
udon.txt
udon10.txt
udontxt
--- \.txt$
.secret.txt
kitsune-udon.txt
soba.txt
udon.txt
udon10.txt
--- ^udon
udon.txt
udon10.txt
udontxt
--- ^udon.*\.txt$
udon.txt
udon10.txt
grep '.txt' に、拡張子のない udontxt が混ざりました。. は「どれでも1文字」なので、n の後ろの txt でも合ってしまうんです。
ドットはドットじゃないんですか
正規表現の世界では、ドットは何にでも化ける忍者なんだよ
忍者を本物のドットに戻すのが \. です。\.txt$ にすると、末尾が本当に .txt の行だけになりました。
grep ‘*.txt’ が0件になった理由
冒頭の新人さんの話に戻ります。
ls -A | grep '*.txt'
echo "[exit $?]"
[exit 1]
1件も出ず、終了コードは1(見つからなかった)でした。
GNU grep のマニュアルの「基本と拡張」の節によると、grep の既定の書き方(BRE、基本正規表現)では、パターンの先頭にある * は、くり返しではなく、ただの * という文字として扱われます。
つまり grep は「* という文字のあとに、どれでも1文字、そのあとに txt」を探していました。名前に * が入ったファイルはないので、0件です。
おかわりを頼もうとしたら、前に何も食べてなかった
何も食べていないのに、おかわりはできませんね
正しくは、何をおかわりするかを先に書きます。
ls -A | grep '\.txt$'
結果は前の章の \.txt$ と同じで、末尾が .txt の5件です。
ちなみに、ワイルドカードで済む場面なら、grep を使わず ls *.txt や echo *.txt のほうが素直です。
grep を挟んだほうが、プロっぽく見えるので
プロっぽさで0件になるなら、素人のままでいいと思います。
grep や find に渡すパターンは ” で囲む
次は、ワイルドカードが勝手に広がる落とし穴です。a.txt・b.txt と、サブフォルダの sub/c.txt があるフォルダで、find を2通りに打ちます。
find . -name '*.txt' | sort
find . -name *.txt
echo "[exit $?]"
./a.txt
./b.txt
./sub/c.txt
--- find unquoted
find: paths must precede expression: `b.txt'
find: possible unquoted pattern after predicate `-name'?
[exit 1]
(--- の行は、2つのコマンドの間に入れた区切りです)
'' で囲んだほうは、サブフォルダまで3件見つかりました。
囲まなかったほうはエラーです。
bash のマニュアルのファイル名展開の節にあるとおり、bash は引用符で囲まれていない言葉に *・?・[ があると、それをパターンとして、合うファイル名に置き換えます。find が動く前に、bash が *.txt を a.txt b.txt に書き換えていたんです。find が受け取ったのは -name a.txt b.txt でした。
囲んだほうは、*.txt がそのまま find に届き、find 自身がワイルドカードとして、サブフォルダの中の名前まで合わせました。
find さん、エラーの2行目で、引用符を忘れていませんかって聞いてくれてますね
優しい。おうどんのレビューより優しい
もっと怖いのは、エラーにならないときです。b.txt を消して、a.txt だけにして同じコマンドを打ちました。
find . -name *.txt
echo "[exit $?]"
./a.txt
[exit 0]
成功です。でも sub/c.txt は見つかっていません。今いるフォルダに .txt が1つだけあったので、bash がそれに置き換え、find は a.txt という名前だけを探しました。
エラーが出ないので、見落としたことにも気づけません。
bash は気をきかせて注文を書き換え、find は書き換えられた注文を忠実に作る。2人とも真面目なのに、c.txt だけが厨房に届かない。
いちばん悪いのは、引用符を付けなかった人です。
grep に渡す正規表現、find の -name に渡すワイルドカード、どちらも '...' で囲んで、bash に広げさせないのが基本です。
🔰 ここまで読めば今日から困らない
- ワイルドカード(
*・?・[...])は、bash がファイル名を選ぶための型 - 正規表現(
.・*・^・$など)は、grep などが文字の中から模様を探すための書き方 - 正規表現の
*は「直前の1文字のおかわり」。何でもいいなら.*、本物のドットは\. - grep や find にパターンを渡すときは
'...'で囲む
| やりたいこと | ワイルドカード | 正規表現 |
|---|---|---|
| 何文字でも | * | .* |
| ちょうど1文字 | ? | . |
.txt で終わる | *.txt | \.txt$ |
udon で始まる | udon* | ^udon |
| u か s で始まる | [us]* | ^[us] |
この表、壁に貼っておきます
おうどんは、おでこに貼っておく
自分では見えないので、壁にしてください。
表まで覚えたら、今日は帰ってうどん食べていい?
帰って大丈夫です。おかわりは、ほどほどに。
実践編として、ワイルドカードが変数の中で勝手に広がる話はシェルスクリプトで空白入りのファイル名が壊れる原因と対処にまとめています。
ここから先は中級者向けです。読み飛ばしてもOKです。
中級:ワイルドカードは名前全体、正規表現は一部でいい
ワイルドカードと正規表現は、記号だけでなく、合わせる範囲も違います。
- ワイルドカードは、ファイル名の全体が型に合わないと選ばれない
- grep の正規表現は、行のどこか一部に模様があれば選ばれる
echo *udon*
ls -A | grep 'udon'
kitsune-udon.txt udon.txt udon10.txt udontxt
echo *udon* の結果です。名前の全体を合わせるので、udon の前後にも * を付けています。
grep のほうは、* がなくても、udon を含む4行が出ました(前の章の1つ目の結果と同じです)。
[[ ]] の中でも、== はワイルドカード、=~ は正規表現です。bash のマニュアルの条件式の節には、=~ の右側は POSIX の拡張正規表現として扱われ、文字列のどこか一部に合えば成功する、とあります。
for f in udon.txt kitsune-udon.txt udontxt tempura.TXT; do
g=no; r=no
[[ $f == udon*.txt ]] && g=yes
[[ $f =~ udon.*\.txt ]] && r=yes
printf '%-18s glob=%s regex=%s\n' "$f" "$g" "$r"
done
udon.txt glob=yes regex=yes
kitsune-udon.txt glob=no regex=yes
udontxt glob=no regex=no
tempura.TXT glob=no regex=no
kitsune-udon.txt は、ワイルドカードの udon*.txt では選ばれず(先頭が udon ではない)、正規表現の udon.*\.txt では選ばれました(途中に udon から .txt までの並びがある)。
きつねうどんは、うどんじゃないんですか
名札で見ると『きつね』さん。中身で見るとうどん。哲学だね
哲学ではなく、名札全体を見るか、中身の一部を見るかの違いです。
正規表現で全体を合わせたいなら、^ と $ で両端を押さえます(^udon.*\.txt$)。grep なら、行全体に合わせる -x もあります。
case 文もワイルドカードです。拡張子で分けるときに便利です。
for f in udon.txt soba.txt tempura.TXT; do
case $f in
*.txt) echo "$f: txt" ;;
*) echo "$f: other" ;;
esac
done
udon.txt: txt
soba.txt: txt
tempura.TXT: other
最後の *) は、何にでも合うので「それ以外」の受け皿になります。tempura.TXT は大文字なので *.txt に合わず、受け皿に落ちました。天ぷらだけ、大声で注文したらしいです。
中級:隠しファイルと / は、* でも拾わない
最初の echo *.txt に、.secret.txt が入っていなかった理由です。
bash のマニュアルによると、ファイル名の先頭の .(とスラッシュの直後の .)は、パターンの側でも . を書いて明示しないと合いません(dotglob を有効にしたとき以外)。glob(7) にも、rm * は .profile を消さない、という例があります。
また、/ もパターンの側で / を書かないと合いません。* は / をまたがないんです。
echo .*.txt
echo */*.txt
.secret.txt
old/udon.txt
先頭に . を書けば隠しファイルが、*/ を書けば1段下のフォルダの中が選ばれました。
隠しファイルは、裏メニュー。知ってる人しか頼めない
裏メニューが rm * で消えないのは、助かる場面が多いです。逆に「全部消したつもりが、隠しファイルが残っていた」もあるので、消す前に echo で何に広がるか見ておきましょう。
正規表現にはこの決まりはありません。前の章の grep '\.txt$' では、.secret.txt も普通に出てきました。
じゃあ、見られたくないファイルは名前の頭に . を付ければ安全ですね
見えにくいだけで、ls -A で丸見えです。鍵のかからない裏メニューは、ただのメニューです。
中級:何にも合わないと、* が文字のまま残る
echo *.csv
*.csv
.csv のファイルがないので、*.csv という文字がそのまま出ました。bash のマニュアルにあるとおり、合うファイルがないと、既定ではその言葉は書いたまま残ります(nullglob を有効にすると消え、failglob を有効にするとエラーになってコマンドが実行されません)。
品切れなのに、伝票だけ厨房に回った感じ
伝票を受け取った厨房は、*.csv という名前の料理を探し始めます。もちろん、ありません。
じゃあ、ファイルがないときは *.csv という名前のファイルを処理しちゃうんですか
その通りで、ループで回すと「*.csv という名前のファイル」を1回処理しようとします。対処は実践編の記事に書いたので、そちらをどうぞ。
中級:本物の * や . を探すとき
名前に * が入ったファイル star*.txt を作って試しました。
echo star*.txt
echo 'star*.txt'
ls -A | grep 'star\*'
star*.txt
star*.txt
star*.txt
1行目は、ワイルドカードの * が star で始まるファイルに広がり、たまたま本人だけが選ばれました。2行目は引用符で囲んだので、文字の * のままです。結果が同じでも、意味は違います。
結果が同じなら、どっちでもいいよね
隣に starfish.txt が引っ越してきた日に、どっちでもよくなくなります。1行目は、ヒトデまで連れてきます。
3行目の grep では、\* で「文字としての *」を探しています。
正規表現の記号を全部ただの文字として探したいなら、grep の -F(固定文字列として探す)が楽です。
printf 'price 1.5\nprice 105\n' | grep '1.5'
printf 'price 1.5\nprice 105\n' | grep -F '1.5'
price 1.5
price 105
---
price 1.5
(実行結果の --- は、2つのコマンドの間に入れた区切りです)
grep '1.5' は、. が忍者なので 105 にも合いました。値段の検索で 1.5 円と 105 円を同じにされたら、うどん屋がつぶれます。
ここから先は上級者向け。読み飛ばしてもOKです。
上級:u*don は「どんぶり」にも合う
正規表現の * は「0回以上」なので、0回でもいいんです。
printf 'udon\nuuudon\ndonburi\nkitsune-udon\nsoba\n' > words.txt
grep 'u*don' words.txt
grep -x 'u*don' words.txt
grep -E '^u+don$' words.txt
udon
uuudon
donburi
kitsune-udon
--- -x
udon
uuudon
--- ^u+don$ (-E)
udon
uuudon
(実行結果の --- の行は、コマンドごとに入れた区切りです)
u*don は「u を0回以上、そのあと don」なので、u が0個の donburi の don にも合いました。-x で行全体に合わせるか、+(1回以上)を使えば、うどんだけに絞れます。
どんぶりがうどんに合う。それは料理としては正しい
正規表現としては、たいてい意図していないほうです。
+ を使った最後の行で -E を付けたのは、grep の既定(BRE)では ?・+・{・|・(・) が特別な意味を持たず、\+ のように書く必要があるからです(GNU grep のマニュアルの「基本と拡張」の節)。-E を付けると拡張正規表現(ERE)になり、そのまま書けます。
もう1つ、前のほうで見た「先頭の * は文字になる」は BRE の話です。
ls -A | grep -E '*.txt'
echo "[exit $?]"
.secret.txt
kitsune-udon.txt
soba.txt
udon.txt
udon10.txt
udontxt
[exit 0]
-E にしたら、今回の GNU grep 3.0 では6件出ました(udontxt まで)。ただ、GNU grep のマニュアルの「問題のある正規表現」の節では、正規表現の先頭にあるくり返しの記号は、動きが決まっていない書き方として挙げられています。別の grep や別の版で同じ結果になるとは考えず、そもそも書かないのが安全です。
-E だと通るなら、-E のほうが心が広い
心が広いのではなく、決まっていないだけです。決まっていない人に、お店の鍵は預けられません。
上級:=~ の右側を引用符で囲むと、正規表現でなくなる
[[ ]] の右側は、引用符の付け方で意味が変わります。bash のマニュアルの条件式の節には、== の右側で引用符で囲んだ部分は文字どおりに合わせる、=~ でも引用符で囲んだ部分は文字どおりになる、正規表現は変数に入れておくと引用符の問題を避けやすい、と書いてあります。
f=udon.txt
[[ $f == "udon*.txt" ]] && echo "quoted: match" || echo "quoted: no match"
[[ $f == udon*.txt ]] && echo "unquoted: match" || echo "unquoted: no match"
p='\.txt$'
[[ $f =~ $p ]] && echo "regex var: match" || echo "regex var: no match"
[[ $f =~ "\.txt$" ]] && echo "regex quoted: match" || echo "regex quoted: no match"
quoted: no match
unquoted: match
regex var: match
regex quoted: no match
grep や find では「パターンは引用符で囲む」が基本でしたが、[[ ]] では逆で、引用符で囲むとパターンの意味がなくなります。
囲むと、bash は \.txt$ という文字そのものを探しに行きます。そんな名前のうどんはありません。
grep では囲めと言われて、[[ ]] では囲むなと言われる。どっちなんですか
読む係が違うんです。grep の前には bash が立っているので、囲んで bash から守る。[[ ]] は bash 自身が読むので、囲むと「これはただの文字です」と伝わってしまう。
門番がいる店と、店主が自分で出てくる店の違いです。
正規表現は、上の p='\.txt$' のように変数に入れて、$p を引用符なしで書くのがおすすめです。
=~ で合った部分は、BASH_REMATCH という配列に入ります。0番が合った全体、1番以降がかっこで囲んだ部分です。
f=kitsune-udon.txt
if [[ $f =~ ([a-z]+)\.txt ]]; then
echo "0: ${BASH_REMATCH[0]}"
echo "1: ${BASH_REMATCH[1]}"
fi
0: udon.txt
1: udon
[a-z] に - は入らないので、kitsune- は飛ばされ、udon.txt の部分が合いました。名前を取り出すつもりなら、何を文字に含めるかまで考えておきます。
上級:[^…] と [!…]、大文字小文字、拡張パターン
ワイルドカードで「〜以外」は [!...] です。bash のマニュアルには ! と ^ のどちらでもよいとあり、今回の bash 5.2.26 でも同じ結果でした。
echo [^u]*.txt
kitsune-udon.txt soba.txt
ただし glob(7) によると、POSIX(Unix の共通規格)では、正規表現が [^...] を使うことになったため、ワイルドカードの [^...] の動きは決まっていません。sh で動かすスクリプトなど、bash 以外でも動かしたいなら [!...] にしておきます。
大文字小文字は、今回の Git Bash では区別されました(最初の echo *.txt に tempura.TXT が入らなかったのは、これです)。bash の nocaseglob を有効にすると区別しなくなり、dotglob で隠しファイルも * に含まれます。
shopt -s nocaseglob
echo *.txt
shopt -u nocaseglob
shopt -s dotglob
echo *.txt
shopt -u dotglob
kitsune-udon.txt soba.txt tempura.TXT udon.txt udon10.txt
.secret.txt kitsune-udon.txt soba.txt udon.txt udon10.txt
nocaseglob を入れたら、天ぷらの大声も聞き取れるようになった
耳が良くなったのではなく、大声と小声を同じに扱うことにしただけです。
shopt -s extglob で使える拡張パターンもあります(!(パターン) は「それ以外」、+(a|b) は「1回以上」など)。
shopt -s extglob
echo !(*udon*).txt
echo +(udon|soba).txt
shopt -u extglob
soba.txt
soba.txt udon.txt
拡張パターンは正規表現に少し近づきますが、それでも名前全体に合わせる、という性格は変わりません。
extglob まで覚えたら、もう正規表現いらないのでは
ファイルの中身の行を探すのは、やっぱり grep の仕事です。係は増やさず、仕事で分けましょう。
上級:Python の fnmatch は、ワイルドカードを正規表現に翻訳する
Python の fnmatch モジュールには、ワイルドカードを正規表現に変換する fnmatch.translate() があります。ワイルドカードが正規表現の小さな仲間だということが、目で見えます。
import fnmatch, re
for p in ["*.txt", "udon?.txt", "[!0-9]*"]:
print(f"{p!r:14} -> {fnmatch.translate(p)!r}")
print(fnmatch.fnmatch(".secret.txt", "*.txt"))
print(fnmatch.fnmatch("tempura.TXT", "*.txt"))
print(fnmatch.fnmatchcase("tempura.TXT", "*.txt"))
print(fnmatch.fnmatch("old/udon.txt", "*.txt"))
Python 3.13 の実行結果です。
'*.txt' -> '(?s:.*\\.txt)\\Z'
'udon?.txt' -> '(?s:udon.\\.txt)\\Z'
'[!0-9]*' -> '(?s:[^0-9].*)\\Z'
True
True
False
True
(変換結果は repr() で出しているので、\ が2つに見えています。中身は1つです)
* が .* に、? が . に、. が \. に、[! が [^ に変わり、最後に \Z が付いています。Python の re のドキュメントによると、\Z は文字列の終わりだけに合う記号、先頭の (?s: は . を改行にも合わせる指定です。名前全体に合わせる、という性格も、そのまま正規表現に書き直されていました。
翻訳できるなら、ワイルドカードは正規表現の子ども?
子どもというより、よく使う注文を短く書ける略語、くらいに思っておくと近いです。略語は便利ですが、店が変わると通じないことがあります。
下の4行は、bash と違うところです。
fnmatchは、先頭の.も/も特別扱いしない(ドキュメントにも明記)。.secret.txtもold/udon.txtも*.txtに合ったfnmatch.fnmatchはos.path.normcase()で大文字小文字をそろえるので、Windows ではtempura.TXTも合った。区別したいならfnmatchcase
bash では裏メニューだった .secret.txt が、Python では表のメニューに堂々と並んでいます。のれん分けした店で、方針が変わったようなものです。
同じワイルドカードでも、bash と Python で少しずつ決まりが違います。「ワイルドカードだから同じ」と思わず、使う道具のドキュメントを見るのが安全です。
冒頭の新人さんに戻ると
冒頭の ls | grep '*.txt' は、grep に「何もない所のおかわり」を頼んでいました。
ファイルを選ぶなら ls *.txt、grep で探すなら ‘\.txt$’。どちらの係に頼んでいるかを、先に決めましょう
同じ * でも、bash はおまかせ、grep はおかわり。
記号を見たら、まず「これを読むのは誰か」を考えると迷いません。
それでも迷ったら、man に聞きましょう。おうどんに聞くと、うどんの話が始まります。
まずは、grep や find に渡すパターンを '...' で囲むところから。おかわりは、何をおかわりするか言ってから頼みましょう🍜
参考資料
- Bash Reference Manual: Pattern Matching —
*・?・[...]の意味、!と^の否定、拡張パターン(extglob)、先頭の.と dotglob - Bash Reference Manual: Filename Expansion — 引用符のない言葉の
*・?・[をパターンとして置き換える、合わないときに残る・nullglob・failglob、先頭の.と/、nocaseglob - Bash Reference Manual: Conditional Constructs —
[[ ]]の==はパターン・=~は拡張正規表現で一部に合えば成功、引用符で文字どおりになる、変数に入れる、BASH_REMATCH、case - glob(7) – Linux manual page — ワイルドカードは正規表現ではない、ファイル名に合わせる、
/と先頭の.、rm *と.profile、POSIX での[^...] - GNU Grep Manual —
.・*・?・+・^・$・角かっこの意味、BRE と ERE の違い、BRE で先頭の*は文字、問題のある正規表現、-E・-F・-x - fnmatch — Unix filename pattern matching(Python 3.13) — パターンの表、
/と先頭の.を特別扱いしない、translate()、fnmatchとfnmatchcaseの違い - re — Regular expression operations(Python 3.13) —
\Zは文字列の終わり、(?s)は.を改行にも合わせる
2026年11月時点の内容です。










コメント