「変更前のフォルダ」と「変更後のフォルダ」を比べて、追加・削除・変更されたファイルを洗い出したい。PowerShell なら、各ファイルのハッシュ値を計算して比べるのが確実である。ファイル名や更新日時で比べるやり方もよく紹介されているが、それでは中身の変更を見落とす(理由は後半で実際に試した結果を載せる)。
私は仕事で、事前に用意した資材(設定ファイルなど)が知らない間に変わっていないか、ハッシュ値で確かめている(1つのフォルダのハッシュ値を一覧にする方法はGet-FileHash でフォルダ内のハッシュ値を一括で CSV 出力する記事で紹介した)。この記事では、その一覧同士を比べて差分を出すところまでをまとめる。
2つのフォルダを比較するスクリプト
先頭の2行を、比べたいフォルダのパスに書き換える。$before が変更前のフォルダ、$after が変更後のフォルダである。あとは PowerShell に貼り付けて実行するだけで、管理者権限は不要である。
スクリプトは3つの部分に分かれている。Get-HashList でフォルダ配下の全ファイルの「相対パス」と「ハッシュ値」の一覧を作り、Compare-Object で2つの一覧を比べ、最後に結果を「追加・削除・変更」に読み替えて表示している。
$before = "C:\work\before"
$after = "C:\work\after"
function Get-HashList($root) {
$root = (Resolve-Path $root).Path
Get-ChildItem -Path $root -Recurse -File | ForEach-Object {
[PSCustomObject]@{
RelativePath = $_.FullName.Remove(0, $root.Length).TrimStart('\')
Hash = (Get-FileHash -Path $_.FullName -Algorithm SHA256).Hash
}
}
}
$a = Get-HashList $before
$b = Get-HashList $after
$diff = Compare-Object -ReferenceObject $a -DifferenceObject $b -Property RelativePath, Hash
$result = $diff | Group-Object RelativePath | ForEach-Object {
$sides = $_.Group.SideIndicator
if ($sides -contains '<=' -and $sides -contains '=>') { $state = '変更' }
elseif ($sides -contains '<=') { $state = '削除' }
else { $state = '追加' }
[PSCustomObject]@{ 状態 = $state; ファイル = $_.Name }
}
$result | Sort-Object ファイル | Format-Table -AutoSize
実行すると、違いのあったファイルだけが次のように表示される(後半のテスト用フォルダで実行した結果)。
状態 ファイル ---- -------- 追加 new.txt 削除 old.txt 変更 settings.ini
何も表示されなければ、2つのフォルダの中身は完全に同じである。
結果の見方
| 状態 | 意味 |
| 変更 | 両方のフォルダにあるが、中身が違う |
| 削除 | 変更前($before)にだけある |
| 追加 | 変更後($after)にだけある |
サブフォルダの中のファイルは、sub\data.csv のようにフォルダからの相対パスで表示される。相対パスで比べているので、2つのフォルダが別の場所にあっても、同じ位置にあるファイル同士が比べられる。
結果を CSV に保存する
記録に残したい場合は、続けて次を実行する。出力先のパスは好きな場所に書き換えてよい。
$result | Sort-Object ファイル | Export-Csv "C:\work\diff.csv" -NoTypeInformation -Encoding Default
"状態","ファイル" "追加","new.txt" "削除","old.txt" "変更","settings.ini"
-Encoding Default は、Windows PowerShell 5.1 では Shift-JIS になり、Excel で開いても文字化けしない。PowerShell 7 では意味が変わる(UTF-8 になる)ので、PowerShell 7 で Excel 向けに出すなら -Encoding utf8BOM を指定する。
ファイル名や更新日時で比べてはいけない理由(実際に試した)
フォルダの比較を調べると、Compare-Object でファイル名や更新日時を比べる方法がよく出てくる。手軽だが、中身が同じかどうかの判定には使えない。テスト用のフォルダを作って、実際に確かめた。
テスト用のフォルダ
「変更前(before)」と「変更後(after)」に、わざと次の違いを入れた。
| ファイル | before と after の違い |
| config.txt | 中身は同じ。更新日時だけ違う |
| settings.ini | 中身だけ違う(timeout=30 → timeout=60)。サイズと更新日時は同じ |
| old.txt | before にだけある(削除された) |
| new.txt | after にだけある(追加された) |
| sub\data.csv | まったく同じ |
同じ状態は次のコマンドで作れる(%TEMP% の下に作る)。自分の手元で試したい場合に使ってほしい。
$root = "$env:TEMP\hashtest"
New-Item "$root\before\sub", "$root\after\sub" -ItemType Directory -Force | Out-Null
Set-Content "$root\before\config.txt" "server=app01"
Set-Content "$root\after\config.txt" "server=app01"
Set-Content "$root\before\settings.ini" "timeout=30"
Set-Content "$root\after\settings.ini" "timeout=60"
Set-Content "$root\before\old.txt" "old file"
Set-Content "$root\after\new.txt" "new file"
Set-Content "$root\before\sub\data.csv" "id,name"
Set-Content "$root\after\sub\data.csv" "id,name"
Get-ChildItem $root -Recurse -File | ForEach-Object { $_.LastWriteTime = [datetime]'2026-11-01 10:00' }
(Get-Item "$root\after\config.txt").LastWriteTime = [datetime]'2026-11-02 10:00'
$before = "$root\before"
$after = "$root\after"
できたファイルは次のとおり。settings.ini は before も after もサイズ 12、更新日時も同じで、一覧を見ただけでは区別がつかない。
FullName Length LastWriteTime -------- ------ ------------- C:\Users\<User>\AppData\Local\Temp\hashtest\after\config.txt 14 2026/11/02 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\after\new.txt 10 2026/11/01 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\after\settings.ini 12 2026/11/01 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\after\sub\data.csv 9 2026/11/01 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\before\config.txt 14 2026/11/01 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\before\old.txt 10 2026/11/01 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\before\settings.ini 12 2026/11/01 10:00:00 C:\Users\<User>\AppData\Local\Temp\hashtest\before\sub\data.csv 9 2026/11/01 10:00:00
ファイル名で比べた場合: 中身の変更を見落とす
Compare-Object (Get-ChildItem $before -Recurse -File) (Get-ChildItem $after -Recurse -File) -Property Name
Name SideIndicator ---- ------------- new.txt => old.txt <=
追加と削除は分かるが、中身が変わった settings.ini は出てこない。名前が同じだからである。
名前・サイズ・更新日時で比べた場合: 誤判定と見落としが出る
Compare-Object (Get-ChildItem $before -Recurse -File) (Get-ChildItem $after -Recurse -File) -Property Name, Length, LastWriteTime
Name Length LastWriteTime SideIndicator ---- ------ ------------- ------------- config.txt 14 2026/11/02 10:00:00 => new.txt 10 2026/11/01 10:00:00 => config.txt 14 2026/11/01 10:00:00 <= old.txt 10 2026/11/01 10:00:00 <=
中身が同じ config.txt が「違う」と出て、本当に変わった settings.ini はやはり出てこない。更新日時は、開いて保存し直しただけでも、コピーの仕方によっても変わる。中身を書き換えてもサイズが同じなら、サイズでは気づけない。
ハッシュ値で比べた場合: 正しく判定できる
冒頭のスクリプトのうち、2つの一覧を比べたところまで($diff)の中身はこうなっていた。
RelativePath Hash SideIndicator ------------ ---- ------------- new.txt CDBA66F93B029372B55EF8112536AED0829A14D70B294C1A9E33A49ED4AD757A => settings.ini 8644FF8EFA542A1470E8CBFBF39AE90F6025E542F65D8A61FC1396630E635F26 => old.txt 9849BF57C781A24E5CDF242DFDE29370C69C0A5E3C83AB03A1E43FF4ABC55B4D <= settings.ini B75039373646B8C569AFFEFE71AA79F6E824CFC4D581F2BABA62C4FC037A624B <=
SideIndicator の <= は before にだけ、=> は after にだけある、という意味である。settings.ini はハッシュ値が違うので両側に1行ずつ出ている。更新日時だけ違う config.txt と、まったく同じ sub\data.csv は出てこない。冒頭のスクリプトは、これをファイルごとにまとめて「変更・削除・追加」に読み替えている。
試し終わったら、テスト用のフォルダは消しておく。
Remove-Item "$env:TEMP\hashtest" -Recurse -Force
使うときの注意
- ファイル数やサイズが大きいと時間がかかる。ハッシュ値は全ファイルを最後まで読んで計算するためだ。追加・削除だけ先に知りたいなら、ファイル名での比較で当たりを付けてから、中身の確認を必要なフォルダに絞るのも手である
- 変更前の一覧を残しておけば、後から比べられる。資材を配る前に
Get-HashListの結果を CSV に保存しておけば、後で今の状態と比べて、誰かが中身を書き換えていないか確かめられる - NAS へのバックアップが正しくコピーされたかの確認にも使える(バックアップの方法はRobocopy で NAS にバックアップする記事を参照)
- 今回試したのはローカルのフォルダである
- スクリプトを .ps1 ファイルに保存して実行するときに「このシステムではスクリプトの実行が無効になっているため」と出たら、PowerShell の実行ポリシーの記事を見てほしい
この記事のまとめ

- 2つのフォルダの中身を確実に比べるなら、相対パスとハッシュ値の一覧を作って Compare-Object で比べる
- 結果は、両側に出たら「変更」、変更前だけなら「削除」、変更後だけなら「追加」
- ファイル名の比較は中身の変更を見落とし、更新日時やサイズの比較は誤判定と見落としが出る

コメント