株式会社Good Labでエンジニアをしている コータロー です。
日々、Java・SQL・Gitなどの技術情報や、新人エンジニア向けの学習ノウハウ、
AI活用についての情報を発信しています。
Good Labについて気になった方は、コーポレートサイトもぜひご覧ください。
▶コーポレートサイト
本記事は 「PowerShell 業務自動化レシピ」 シリーズの第1回です。このシリーズは、文法解説ではなく 「1記事=1つの業務課題」を、コピペで動く検証済みスクリプトで解決することを目的とした実務レシピ集です(全12回想定)。SES・受託・社内運用の現場で、いまだに手作業でやっている地味な作業を片っ端から自動化していきます。
第1回のテーマは 「大量ファイルの一括リネーム」 と 「拡張子別の自動フォルダ振り分け」。共有フォルダやダウンロードフォルダに溜まったファイルを手で整理した経験、誰しもあるはずです。
本記事は PowerShell 7(pwsh、クロスプラットフォーム) を基準にしています。Windows標準の Windows PowerShell 5.1 での差異は、各所と最後の章で明記します。
Before:手作業のつらいシーン
シーン1:命名規則がバラバラなファイル群
クライアントから受領したスクショ・帳票が スクリーンショット 2026-06-01 13.42.00.png IMG_2049.JPG 無題.png …のようにバラバラ。納品ルールは「報告書_001.png から連番」。
- 1枚ずつ右クリック→名前の変更→打ち込み
- 100枚あったら…体感 30〜40分、しかも打ち間違いのリスクつき
シーン2:拡張子が混在したフォルダ
共有フォルダに .xlsx .pdf .png .zip が数百個フラットに散乱。「PDFだけ集めて」と言われ、エクスプローラーで拡張子ソート→Ctrl+クリックで選択→ドラッグ。
- 種類が増えるたびにフォルダを作って移動…20〜30分
- 選択ミスで関係ないファイルまで動かす事故も
これ、全部スクリプト一本で終わります。
After:スクリプト一本で完了
レシピ1:連番で一括リネーム
C:\work\images(macOS/Linuxなら ~/work/images)の中のファイルを、更新日時順に 報告書_001.png, 報告書_002.png …とリネームします。
# ===== 設定 =====
$targetDir = "C:\work\images" # 対象フォルダ
$prefix = "報告書_" # 付けたい接頭辞
$startNo = 1 # 連番の開始番号
$digits = 3 # 桁数(3なら 001, 002, ...)
# ===== 本体 =====
$i = $startNo
Get-ChildItem -Path $targetDir -File |
Sort-Object LastWriteTime |
ForEach-Object {
$newName = "{0}{1:D$digits}{2}" -f $prefix, $i, $_.Extension
Rename-Item -LiteralPath $_.FullName -NewName $newName -WhatIf
$i++
}
.Extension には先頭のドット込みで .png が入るので、報告書_001.png のように元の拡張子を保ったままリネームできます。
いきなり本番実行せず、まず上の
-WhatIf付き で動かしてください。実際にはリネームせず「何がどう変わるか」だけを表示します。出力例(PowerShell 7):
What if: 対象 "アイテム: C:\work\images\IMG_2049.JPG 宛先: C:\work\images\報告書_001.JPG" に対して操作 "ファイル名の変更" を実行します。
What if: 対象 "アイテム: C:\work\images\無題.png 宛先: C:\work\images\報告書_002.png" に対して操作 "ファイル名の変更" を実行します。
意図通りなら、コードの -WhatIf を 削除して もう一度実行すれば実際にリネームされます。
レシピ2:拡張子別に自動フォルダ振り分け
C:\work\inbox の中のファイルを、拡張子ごとに pdf xlsx png …というサブフォルダを自動作成して振り分けます。
# ===== 設定 =====
$targetDir = "C:\work\inbox" # 振り分け対象フォルダ
# ===== 本体 =====
Get-ChildItem -Path $targetDir -File |
Group-Object { $_.Extension.TrimStart('.').ToLower() } |
ForEach-Object {
# 拡張子なしファイルは "_noext" フォルダへ
$folderName = if ($_.Name) { $_.Name } else { "_noext" }
$destDir = Join-Path $targetDir $folderName
if (-not (Test-Path -LiteralPath $destDir)) {
New-Item -ItemType Directory -Path $destDir | Out-Null
}
$_.Group | ForEach-Object {
Move-Item -LiteralPath $_.FullName -Destination $destDir -WhatIf
}
}
実行すると、こうなります。
C:\work\inbox\
├─ pdf\ ← *.pdf がまとめて移動
├─ xlsx\
├─ png\
├─ zip\
└─ _noext\ ← 拡張子なしファイル
こちらも まず -WhatIf 付きで確認 → 問題なければ -WhatIf を外して本実行 の順番が鉄則です。
補足:
-WhatIfが付いているのはMove-Item(ファイル移動)だけなので、ドライラン中でも振り分け先フォルダ(pdf\xlsx\など)は実際に作成されます(New-Itemには-WhatIfを付けていないため)。中身が空のフォルダができますが、本実行すればそこにファイルが入ります。気になる場合は、確認段階ではNew-Itemの行も一時的にコメントアウトしてください。
コードの要点解説(なぜこの書き方か)
文法そのものではなく、現場で事故らないための選択を中心に解説します。
1. -LiteralPath を使っている理由
-Path はワイルドカード(* ? [ ])を解釈します。ファイル名に [2026] のような角カッコが入っていると、-Path では「存在しない」と誤判定されてスルーされることがあります。
ファイル名をそのまま文字列として扱いたいリネーム・移動では、-LiteralPath が安全です。Get-ChildItem が返す各オブジェクトを処理する場面では基本これを使います。
2. Sort-Object LastWriteTime で順序を固定する
連番を振るとき、並び順が不定だと「どのファイルが001になるか」が運任せになります。
更新日時順なら再現性があり、撮影・出力された順に番号が振られて直感的です。名前順にしたいなら Sort-Object Name、作成日時順なら Sort-Object CreationTime に変えるだけです。
3. {0:D$digits} での桁揃え
-f(書式演算子)の D3 は「3桁のゼロ埋め整数」を意味します。5 → 005、42 → 042。
ファイル名は文字列ソートされるため、ゼロ埋めしないと 1, 10, 2, 3 ... という気持ち悪い並びになります。桁揃えは地味ですが必須です。
4. Group-Object { ... } で拡張子ごとにまとめる
Group-Object にスクリプトブロックを渡すと、その評価結果でグルーピングできます。
$_.Extension.TrimStart('.') で先頭ドットを除去し、.ToLower() で大文字小文字を統一。これで .PDF と .pdf が 同じ pdf フォルダにまとまります。Windowsはファイル名の大文字小文字を区別しないため、この正規化をしておかないと環境によってフォルダが分裂します。
5. 拡張子なしファイルの扱い
.Extension は拡張子なしファイルだと 空文字列 を返します。そのままだとフォルダ名が空になりエラーになるため、if ($_.Name) { ... } else { "_noext" } で受け皿フォルダに逃がしています。地味ですが、現場のフォルダには必ず「拡張子なしの謎ファイル」が紛れています。
応用・注意点
① まず必ず -WhatIf でドライラン
リネーム・移動は 破壊的操作 です。本記事のスクリプトには最初からすべて -WhatIf を付けてあります。
-
-WhatIf付き … 実際には変更せず、何が起きるかだけ表示 -
-WhatIfを外す … 本実行
慣れるまでは「-WhatIf で確認 → 外して実行」を必ずワンセットにしてください。一括操作は、間違うと一括で壊れます。
さらに慎重にいくなら、操作前にバックアップを取るのが確実です。
# 操作前にフォルダごとコピーしてバックアップ
Copy-Item -Path "C:\work\inbox" -Destination "C:\work\inbox_backup" -Recurse
② 1件ずつ確認しながら実行したいとき
-WhatIf の代わりに -Confirm を付けると、1件ごとに「実行しますか?(Y/N)」と聞いてきます。「だいたい合ってるけど数件だけ除外したい」ときに便利です。
Move-Item -LiteralPath $_.FullName -Destination $destDir -Confirm
③ リネーム・移動先が衝突したらどうなる?
移動先に同名ファイルが既に存在する場合、Move-Item / Rename-Item は エラーになって、そのファイルだけ処理がスキップされます(既存ファイルが上書きされて消えることはありません)。
意図的に上書きしたい場合のみ、Move-Item に -Force を付けます。-Force は既存ファイルを問答無用で上書きするので、付けるときは本当に上書きしてよいか確認してください。
④ サブフォルダの中まで対象にしたいとき
Get-ChildItem に -Recurse を付けると配下のサブフォルダまで再帰的に拾います。ただしレシピ2(振り分け)で -Recurse を使うと、自分が作った振り分け先フォルダの中身まで巻き込んで再処理する事故が起きがちです。振り分けは原則フラット(再帰なし)で運用してください。
⑤ Windows PowerShell 5.1 との差異
本記事のスクリプトは、コアな部分は 5.1 でもそのまま動作します。ただし以下に注意してください。
| 項目 | PowerShell 7(pwsh) | Windows PowerShell 5.1 |
|---|---|---|
| 既定の文字コード | UTF-8 | レガシー(多くがShift_JIS/ANSI) |
| 日本語ファイル名・コンソール表示 | 概ね安定 | コンソールやスクリプト保存の文字コード次第で文字化けすることがある |
| 推奨 | こちらを推奨 | スクリプトファイルは UTF-8 (BOM付き) で保存すると文字化けを避けやすい |
5.1 で日本語接頭辞(例:報告書_)を使う場合は、スクリプト保存時のエンコードに特に注意してください。迷ったら、可能な環境では PowerShell 7 を入れて使うのが安全です。
まとめ
- 一括リネームは
Get-ChildItem→Sort-Object→Rename-Item、桁揃えは{0:D3} - 拡張子振り分けは
Group-Objectで拡張子を正規化 →Move-Item - 破壊的操作は 必ず
-WhatIfでドライラン、心配ならCopy-Itemでバックアップ - ファイル名はワイルドカード事故を避けるため
-LiteralPath
これだけで、毎回30分かけていた整理作業が数秒になります。
次回 第2回は「古いログファイルの抽出・自動削除」。Get-ChildItem の LastWriteTime フィルタで「N日より古いログだけ」を安全に消すレシピを扱います。お楽しみに。
参考
- Get-ChildItem (Microsoft.PowerShell.Management) - Microsoft Learn
- Rename-Item (Microsoft.PowerShell.Management) - Microsoft Learn
- Move-Item (Microsoft.PowerShell.Management) - Microsoft Learn
- Group-Object (Microsoft.PowerShell.Utility) - Microsoft Learn
- about_WhatIf_Preference (PowerShell) - Microsoft Learn
- about_Format (-f 演算子 / 複合書式指定) - Microsoft Learn
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!