はじめに (対象読者・この記事でわかること)
このページは、Linux 環境でシステム管理や開発作業を行うエンジニア・初心者を対象にしています。特に、あるルートディレクトリ配下に同名のサブフォルダが複数存在し、そこに格納されたすべてのファイルを一括で抽出したいというニーズを持つ方に最適です。この記事を読むと、find コマンドや Python/Node.js スクリプト を用いたパターン検索・抽出手順が理解でき、実務で即座に使えるスクリプトを自作できるようになります。背景には「配置が乱雑で手動検索が非効率」だったという経験があり、同様の課題を抱える読者の作業負担を軽減したいという思いで執筆しました。
前提知識
この記事を読み進める上で、以下の知識があるとスムーズです。
- 基本的な Linux コマンド(ls, cd, pwd など)の操作経験
- シェルスクリプトや Bash の基礎知識
- 任意程度のプログラミング経験(Python または Node.js)
背景と概要:なぜフォルダ階層を意識した抽出が必要か
大規模プロジェクトや長期間にわたる運用環境では、ログや設定ファイル、バイナリが階層的に保存されることが多く、同名ディレクトリ(例: assets や config)が複数階層に散在します。手作業でそれぞれのディレクトリに入り、cp や rsync でファイルを集めると、時間と労力が膨大になるだけでなく、抜け漏れのリスクも高まります。そこで 「任意の階層にある特定ディレクトリ名」 を検索し、その中身すべてを一括で抽出する手段が求められます。
本稿では、以下の3つのアプローチを紹介します。
- Unix 標準コマンド
findとcpの組み合わせ – 最もシンプルで依存関係が少ない方法。 - Python スクリプト – 条件付きコピーや除外パターンを柔軟に組み込める。
- Node.js(JavaScript)スクリプト – プロジェクトが既に Node 環境の場合に統合しやすい。
それぞれのメリット・デメリットを踏まえたうえで、実装例と注意点を詳述します。
具体的な手順や実装方法
1. find コマンドだけで抽出する方法
ステップ1‑1:対象ディレクトリ構造を確認
Bash$ tree /path/to/root | grep "target_dir_name"
target_dir_name には抽出したいフォルダ名(例: logs)を入れます。tree がインストールされていない場合は sudo apt install tree で取得できます。
ステップ1‑2:find で該当ディレクトリを列挙
Bash$ find /path/to/root -type d -name "target_dir_name"
このコマンドは、ルート以下の全階層から名前が完全一致するディレクトリを拾い上げます。
ステップ1‑3:結果をループでコピー
Bash#!/bin/bash ROOT="/path/to/root" DEST="/tmp/collected_files" TARGET="target_dir_name" mkdir -p "$DEST" find "$ROOT" -type d -name "$TARGET" | while read -r dir; do # サブディレクトリ構造を保持したままコピー rel_path="${dir#$ROOT/}" mkdir -p "$DEST/$rel_path" cp -a "$dir/"* "$DEST/$rel_path/" done
rel_pathは$ROOTからの相対パスを計算し、コピー先でも同じ階層構造を再現します。-aオプションで属性・シンボリックリンクもそのまま保持します。
ハマった点と解決策
-
空ディレクトリがコピーされない
cpはデフォルトで空ディレクトリを無視します。空フォルダも保持したい場合はrsync -a --include='*/' --exclude='*' "$dir/" "$DEST/$rel_path/"を代替します。 -
ファイル名に改行が含まれると
while readが壊れる
find -print0 | while IFS= read -r -d '' dirに変更すると NUL 区切りで安全に処理できます。
2. Python で柔軟に抽出する方法
Python の pathlib と shutil を利用すれば、除外パターンやフィルタリングを簡単に組み込めます。
ステップ2‑1:スクリプト全体像
Python#!/usr/bin/env python3 import sys from pathlib import Path import shutil def collect_files(root: Path, target_name: str, dest: Path, ignore_ext=None): """ root : 探索開始ディレクトリ target_name: 抽出対象のフォルダ名 dest : 結果を格納するディレクトリ ignore_ext: 除外したい拡張子のリスト(例: ['.tmp', '.log']) """ if ignore_ext is None: ignore_ext = [] for dir_path in root.rglob(target_name): if dir_path.is_dir(): rel = dir_path.relative_to(root) target_dest = dest / rel target_dest.mkdir(parents=True, exist_ok=True) for item in dir_path.iterdir(): if item.is_file() and item.suffix in ignore_ext: continue shutil.copy2(item, target_dest / item.name) if __name__ == "__main__": if len(sys.argv) != 4: print(f"Usage: {sys.argv[0]} <root_dir> <target_dir_name> <dest_dir>") sys.exit(1) root_dir = Path(sys.argv[1]).resolve() target_dir_name = sys.argv[2] dest_dir = Path(sys.argv[3]).resolve() collect_files(root_dir, target_dir_name, dest_dir, ignore_ext=['.tmp']) print("抽出完了:", dest_dir)
ステップ2‑2:実行例
Bash$ python3 collect.py /var/www mydata /tmp/result
ignore_extに['.tmp']を指定しているので、拡張子が.tmpのファイルは除外されます。shutil.copy2はメタデータ(mtime, atime, 権限)も保持します。
ハマりポイント
-
シンボリックリンクがコピー対象になる
shutil.copy2はリンク先実体をコピーしますが、リンクそのものが欲しい場合はshutil.copyの代わりにshutil.copytree(..., symlinks=True)を利用してください。 -
大量ファイルでメモリ使用量が増える
Path.rglobはジェネレータで遅延評価されるため基本的にメモリフットプリントは小さいですが、ループ内で一括リスト化すると逆効果です。forで直接処理するようにしましょう。
3. Node.js で実装するケース
Node.js の fs と path、そして便利な fast-glob パッケージを組み合わせると、非同期処理で高速に検索・コピーできます。
ステップ3‑1:必要パッケージのインストール
Bashnpm init -y npm install fast-glob fs-extra
fast-globは高速 glob パターンマッチング。fs-extraはcopyでディレクトリ構造保持が出来ます。
ステップ3‑2:スクリプト例
Js// collect.js const fg = require('fast-glob'); const fse = require('fs-extra'); const path = require('path'); async function collect(root, targetName, dest, ignorePatterns = []) { // fast-glob のパターンで「任意階層の targetName ディレクトリ」だけを取得 const pattern = `**/${targetName}`; const dirs = await fg(pattern, { cwd: root, onlyDirectories: true, ignore: ignorePatterns, absolute: true, }); for (const srcDir of dirs) { const rel = path.relative(root, srcDir); const destDir = path.join(dest, rel); await fse.ensureDir(destDir); // 内容全体をコピー(空ディレクトリも含む) await fse.copy(srcDir, destDir, { overwrite: true, errorOnExist: false, filter: (src) => { // .log 等除外したい拡張子があればここで判定 const ext = path.extname(src); return !['.tmp'].includes(ext); }, }); console.log(`Copied ${srcDir} → ${destDir}`); } } const [,, root, target, dest] = process.argv; if (!root || !target || !dest) { console.error('Usage: node collect.js <root> <target_dir_name> <dest>'); process.exit(1); } collect(path.resolve(root), target, path.resolve(dest)) .then(() => console.log('All done!')) .catch(err => console.error('Error:', err));
ステップ3‑3:実行例
Bashnode collect.js /opt/project logs ./collected
エラーと対処
ENOTEMPTYエラー – 既に同名ファイルが存在する場合はoverwrite: trueを指定していますが、別プロジェクトで同時実行すると競合が起きることがあります。その際はawait fse.emptyDir(destDir)で事前にクリアすると安全です。- パーミッションエラー – 実行ユーザーが対象ディレクトリにアクセスできない場合は
sudoで実行するか、対象ディレクトリの権限を確認してください。
まとめ
本記事では、任意の階層に散在する特定ディレクトリ名を検索し、その中のすべてのファイルを一括抽出する方法 を3つのアプローチで解説しました。
find+ Bash:最小依存で高速に実現。空ディレクトリや特殊文字に注意。- Python:条件付きコピーや除外フィルタを柔軟に設定可能。スクリプトで再利用しやすい。
- Node.js:非同期処理で大量ファイルでも高速、プロジェクトにJavaScriptがある場合に統合が容易。
これらを活用すれば、手動検索の手間を大幅に削減でき、作業の正確性と効率が向上します。次回は、抽出したファイルを自動で圧縮・転送するパイプライン構築について解説する予定です。
参考資料
- GNU findutils マニュアル
- Python shutil — 高レベルファイル操作
- fast-glob GitHub リポジトリ
- fs-extra – Node.js の拡張ファイルシステムモジュール
