サブディレクトリを含めてファイルを検索し,ファイル名から取り出した共通部分ごとに件数を数える.同じ名前の接頭辞を持つファイルを探したり,拡張子ごとに分類したりするときに使える.

調べたいこと

ここでいう共通部分は,命名規則に合わせて取り出す文字列のことだ.ファイル名を比較して,自動で共通部分を見つける処理ではない.

まずは 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] > 12件以上あるキー に絞る

集計する部分を変える

どちらのコマンドも,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 環境でも動作を確認している.

参考