サブディレクトリを含めてファイルを検索し,ファイル名から取り出した共通部分ごとに件数を数える.同じ名前の接頭辞を持つファイルを探したり,拡張子ごとに分類したりするときに使える.
調べたいこと
ここでいう共通部分は,命名規則に合わせて取り出す文字列のことだ.ファイル名を比較して,自動で共通部分を見つける処理ではない.
まずは report_202609.csv のようなファイル名から,最初のアンダースコアより前を取り出して集計する.この文字列を,集計の「キー」と呼ぶ.数字だけでなく,英字や日本語も対象になる.
1folder/ 2├── report_202608.csv 3├── report_202609.csv 4├── photo_001.jpg 5└── sub_folder/ 6 ├── report_202607.csv 7 └── memo_001.txt
この例では,report が3件,photo と memo がそれぞれ1件になる.まず全件を集計し,必要に応じて2件以上あるキーに絞る.後半では,数値IDや拡張子で集計するための変更方法も紹介する.
ファイルの内容は比較しないため,同じキーに分類されても,中身が同じとは限らない.
実行前の準備
調査対象のフォルダーへ移動する.以降のコマンドは,現在のディレクトリとその配下を検索する.
1cd /path/to/folder
/path/to/folder は実際のパスに置き換える.コマンドはファイルの名前を調べるだけで,変更や削除は行わない.
共通部分ごとの件数を表示する
1find . -type f -print | awk -F'/' ' 2{ 3 file = $NF 4 if (match(file, /^[^_]+_/)) { 5 key = substr(file, RSTART, RLENGTH - 1) 6 count[key]++ 7 } 8} 9END { 10 for (key in count) { 11 if (count[key] > 0) { 12 printf "%s: %d\n", key, count[key] 13 } 14 } 15}' | LC_ALL=C sort
上の例では,次の結果になる.
1memo: 1 2photo: 1 3report: 3
find で通常のファイルを列挙し,awk でキーを取り出して件数を数える./^[^_]+_/ は「先頭から,アンダースコア以外の文字が1文字以上続き,そのあとにアンダースコアがある」という条件だ.RLENGTH - 1 で末尾の区切り文字を除く.
末尾の LC_ALL=C sort は,集計結果を文字列として並べる.数値順ではないため,数字だけのキーでも 10 が 2 より先になる.
同じキーが2件以上あるものだけを表示したい場合は,if (count[key] > 0) を if (count[key] > 1) に変更する.上の例では report: 3 だけが残る.
該当するファイルのパスも表示する
同じキーを持つファイルがどこにあるのか確認したい場合は,次のコマンドを使う.ここでは2件以上あるキーに絞っている.
1find . -type f -print | awk -F'/' ' 2{ 3 path = $0 4 file = $NF 5 if (match(file, /^[^_]+_/)) { 6 key = substr(file, RSTART, RLENGTH - 1) 7 count[key]++ 8 files[key] = files[key] "\n " path 9 } 10} 11END { 12 for (key in count) { 13 if (count[key] > 1) { 14 printf "%s (%d files):%s\n\n", key, count[key], files[key] 15 } 16 } 17}'
出力例は次のとおり.ファイルの表示順は環境によって異なる.
1report (3 files): 2 ./report_202608.csv 3 ./report_202609.csv 4 ./sub_folder/report_202607.csv
このコマンドの末尾には sort を付けない.見出しとパスが別々の行になっているため,全体を行単位で並べ替えると,キーとファイルの対応が崩れてしまう.キーのグループ自体の表示順も未指定としている.
コマンドのポイント
| 記述 | 役割 |
|---|---|
find . -type f -print | 現在のディレクトリ以下にある通常のファイルのパスを表示する |
awk -F'/' | パスを / で区切って扱う |
$NF | 最後の項目,つまりファイル名を取り出す |
/^[^_]+_/ | ファイル名の先頭から,最初のアンダースコアまでに一致する |
substr(file, RSTART, RLENGTH - 1) | 一致した部分から末尾のアンダースコアを除いたキー を取り出す |
count[key]++ | キーごとの件数を数える |
count[key] > 1 | 2件以上あるキー に絞る |
集計する部分を変える
どちらのコマンドも,if (match(...)) と key = ... の部分で集計のキーを決めている.ここを書き換えると,別の命名規則にも使える.count[key]++ や files[key] への追加処理はそのまま残す.
数値IDで集計する
3287_abc.png のようなファイル名から,先頭の数値IDだけを取り出す場合は,抽出条件を次のように変更する.
1if (match(file, /^[0-9]+_/)) { 2 key = substr(file, RSTART, RLENGTH - 1)
これは置き換える2行だけの抜粋だ.3287_abc.png と 3287_xyz.jpg は,どちらもキーが 3287 になる.report_202609.csv はこの条件に一致しないため,集計対象から外れる.001 と 1 は別のキーとして扱う.
ハイフンより前で集計する
report-202609.csv のようにハイフンで区切る場合は,次の2行に置き換える.
1if (match(file, /^[^-]+-/)) { 2 key = substr(file, RSTART, RLENGTH - 1)
report-202609.csv と report-202608.csv が,キー report にまとまる.区切り文字が複数ある場合も,最初のハイフンまでを取り出す.
拡張子で集計する
最後のドットより後ろをキーにする場合は,次の2行に置き換える.
1if (match(file, /\.[^.]+$/)) { 2 key = substr(file, RSTART + 1, RLENGTH - 1)
photo.jpg は jpg,report.csv は csv に分類される.ここでは末尾の区切り文字を除くのではなく,先頭のドットを除くため,開始位置を RSTART + 1 にしている.
このルールでは,archive.tar.gz は gz,.env は env になる.ドットがない名前や,ドットで終わる名前は対象外だ.大文字と小文字は区別するため,JPG と jpg は別々に集計される.
対象と制限
- 最初の例では,アンダースコアがない名前や,アンダースコアで始まる名前を集計しない.
report_はキーreportとして扱う. - キーは文字列として扱い,大文字・小文字や先頭のゼロを区別する.
- 空白や日本語を含むパスも扱えるが,パスに改行を含むファイルは対象外とする.
findの出力を改行ごとに処理するためだ.そのようなパスも扱う場合は,find -print0と NUL 区切りに対応した処理を組み合わせる必要がある. - この指定ではシンボリックリンクをたどらない.ハードリンクは,見つかったパスごとに数える.
- 読み取り権限などの理由で検索できないディレクトリがあれば,結果は不完全になる.
findのエラーも確認する. - ファイルが多い場合,パス一覧の保持にはその分のメモリを使う.件数だけが必要なら最初のコマンドを使う.
Linux の GNU find・GNU awk でサンプルファイルを使って動作確認した.また,筆者の macOS 環境でも動作を確認している.

