ワイルドカード(glob)は、bashで最も手軽に「まとめて処理」を実現できる仕組みです。*.log のような1語で、複数ファイルを一気に対象化できる──この便利さは、日々の運用を確実に加速させます。
一方で、未一致時に文字列のまま渡ってしまう、隠しファイルが意図せず含まれる、展開順が環境に左右される、といった“罠”も同じくらい身近に潜んでいます。誤ったグロブは、たった一行でディレクトリを壊し、復旧に何時間もかかる事故へ直結します。
本稿は、そんな「速さ」と「安全」の両立を目的に、bashのワイルドカードを“運用と安全設計”の視点で体系化したものです。
グロブの基本復習から始め、nullglob/failglob/dotglob/globstar などの重要オプションの使いどころ、クォートや配列、ロケール固定による挙動の再現性確保まで、実務で事故を避けるための原則を筋道立てて解説します。さらに、「破壊系コマンドはドライランを標準化する」「未一致方針を事前に固定する」「展開結果は配列で受ける」など、すぐに取り入れられるベストプラクティスを20項目に凝縮しました。
対象読者は、日常的にログ整理・バックアップ・一括リネーム・CSV集計といった“雑多だけど欠かせない”処理をbashで回している方です。安全性を高めつつ、作業時間を短縮したい。チームやCIでも再現性のあるスクリプトにしたい。そのための設計指針とテンプレートを、本記事で一式そろえます。読み終える頃には、あなたのグロブは“速いけれど危ない”道具ではなく、“速くて安全”な標準装備になっているはずです。
本記事のねらい(運用と安全設計 – 視点)
この文章の目的は、bashのワイルドカード(glob)を「速く正確に使う」だけでなく、「事故を未然に防ぐ設計」にまで昇華させることにあります。便利な *.log や img_??.jpg が、意図しないファイルに及ぶとき、それは単なる書き手の不注意ではなく、設計段階での安全策が不足しているサインです。運用の現場では、個人の勘や場当たり的な確認ではなく、仕組みとしての安全が求められます。本記事は、その仕組み化に直接役立つ原則と具体手順を示します。
不確実性をどう扱うか
まず重視したいのは、未一致時の挙動、隠しファイルの扱い、展開順序の再現性という三つの不確実性です。これらは、日常のワンライナーでは見落とされがちですが、スクリプトとして長期運用されると、環境差や小さな仕様変更が大きな破壊につながる起点になります。nullglob や failglob を使って未一致の扱いを固定し、dotglob で「見える世界」を明確にし、LC_ALL=C などで並び順のばらつきを抑えることで、再現性の高い実行基盤が生まれます。
ドライランの標準化
次に、人間の確認作業に依存しない安全策です。破壊的な処理は「必ずドライランを通る」運用に置き換えます。rm や mv を直接叩くのではなく、まずは展開結果を echo や printf で出力し、対象集合が想定どおりかを“機械的に”検証します。ドライランをトグルで切り替えられるようにしておけば、開発・検証・本番の各局面で一貫した運用が可能になります。
多様なファイル名への耐性
また、ファイル名の多様性に耐えることも柱です。空白、改行、先頭ハイフン、絵文字など、いまやファイル名に現れうる文字は予測不能です。したがって、展開結果を配列に受ける設計、二重引用を徹底するコーディング規律、-- セパレーターでのオプション誤解釈回避といった“地味だが効く”工夫を積み上げる必要があります。これらはコードの可読性をわずかに下げることがあっても、総合的な保守性と事故率低下という大きなリターンをもたらします。
globを万能にしない判断軸
最後に、globを「なんでも屋」にしない判断軸を明確にします。条件が複雑化し始めたら、find や xargs、awk など、役割に適したツールへ委譲する勇気が求められます。globは入口として強力ですが、出口まで背負わせると、思わぬ角で転びやすくなる――このバランス感覚を、実例とともに身体化していきます。
本記事の焦点整理
| 焦点 | ねらい |
|---|---|
| 再現性の確保 | shopt とロケール固定で挙動を明文化し、どの環境でも同じ結果を得る |
| 事故ゼロ設計 | ドライラン標準化・配列受け・クォート徹底で、人間の勘に依存しない |
この二軸を押さえておくことで、globを「速いけれど危ない道具」から「速くて安全な標準装備」へと進化させることができます。
先に結論:安全原則5か条
bashのワイルドカードを運用するうえで最初に共有しておきたいのは、細かなテクニックよりも大枠の安全原則です。これらを踏まえて設計すれば、どんなスクリプトでも致命的な事故を避けやすくなります。
クォートを最優先する
ファイル名に空白や特殊文字が含まれても壊れないようにするためには、二重引用を徹底することが基本です。"$file" のように書く癖をつけるだけで、余計な分割や意図しない展開を防げます。
テスト駆動で進める
新しいglobパターンを書くときは、いきなり本番処理に組み込むのではなく、まずは echo や printf で展開結果を確認します。対象の集合が想定どおりかを目で見てから次のステップに進めば、誤操作の可能性を大きく減らせます。
破壊系は必ずドライランを通す
削除や移動といった不可逆な操作は、必ずドライランを経由させる設計にしましょう。rm "$@" の前に echo "Would remove: $@" を入れて確認する、あるいは DRYRUN=1 のような変数で挙動を切り替える仕組みを持たせると、実運用での安心感が格段に増します。
shoptで挙動を固定する
bashのglobは、シェルオプションによって挙動が大きく変わります。nullglob、failglob、dotglob などを明示的に設定し、未一致や隠しファイルの扱いを曖昧にしないことが重要です。スクリプトの先頭で shopt -s nullglob のように方針を固定しておけば、実行環境による差異がなくなります。
ロケールを固定して再現性を確保する
ファイルの展開順序は環境のロケール設定に依存します。思わぬ並び替えが起きると処理結果が変わることもあります。そこで LC_ALL=C を明示することで、どの環境でも同じ順序でglob展開されるように設計しておくと安心です。
これら5か条は、一見すると当たり前のように見えるかもしれません。しかし実際には「つい省略して事故につながる」部分でもあります。以下のセクションでは、この原則を支える具体的な仕組みやオプション、そして実務的な落とし穴と回避策を掘り下げていきます。
グロブの基礎を最短復習
グロブは「パターンを書けば、該当するファイル名に自動展開される」仕組みです。コマンドが受け取るのはパターンそのものではなく、展開後の実ファイル名の並びです。この一点を押さえるだけで、未一致や順序の問題を具体的に考えられるようになります。echo *.log と書いた時、シェルが *.log を error.log access.log へと差し替え、コマンドへ引き渡す――その置き換えがどのような規則で動くかを短距離で確認していきます。
展開の仕組み:*・?・[] と未一致時の挙動
アスタリスクは「0文字以上の任意列」、クエスチョンは「任意の1文字」、ブラケットは「集合のいずれか1文字」を表します。ここで重要なのは、未一致だった場合にどう振る舞うかで、bashはオプション設定によって対応が変わります。素の状態では、*.csv が1件も見つからないと文字列のまま *.csv がコマンドに渡るため、削除や移動の文脈では事故に直結します。未一致方針は後述の nullglob や failglob で明示的に固定する前提に切り替えると、スクリプトは格段に扱いやすくなります。
また、展開は現在の作業ディレクトリを基準に行われます。cd の有無で結果が変わるため、再現性が必要な処理では絶対パスや pushd/popd を使い、前後関係を自分で管理する書き方が健全です。ファイル名の並び順は環境のロケールに左右される点も、実務では見落とせません。
グロブと正規表現の違いを感覚で掴む
しばしば混同されますが、グロブはファイル名マッチのための簡易パターンであり、正規表現は文字列処理のための強力な記法です。* はグロブでは「任意列」を表しますが、正規表現では直前の繰り返しを意味し、意味が異なります。grep -E にグロブを渡してもうまくいかないのはこの差異が原因です。ファイル集合の選択はグロブで、内容の判定や抽出は grep や awk で、という役割分担が基本戦略になります。
| 比較軸 | グロブ(ワイルドカード) | 正規表現(例:grep -E) |
|---|---|---|
| 主目的 | ファイル名の展開 | 文字列のパターン照合 |
| 代表記法 | * ? [] | . + * [] () ` |
| 評価の場所 | シェルが評価して引数に展開 | コマンド内部で入力テキストに適用 |
| 典型ユース | rm *.log、for f in img_??.jpg | `grep -E ‘ERROR |
違いを理解したうえで、“選ぶ/運ぶ”はグロブ、“調べる/変える”は正規表現と覚えておくと、誤用が減り、処理が素直になります。
波括弧展開 {a,b} は「列挙の生成」であってグロブではない
{a,b} はグロブと同じような見た目でも、シェルによる単純なパターン展開であり、ファイルシステムの検索ではありません。touch {01..12}.log のように規則的な名前を一括生成するには非常に便利ですが、{jpg,png} のような書き方は「候補の列挙」しか行いません。存在しないファイル名も平然と出力に含まれるため、存在チェックは別途必要です。
ファイルの集合を選ぶのが目的なら *.{jpg,png} のように波括弧展開とグロブを組み合わせます。前者が候補を列挙し、後者が実在ファイルに絞り込む、という二段構えで理解すると、思考が整理されます。
shoptで挙動を制御する(重要オプション)
bashのグロブは、そのまま使うと「便利だけど曖昧」な挙動をします。そこで鍵になるのが shopt コマンドです。これはシェルの動作を細かく切り替えるための仕組みで、globに関しても多くの重要オプションを備えています。安全性を高めるためには、スクリプトの先頭で方針を固定することが不可欠です。
nullglob と failglob
もっとも基本的な選択肢は「未一致時にどう振る舞うか」です。
nullglobを有効にすると、パターンが一致しない場合は空集合に展開されます。結果がそのままrm *.csv→rmだけになるので、安全に失敗させることができます。failglobは、未一致ならエラーを発生させてシェルを止めます。スクリプトを強制的に中断させたいときにはこちらが有効です。
どちらを選ぶかはケースによりますが、「空集合に展開」か「即エラー」のいずれかに統一するだけで、曖昧な動作を排除できます。
dotglob と隠しファイル
デフォルトでは、globは . で始まる隠しファイルを無視します。これを含めたい場合に dotglob を使います。ログローテーションやキャッシュ整理など、隠しファイルを確実に対象にする必要がある処理では、スクリプト先頭で shopt -s dotglob を明示するのが安全です。逆に不要な場合は shopt -u dotglob として明示的に外すことで、「なぜ含まれないのか」を後から説明できる状態にしておけます。
globstar と再帰探索
globstar を有効にすると、** でディレクトリを再帰的に走査できるようになります。for f in **/*.log; do ...; done と書くだけで深い階層まで一気に対象化できるのは強力です。ただし、再帰の範囲が広がりすぎるリスクもあるため、nullglob や failglob と組み合わせて意図しない全削除を防ぐことが重要です。
extglob と高度なパターン
extglob を有効にすると、+(pattern) や !(pattern) のような拡張構文を利用できます。複雑な条件指定が可能になる一方で、表現が長くなり過ぎるとテストや保守が難しくなります。ここでは「短い除外条件を表現するときに使う」など、用途を限定することが肝心です。
| オプション | 役割 | 注意点 |
|---|---|---|
| nullglob | 未一致は空集合に展開 | rm時に安全。空集合による副作用は確認必須 |
| failglob | 未一致でエラーを発生 | スクリプトを強制停止。早期検知に有効 |
| dotglob | 隠しファイルを展開対象に | 必要に応じてON/OFFを明示する |
| globstar | **で再帰的に探索 | 範囲が広がるので安全策と併用必須 |
| extglob | 拡張パターンを利用可能 | 表現が複雑化しすぎないようにする |
これらのオプションは、使うかどうかを「状況に応じて切り替える」のではなく、スクリプト設計の段階で一貫した方針を固定することが大切です。そうすることで、環境や実行者が変わっても同じ挙動を再現でき、安心してglobを活用できるようになります。
ベストプラクティス11選(コード付き)
ここでは、globの“速さ”と“安全”を両立させるための実践手法を、短い解説とサンプルコードで示します。各項目はそのままスクリプトに流用できます。必要に応じて set -euo pipefail を併用してください。
破壊系コマンドは必ずドライランを経由させる
不可逆な操作は、展開結果を必ず目視確認してから本実行に移します。トグルで切り替えられる形にしておくと運用が安定します。
#!/usr/bin/env bash
set -euo pipefail
DRYRUN=${DRYRUN:-1} # 1=ドライラン, 0=本実行
shopt -s nullglob
logs=( *.log )
printf 'Target files:\n'
printf ' - %s\n' "${logs[@]}"
if (( DRYRUN )); then
printf '\n[DRYRUN] Would remove:\n'
printf 'rm -- %q\n' "${logs[@]}"
else
rm -- "${logs[@]}"
fi
未一致の挙動は nullglob か failglob で固定する
曖昧さを排除し、未一致を明文化します。どちらを採るかはプロジェクト方針で統一します。
# 空集合にする(安全に“何もしない”挙動)
shopt -s nullglob
csvs=( *.csv )
printf 'found %d csv(s)\n' "${#csvs[@]}"
# or: 未一致なら即エラー(早期検知)
shopt -s failglob
csvs=( *.csv ) # 一件も無ければここでエラー
配列に展開結果を受けてから処理する
二重引用と配列で、空白や改行を含むファイル名にも強くなります。
shopt -s nullglob
files=( *.txt )
for f in "${files[@]}"; do
# 常に二重引用。オプション誤解釈を避けるため -- を使う
cp -- "$f" "/backup/$f"
done
-- セパレーターと二重引用で誤解釈を防ぐ
先頭が - のファイル名でも安全に扱います。
touch -- -danger.txt
rm -f *.txt # 危険:-danger.txt が一致すると rm が誤解
rm -- -danger.txt # 安全:-- により引数として扱われる
# ループでも常に:
for f in *.txt; do
mv -- "$f" "safe/$f"
done
globstar は範囲を最小化し、除外と併用する
再帰は強力だからこそ、対象を最小限に絞るのが鉄則です。除外は extglob を併用すると書きやすくなります。
shopt -s globstar nullglob extglob
# node_modules と .git を除外して .log のみ
for f in **/*.log; do
case "$f" in
*/node_modules/*|*/.git/*) continue ;;
esac
gzip -9 -- "$f"
done
extglob の否定パターンはテスト前提で使う
表現力は増しますが過剰一致のリスクがあるため、必ずテストディレクトリで確認してから本運用に入れます。
shopt -s extglob nullglob
imgs=( !(*_tmp).png ) # *_tmp.png を除いた .png 一式
printf '%s\n' "${imgs[@]}"
隠しファイルの扱いは dotglob で方針を固定する
見える世界を明示し、後から説明できる状態にします。
# 隠しファイルも含める
shopt -s dotglob nullglob
all=( * )
# 隠しは含めない(デフォルトへ戻す)
shopt -u dotglob
visible=( * )
並び順の再現性はロケール固定か明示ソートで担保する
環境差で並びが変わると結果が不安定になります。順序をコードで固定します。
# 実行全体を C ロケールで
export LC_ALL=C
# 明示ソートで順序確定(配列へ再投入)
shopt -s nullglob
mapfile -t files < <(printf '%s\n' *.csv | sort)
for f in "${files[@]}"; do
awk -F, '{print $1}' -- "$f"
done
NUL 区切りで巨大件数・変則名にスケールさせる
空白や改行を含む名前、巨大件数でも堅牢にスケールします。
# find で選定 → NUL 区切り → while で堅牢に処理
find . -type f -name '*.jpg' -print0 |
while IFS= read -r -d '' path; do
convert "$path" -strip -interlace Plane -quality 85 "${path%.jpg}.webp"
done
サンドボックス(対象縮小)で安全に試し、差分を確認する
いきなり実データに当てず、テスト用ツリーで挙動を確定してから本番に持ち込みます。
# テスト用ディレクトリを用意
tmp="$(mktemp -d)"
cp -- *.log "$tmp"/
pushd "$tmp" >/dev/null
shopt -s nullglob
before=( *.log )
# ここでワンライナーや関数を検証
gzip -9 -- *.log || true
after=( *.gz )
printf 'before:%s\n' "${before[*]}"
printf 'after :%s\n' "${after[*]}"
popd >/dev/null
rm -rf -- "$tmp"
find と xargs -0 の役割分担で“選ぶ/運ぶ”を最適化する
条件が複雑なら、選定は find、実行はコマンドに任せて責務分離します。
# 30日以上前の .log を安全に削除
find /var/log -type f -name '*.log' -mtime +30 -print0 \
| xargs -0 -r rm --
-r は引数が無いときにコマンドを実行しないオプションです(GNU xargs)。未一致時の空振り事故を防げます。
どの項目も共通しているのは、曖昧な前提をコードで固定するという姿勢です。shopt、二重引用、--、配列、ロケール固定、NUL 区切り、サンドボックス検証といった小さな積み重ねが、glob を“速くて安全な標準装備”へと変えていきます。
典型シナリオ別レシピ(安全版)
運用でよく遭遇する処理を、安全設計を前提に最短ルートで組み立てます。いずれも「曖昧さを残さない」ために、shopt・二重引用・--・ロケール固定を最初に整えます。
画像だけ圧縮(拡張子を限定し、再圧縮の二重適用を防ぐ)
ディレクトリ配下の画像をまとめて圧縮する場合でも、対象の拡張子を明示し、出力先や上書きの方針を固定します。Web向け最適化の例です。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
shopt -s nullglob globstar
# 出力は _min を付けて並存させる(上書き事故回避)
for src in **/*.{jpg,jpeg,png}; do
# すでに最適化済みならスキップ
[[ "$src" == *_min.* ]] && continue
case "$src" in
*/node_modules/*|*/.git/*) continue ;;
esac
out="${src%.*}_min.${src##*.}"
# 実運用では convert/oxipng/jpegoptim など好みに応じて
if [[ "${src##*.}" =~ jpe?g ]]; then
convert -- "$src" -strip -interlace Plane -quality 85 "$out"
else
oxipng -o 4 -s -- "$src" -o 4 -s -o 4 --out "$out"
fi
done
再圧縮のループに落ちない命名規則と、隠しディレクトリや依存フォルダの除外が肝心です。
ログだけ削除(保持期間のポリシーをコード化する)
削除は最も事故になりやすい操作です。保持期間の基準を find の条件に埋め込み、空振り時は実行しない設計にします。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
# 30日以上前の .log を削除(GNU xargs の -r で空振り実行を抑止)
find /var/log/app -type f -name '*.log' -mtime +30 -print0 \
| xargs -0 -r rm --
削除基準は「ローテーション完了後」など運用フローと合わせておくと、復旧の判断が容易になります。
バックアップだけ同期(片方向・除外・検証の三点を固定)
rsync を使うときは片方向を明示し、不要ディレクトリを除外し、ドライランから入ります。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
SRC="/srv/data/"
DST="/backup/data/"
EXCLUDES=(
"--exclude=.git/"
"--exclude=node_modules/"
"--exclude=cache/"
)
# まずはドライラン
rsync -aHAX --delete --itemize-changes -n "${EXCLUDES[@]}" -- "$SRC" "$DST"
# 差分が想定どおりか確認後、本実行
# rsync -aHAX --delete --itemize-changes "${EXCLUDES[@]}" -- "$SRC" "$DST"
--delete は強力です。ドライランの出力をレビューするフローを標準運用にしておくと事故率が劇的に下がります。
CSV処理の実務ミニ特集(glob × テキスト処理)
CSVは「横結合」「縦結合」「列抽出」でつまずきやすい場面が多い領域です。glob で集合を選び、paste や awk に役割を渡すのが安定します。
横結合:paste の安全運用と空振り対策
複数の単列CSV(同一行数)を横方向に結合する例です。未一致は空集合にし、入力量を事前に可視化します。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
shopt -s nullglob
cols=( col_*.csv ) # 例: col_a.csv, col_b.csv ...
printf 'columns: %d\n' "${#cols[@]}"
printf '%s\n' "${cols[@]}" | nl -ba
# 行数チェック(最初のファイルを基準)
base_lines=$(wc -l < "${cols[0]:-"/dev/null"}")
for f in "${cols[@]}"; do
lines=$(wc -l < "$f")
[[ "$lines" -eq "$base_lines" ]] || {
printf 'line mismatch: %s (%d != %d)\n' "$f" "$lines" "$base_lines" >&2
exit 1
}
done
# 区切り文字を , にして横結合
paste -d',' -- "${cols[@]}" > merged.csv
行数不一致の早期検知を入れておくと、後段の不整合を未然に防げます。
縦結合:ヘッダ管理とファイル名の多様性に耐える
同じ列構成のCSVを縦に結合し、先頭ファイルのみヘッダを残す例です。NUL 区切りで堅牢に処理します。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
shopt -s nullglob
out="stacked.csv"
: > "$out" # 空で初期化
first=1
find . -maxdepth 1 -type f -name 'data_*.csv' -print0 |
while IFS= read -r -d '' f; do
if (( first )); then
cat -- "$f" >> "$out"
first=0
else
# 2行目以降(ヘッダ除去)
awk 'NR>1' -- "$f" >> "$out"
fi
done
find ... -print0 と read -d '' の組み合わせは、空白や改行を含むファイル名にも強く推奨されます。
列抽出:awk -F, とエスケープの前提条件を言語化する
CSVの列抽出は簡単そうで事故が起きやすい領域です。フィールド区切りが本当に単純なカンマなのか、引用符やエスケープを含むのかで方針が変わります。単純CSVを前提にした最短の列抽出は以下のとおりです。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
shopt -s nullglob
inputs=( report_*.csv )
mapfile -t inputs < <(printf '%s\n' "${inputs[@]}" | sort)
# 1列目と3列目を抽出(単純CSV前提)
for f in "${inputs[@]}"; do
awk -F',' 'BEGIN{OFS=","} {print $1,$3}' -- "$f"
done > extracted.csv
ダブルクォートやカンマを含むセルが混在する場合は、mlr --icsv --ocsv(Miller)や csvkit、python -c で csv モジュールを使うなど、CSVパーサを使う方針に切り替えます。
| 状況 | 推奨手段 |
|---|---|
| 純粋な区切り・引用なし | awk -F, で OK |
| 引用・改行・エスケープあり | mlr/csvkit/python csv へ委譲 |
| 巨大ファイルで高速化が必要 | mlr のストリーム処理が有利 |
ここまで整えると、glob は「選ぶ」部分に集中し、CSVの解釈は専用ツールへ任せる筋が通ります。結果として、処理の再現性と可読性が両立します。
グロブとfind/xargsの使い分け
グロブは「簡潔に対象を選ぶ」点で一級ですが、条件が複雑になった瞬間に読みやすさと安全性が落ちます。更新時刻の閾値、サイズ条件、除外ディレクトリ、深さ制限、NUL区切りが必要な巨大件数――このあたりが見え始めたら、選定は find に委譲し、実処理はコマンドへ渡す設計に切り替えるのが健全です。xargs -0 は、NUL 区切りの安全な受け渡しを可能にし、空白・改行・先頭ハイフンなど変則的なファイル名にも強くなります。
役割分担の原則を言語化する
まず「どこで選び、どこで処理するか」を定義します。グロブはシェルの段階で選びますが、表現力は限定的です。find はファイルシステムに対する問合せエンジンだと捉えると、選定ロジックは find に、運搬は xargs に、変換は各コマンドにという三分法が自然に定まります。こうして責務を分離すれば、可読性・テスト容易性・再現性が一度に上がります。
| 観点 | グロブ | find + xargs |
|---|---|---|
| 記述量 | 短い・直感的 | 長いが明示的 |
| 表現力 | パターン一致中心 | 時刻・サイズ・型・深さ・除外など網羅 |
| 例外対策 | 工夫が要る | -print0 と -0 で堅牢 |
| 規模 | 小〜中規模 | 中〜大規模・大量件数に強い |
| 再現性 | ロケール・カレント依存 | 条件がコード化されやすい |
短い一括処理や“目の前の数十ファイル”ならグロブで十分です。条件が一つ増えた段階で find に寄せる習慣を持つと、将来の保守が楽になります。
find を“選ぶ言語”として使い倒す
find は「どの集合を対象にするのか」を正確に記述できます。更新時刻やサイズ、拡張子の組み合わせに加え、除外したいパスを -prune で切り落とせます。実処理は -exec ... + で直接呼び出してもよいですが、件数や名前の多様性を考えると -print0 | xargs -0 のパイプ設計が読みやすく、ログの採取も容易です。
# 例:48時間以内に更新された .csv を選定し、ヘッダを見出し語に整形
find ./data -type f -name '*.csv' -mmin -$((48*60)) -not -path '*/cache/*' -print0 |
xargs -0 -r awk -F',' 'BEGIN{OFS=","} NR==1{$1=toupper($1)} {print}'
この設計では、「いつ更新されたどのファイルを」「何から除外して」「どのように渡すか」が明文化されます。グロブでは曖昧になりがちな前提が、find の条件としてコードに残る点が効きます。
-exec … + と xargs -0 の使い分けを体で覚える
-exec … + は find が適切な件数で引数を束ねてコマンドに渡すため、シンプルで速いことが多いです。ログの粒度や処理のカスタマイズが増える場合は xargs 側でオプションを調整できる余地があり、-n(1回あたりの引数数)や -P(並列度)でスループットを最適化できます。いずれにしても NUL 区切り(-print0 と -0)の徹底が、安全運用の土台になります。
# -exec パターン:まとまった束で圧縮
find logs -type f -name '*.log' -mtime +14 -exec gzip -9 -- {} +
# xargs パターン:ログ採取や並列実行の調整がしやすい
find images -type f -name '*.jpg' -print0 \
| xargs -0 -n 100 -P 4 -I{} sh -c 'convert "$1" -strip -quality 85 "${1%.jpg}.webp"' _ {}
ここまでの違いを把握しておけば、グロブで無理をせず、複雑性を受け止める“正しい肩”に仕事を渡せます。結果として、スクリプトは短くならなくても、読み手にとって予測可能で安全なコードになります。
クォート戦略&ファイル名の地雷
bashで安全運用を目指すなら、まずはクォートの一貫性と引数の解釈ミス対策を身につけます。ファイル名に空白や改行、先頭ハイフン、メタ文字、絵文字や結合文字が混じるのは珍しくありません。想定外の入力に出会っても壊れない書き方に、日頃から体を慣らしておくことが重要です。
二重引用を標準にし、展開は配列で受ける
二重引用は“壊れないデフォルト”です。展開結果は配列に受けてから処理すると、空白や改行を含む名前でも破綻しません。
#!/usr/bin/env bash
set -euo pipefail
shopt -s nullglob
files=( *.txt )
for f in "${files[@]}"; do
# すべての引数は二重引用で包む
wc -l -- "$f"
done
ここでの -- は、たとえ "$f" が -n のように見えてもオプション扱いを無効化します。
先頭ハイフン問題は -- で封じる
-report.txt のようなファイル名は、コマンドにとってオプションに見えます。-- を挟むだけで事故を断ち切れます。
touch -- -danger.txt
ls -- -danger.txt
rm -- -danger.txt
-- を入れる癖は、運用の“護身”として最もコスパが高い作法の一つです。
改行や制御文字に備えて、NUL 区切りを選ぶ
改行を含むファイル名は、改行区切りのループを崩壊させます。NUL 区切りなら堅牢に処理できます。
# 生成側: -print0 で NUL 区切り
find . -type f -name '*.csv' -print0 |
# 受け側: read -d '' で NUL 終端を読む
while IFS= read -r -d '' path; do
printf 'processing: %q\n' "$path"
done
%q はデバッグ出力に便利で、シェルが安全に再解釈できる形にエスケープして表示します。
メタ文字やワイルドカードを“そのままの文字”として扱う
[ や * を含むファイル名自体を扱いたい場面では、シェルが展開しない位置に逃がします。パスは変数に入れて二重引用、あるいは printf '%s\0' で生の値として渡します。
# 例: 文字通りの [abc].txt というファイルを移動
name='[abc].txt'
mv -- "$name" ./literal/
展開を避けるには「リテラル化」か「後段へ値として渡す」の二択だと覚えると判断が速くなります。
Unicode と正規化の“見た目は同じ”問題に気づく
Unicode では同じ見た目の文字が複数のバイト列で表現されます(NFC/NFD など)。macOS の一部ファイルシステムでは NFD、Linux では NFC が一般的です。遠隔同期や照合での不一致の謎は、しばしばこの正規化差が原因になります。
# ざっくり比較:バイト列を可視化
hexdump -C -- "レポート.txt" > /tmp/a.hex
hexdump -C -- "レポート.txt" > /tmp/b.hex
diff -u /tmp/a.hex /tmp/b.hex || true
運用では「生成系を統一する」「受け側で正規化フィルタをかける」など、どちらで揃えるかを方針化しておくと後々のトラブルが減ります。
クォート戦略の要点を手元に置く
| 事象 | 破綻の例 | 有効な対策 |
|---|---|---|
| 空白・改行 | for f in $(ls) が分裂 | 配列+二重引用、NUL 区切り |
| 先頭ハイフン | rm *.txt が -danger.txt を誤解 | -- セパレーターの常用 |
| メタ文字 | mv [a]* が意図せず展開 | 変数へ格納して二重引用 |
| Unicode正規化 | 同名のはずが一致しない | 生成系の統一・正規化方針の明文化 |
クォートと引数解釈にまつわる地雷を先に設計で潰しておくと、グロブは途端に扱いやすくなります。毎回の“勘と祈り”に頼らず、コードに安全を織り込む姿勢が、長期運用の品質を支えます。
ロケールと展開順序の罠
同じグロブでも、実行環境のロケールが違えば展開順序は変わります。アルファベットの大小や濁点の扱い、数字の並び順が環境依存で揺れるため、結果の再現性が要求されるスクリプトでは順序をコードで固定する必要があります。LC_ALL=C を設定すると、バイト順に近い単純な順序で処理され、OS やユーザー設定の影響を受けにくくなります。
順序に依存する処理は、ロケール固定に加えて明示的なソートで確定させます。展開結果を配列に受け、printf と sort を通してから再投入すると、外形的にも「順序を制御している」ことが読み手に伝わります。数字の自然順を期待する場面は、sort -V のようなバージョン比較ソートへ切り替えれば意図が保てます。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
shopt -s nullglob
files=( *.csv )
# 自然順が必要なら -V、単純で良ければデフォルト
mapfile -t files < <(printf '%s\n' "${files[@]}" | sort -V)
for f in "${files[@]}"; do
awk -F, '{print $1}' -- "$f"
done
「ロケール固定」か「明示ソート」かは二者択一ではありません。要件次第で両方を組み合わせ、順序に関する暗黙の前提を消すことが安全設計の基本線になります。
スクリプト雛形(安全版テンプレ)
毎回ゼロから“安全装備”を書くのは抜け漏れの元です。冒頭で set と shopt、ロケール、ロギング、ドライラン切り替えを定型化しておくと、以後のコードは本質に集中できます。以下は最小構成の雛形です。
#!/usr/bin/env bash
# shellcheck disable=SC2155
set -euo pipefail
# --- Policy: locale & glob behavior ---
export LC_ALL=C
shopt -s nullglob # 未一致は空集合
# shopt -s failglob # 代わりに即エラー方針にしたい場合はこっち
# shopt -s dotglob # 隠しファイルを含める場合のみ
# shopt -s globstar # ** 再帰が必要な場合のみ
# shopt -s extglob # 拡張パターンが必要な場合のみ
# --- Config: dry-run toggle & logging ---
readonly DRYRUN="${DRYRUN:-1}" # 1=ドライラン, 0=本実行
log(){ printf '[%(%F %T)T] %s\n' -1 "$*" >&2; }
# --- Utilities ---
die(){ log "ERROR: $*"; exit 1; }
# --- Args & preconditions ---
ROOT="${1:-.}"
[[ -d "$ROOT" ]] || die "not a directory: $ROOT"
# --- Plan: list targets deterministically ---
mapfile -t targets < <(cd "$ROOT" && printf '%s\n' *.log | sort || true)
log "found ${#targets[@]} target(s)"
for t in "${targets[@]}"; do
log "target: $ROOT/$t"
done
# --- Act: dry-run first, then commit ---
if (( DRYRUN )); then
log "[DRYRUN] Would gzip the above files"
exit 0
fi
for t in "${targets[@]}"; do
gzip -9 -- "$ROOT/$t"
done
log "done"
この雛形の肝は、方針を先頭で固定し、対象集合を実行前に可視化している点です。DRYRUN は環境変数で切り替え可能にしておくと、CI と手動運用の両方で扱いやすくなります。
テスト&検証の流れ
安全設計は一度書いて終わりではありません。テスト用ツリーでの検証→差分確認→本番適用を一連の流れとしてスクリプト化しておくと、再利用のたびに品質が積み上がります。テストでは「未一致」「隠しファイル」「先頭ハイフン」「空白・改行」「大量件数」の各パターンを最低限含めると、現場で遭遇する大半の地雷を先に踏めます。
検証は、対象リストの取得と結果の比較を機械的に行うのが効率的です。次のスニペットは、事前事後のファイル一覧を採取し、diff で確認する最短の枠組みです。
#!/usr/bin/env bash
set -euo pipefail
export LC_ALL=C
tmp="$(mktemp -d)"
trap 'rm -rf -- "$tmp"' EXIT
# フェイク環境を作る
mkdir -p "$tmp/w"
printf 'a\n' > "$tmp/w/a.log"
printf 'b\n' > "$tmp/w/b.log"
printf 'c\n' > "$tmp/w/.hidden.log"
: > "$tmp/w/ -danger.log" # 先頭ハイフン
: > "$tmp/w/space name.log"
# 変更前スナップショット
( cd "$tmp/w" && find . -type f -print0 | sort -z | xargs -0 -I{} sh -c 'printf "%s\t%s\n" "$(stat -c%s "{}" 2>/dev/null || stat -f%z "{}")" "{}"' ) > "$tmp/before.txt"
# ここで対象スクリプトをドライラン→本実行の順に当てる
DRYRUN=1 ./script.sh "$tmp/w" || true
DRYRUN=0 ./script.sh "$tmp/w"
# 変更後スナップショット
( cd "$tmp/w" && find . -type f -print0 | sort -z | xargs -0 -I{} sh -c 'printf "%s\t%s\n" "$(stat -c%s "{}" 2>/dev/null || stat -f%z "{}")" "{}"' ) > "$tmp/after.txt"
# 差分確認
diff -u "$tmp/before.txt" "$tmp/after.txt" || true
この流れを CI に組み込めば、glob の挙動変更や依存コマンドのバージョン差による副作用を、人手のレビュー前に機械で検知できます。テストケースを増やすほど、現場での安心感が積み上がります。
まとめ:安全設計が生産性を上げる
グロブは、bash における「選択と一括適用」の最短距離です。しかし、未一致、隠しファイル、順序、引数解釈といった不確実性を抱えたままでは、便利さはそのままリスクにも直結します。本稿で繰り返し強調したのは、曖昧さをコードで固定するという姿勢でした。shopt による方針の明示、LC_ALL=C と明示ソートによる再現性の確保、配列と二重引用、-- セパレーター、NUL 区切り、ドライランと差分確認。これらを雛形として手元に置けば、毎回の判断を省力化しつつ、事故率を限りなくゼロに近づけられます。
安全は速度の敵ではありません。むしろ、事故のない運用はレビューの負担を減らし、チームの合意形成を早め、本当に書くべき処理へ時間を割り当てることを可能にします。グロブは“速いけれど危ない”道具ではなく、適切に整えれば“速くて安全な標準装備”です。今日の一行から、設計の側に安全を寄せていきましょう。
参考リンク
- GNUマニュアル日本語版 — Bashマニュアルを日本語で読むことができます (GNU)
- atmarkIT「【 shopt 】コマンド――bashのシェルオプションを表示」 —
shoptのオプション解説と使い方 (ITmedia エンタープライズ) - atmarkIT「shopt コマンド(応用編その1)」 —
shoptの応用設定例解説 (ITmedia エンタープライズ) - 俺的備忘録「shoptコマンドで設定できるbashの便利設定まとめ」 — 複数の
shoptオプションを整理して紹介 (orebibou.com) - gihyo.jp「続・玩式草子 — 第42回 bashの便利な機能」 — Bash の拡張機能、展開・置換など便利技の紹介 (gihyo.jp)
