PowerShellでFileSystemWatcherを使ってファイル変更を検知する方法

PowerShellでFileSystemWatcherを使ってファイルの作成・変更・削除を自動検知する方法を解説するアイキャッチ画像

特定フォルダにファイルが増えたり書き換わったりしたとき、それを検知して何か処理を走らせたいと思ったことはありますよね。
ポーリング(一定間隔でフォルダをチェックし続ける)でも実現できますが、どうせやるならイベント駆動で効率よく動かしたいところです。

PowerShellには[System.IO.FileSystemWatcher]という.NETクラスが利用できて、ファイルの作成・変更・削除・リネームを通知ベースで検知で検知できます。
この記事ではその基本的な使い方から、実運用の参考になるスクリプト例まで解説します。

PowerShellの基本操作や実践コマンドをまとめて確認したい場合はこちらも参考にしてください。

目次

この記事でわかること

  • FileSystemWatcherの基本的な仕組み
  • ファイルの作成・変更・削除・リネームを検知する方法
  • イベント駆動(Register-ObjectEvent)で処理する方法
  • 実用的なスクリプト例(ログ出力・ファイル自動コピー)
  • 注意点とよくあるはまりどころ

FileSystemWatcherとは

System.IO.FileSystemWatcherは.NET Framework / .NETのクラスで、OSレベルのファイルシステム変更通知を受け取る仕組みです。
PowerShellは.NETクラスをそのまま利用できるので、追加インストール不要でスクリプト内から使えますよ。

FileSystemWatcher Class (System.IO) | Microsoft Learndocs

監視できるイベントは次の4種類です。

イベント名内容
Createdファイル・フォルダが作成された
Changedファイルの中身、属性、サイズ、最終書き込み時刻などが変更された
※Changedは環境や保存方法によって複数回発火しやすいようです。
Deletedファイル・フォルダが削除された
Renamedファイル・フォルダ名が変更された

サンプル

Step
FileSystemWatcherオブジェクトを作る

まずウォッチャー本体を作成して、監視対象のフォルダや条件を設定します。

PowerShell – watcherの基本設定
$watcher = New-Object System.IO.FileSystemWatcher

# 監視するフォルダのパス
$watcher.Path = "C:\Logs"

# 監視対象のファイル名フィルター(例: *.log で log ファイルに限定)
$watcher.Filter = "*.*"

# サブフォルダも監視するか
$watcher.IncludeSubdirectories = $false

# 何の変化を検知するか(NotifyFilters)
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite `
    -bor [System.IO.NotifyFilters]::FileName

設定項目の解説です。

  • Path
    監視したいフォルダのパス。ファイル単体の指定はできないのでフォルダを指定します。
  • Filter
    監視対象のファイル名フィルター。*.log のように拡張子を絞り込むことも可能です。
  • IncludeSubdirectories
    $true にするとサブフォルダ内の変更も検知します。デフォルトは$falseでサブフォルダは検知されません。
  • NotifyFilter
    LastWrite(最終書き込み時刻)、FileName(ファイル名)、DirectoryName(フォルダ名)、Sizeなど複数を-borで組み合わせられます。
Step
イベントを登録する(Register-ObjectEvent)

ウォッチャーを作ったら、各イベントに処理(アクション)を登録します。
Register-ObjectEventを使うと、イベントが発生したときにスクリプトブロックが非同期で実行されます。

PowerShell – イベント登録とアクション定義
# 変更検知時の処理(Created / Changed / Deleted 共通)
$action = {
    $info = $Event.SourceEventArgs
    $path = $info.FullPath
    $type = $info.ChangeType
    $time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")

    Write-Host "[$time] $type : $path"
}

Register-ObjectEvent -InputObject $watcher -EventName "Created" -Action $action -SourceIdentifier "Watcher_Created"
Register-ObjectEvent -InputObject $watcher -EventName "Changed" -Action $action -SourceIdentifier "Watcher_Changed"
Register-ObjectEvent -InputObject $watcher -EventName "Deleted" -Action $action -SourceIdentifier "Watcher_Deleted"

# リネームは専用のイベント引数を使う
$renameAction = {
    $info = $Event.SourceEventArgs
    $oldPath = $info.OldFullPath
    $newPath = $info.FullPath
    $time = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")

    Write-Host "[$time] Renamed : $oldPath -> $newPath"
}

Register-ObjectEvent -InputObject $watcher -EventName "Renamed" -Action $renameAction -SourceIdentifier "Watcher_Renamed"

# 監視を開始
$watcher.EnableRaisingEvents = $true

Write-Host "モニタリング開始:$watcher.Path"
Write-Host "Ctrl+C で停止できます。停止時はイベント解除と Dispose() が実行されます。"

# スクリプトを終了させないように待機
while ($true) { Start-Sleep -Seconds 1 }
  • Register-ObjectEventで各イベントに処理を登録します。
  • RenamedOldFullPathFullPathを使って変更前後のパスを取得できます。
  • EnableRaisingEvents = $true にするとウォッチャーがイベント発火を開始します。
  • while ($true) { Start-Sleep -Seconds 1 }の箇所は次のステップで少し改良します。
Step
終了時にイベントを解除する

スクリプトを止めるときはイベントの登録を解除しておくとリソースが解放されてきれいです。

PowerShell
try {
    # スクリプトを終了させないように待機
    while ($true) { Start-Sleep -Seconds 1 }
}
finally {
    # イベントの登録を解除
    Unregister-Event -SourceIdentifier "Watcher_Created" -ErrorAction SilentlyContinue
    Unregister-Event -SourceIdentifier "Watcher_Changed" -ErrorAction SilentlyContinue
    Unregister-Event -SourceIdentifier "Watcher_Deleted" -ErrorAction SilentlyContinue
    Unregister-Event -SourceIdentifier "Watcher_Renamed" -ErrorAction SilentlyContinue

    # 監視を停止してからオブジェクトを破棄
    $watcher.EnableRaisingEvents = $false
    $watcher.Dispose()
    
    Write-Host "モニタリング終了"
}
サンプル(分割なしフルコード)
PowerShell
$watcher = New-Object System.IO.FileSystemWatcher

# 監視するフォルダのパス(記事の C:\Logs をそのまま使用)
$watcher.Path = "C:\Logs"

# 監視対象のファイル名フィルター(*.*で全ファイル)
$watcher.Filter = "*.*"

# サブフォルダも監視するか
$watcher.IncludeSubdirectories = $false

# 何の変化を検知するか(NotifyFilters)
$watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite `
    -bor [System.IO.NotifyFilters]::FileName

# 変更検知時の処理(Created / Changed / Deleted 共通)
$action = {
    $info   = $Event.SourceEventArgs
    $path   = $info.FullPath
    $type   = $info.ChangeType
    $time   = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")

    Write-Host "[$time] $type : $path"
}

Register-ObjectEvent -InputObject $watcher -EventName "Created" -Action $action -SourceIdentifier "Watcher_Created"
Register-ObjectEvent -InputObject $watcher -EventName "Changed" -Action $action -SourceIdentifier "Watcher_Changed"
Register-ObjectEvent -InputObject $watcher -EventName "Deleted" -Action $action -SourceIdentifier "Watcher_Deleted"

# リネームは SourceEventArgs が異なる(OldFullPath と FullPath を使用する)
$renameAction = {
    $info    = $Event.SourceEventArgs
    $oldPath = $info.OldFullPath
    $newPath = $info.FullPath
    $time    = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")

    Write-Host "[$time] Renamed : $oldPath -> $newPath"
}

Register-ObjectEvent -InputObject $watcher -EventName "Renamed" -Action $renameAction -SourceIdentifier "Watcher_Renamed"

# 監視を開始する(EnableRaisingEvents = $true にしないとイベントが発火しない)
$watcher.EnableRaisingEvents = $true

Write-Host "モニタリング開始:$watcher.Path"
Write-Host "Ctrl+C で停止できます。停止時はイベント解除と Dispose() が実行されます。"

try {
    # スクリプトを終了させないように待機
    while ($true) { Start-Sleep -Seconds 1 }
}
finally {
    # イベントの登録を解除
    Unregister-Event -SourceIdentifier "Watcher_Created" -ErrorAction SilentlyContinue
    Unregister-Event -SourceIdentifier "Watcher_Changed" -ErrorAction SilentlyContinue
    Unregister-Event -SourceIdentifier "Watcher_Deleted" -ErrorAction SilentlyContinue
    Unregister-Event -SourceIdentifier "Watcher_Renamed" -ErrorAction SilentlyContinue

    # 監視を停止してからオブジェクトを破棄
    $watcher.EnableRaisingEvents = $false
    $watcher.Dispose()
    
    Write-Host "モニタリング終了"
}

サンプルの実行結果

上記のサンプルのPowerShellを実行してからC:\Logsディレクトリで下記を行った時の出力です。

  • エクスプローラー上で「右クリック > 新規作成 > テキストドキュメント」を選択。
     →新規 テキスト ドキュメント.txtが出来上がってリネームで新規 テキスト ドキュメントが選択されている状態。
  • そのままtestと入力してtest.txtでファイル名を確定。
  • test.txtを開いて「Test Write」と入力して保存。
  • 保存後test.txtを削除。
PowerShell – 事項結果
Id     Name            PSJobTypeName   State         HasMoreData     Location             Command
--     ----            -------------   -----         -----------     --------             -------
1      Watcher_Created                 NotStarted    False                                …
2      Watcher_Changed                 NotStarted    False                                …
3      Watcher_Deleted                 NotStarted    False                                …
4      Watcher_Renamed                 NotStarted    False                                …
モニタリング開始:System.IO.FileSystemWatcher.Path
Ctrl+C で停止できます。停止時はイベント解除と Dispose() が実行されます。
[2026-05-16 16:39:00] Created : C:\Logs\新規 テキスト ドキュメント.txt
[2026-05-16 16:39:03] Renamed : C:\Logs\新規 テキスト ドキュメント.txt -> D:\Logs\test.txt
[2026-05-16 16:39:14] Changed : C:\Logs\test.txt
[2026-05-16 16:40:05] Deleted : C:\Logs\test.txt
モニタリング終了

ps1ファイルとして保存して実行できない場合は、PowerShellの実行ポリシーも確認してみてください。

実用サンプル①: 変更ログをファイルに記録する

コンソールに出力するだけでなく、ログファイルに追記する例です。

PowerShell – ファイル変更ログ記録スクリプト
$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path                  = "C:\Logs"
$watcher.Filter                = "*.log"
$watcher.IncludeSubdirectories = $false
$watcher.NotifyFilter          = [System.IO.NotifyFilters]::LastWrite `
    -bor [System.IO.NotifyFilters]::FileName

$logFile = "C:\Temp\watch_log.txt"

$action = {
    $info  = $event.SourceEventArgs
    $path  = $info.FullPath
    $type  = $info.ChangeType
    $time  = (Get-Date).ToString("yyyy-MM-dd HH:mm:ss")
    $entry = "[$time] $type : $path"

    Add-Content -Path $using:logFile -Value $entry
    Write-Host $entry
}

Register-ObjectEvent $watcher "Created" -Action $action -SourceIdentifier "WL_Created"
Register-ObjectEvent $watcher "Changed" -Action $action -SourceIdentifier "WL_Changed"
Register-ObjectEvent $watcher "Deleted" -Action $action -SourceIdentifier "WL_Deleted"

$watcher.EnableRaisingEvents = $true
Write-Host "監視中... ログ出力先: $logFile"
while ($true) { Start-Sleep -Seconds 1 }

スクリプトブロック内で外側の変数を参照するときは$using:変数名を使います。これを忘れると変数が$nullになってはまりがちです。

実用サンプル②: ファイル作成を検知して自動コピーする

特定フォルダに新規ファイルが置かれたら別フォルダへ自動コピーする例です。
取り込みフォルダの監視などで使えます。

PowerShell – ファイル自動コピースクリプト
$srcFolder = "C:\Drop"
$dstFolder = "C:\Processed"

$watcher = New-Object System.IO.FileSystemWatcher
$watcher.Path                  = $srcFolder
$watcher.Filter                = "*.*"
$watcher.IncludeSubdirectories = $false
$watcher.NotifyFilter          = [System.IO.NotifyFilters]::FileName

$action = {
    $info    = $event.SourceEventArgs
    $srcPath = $info.FullPath
    $dst     = $using:dstFolder
    $name    = $info.Name

    # ファイルが書き込み完了するまで少し待つ
    Start-Sleep -Milliseconds 500

    try {
        Copy-Item -Path $srcPath -Destination "$dst\$name" -Force
        Write-Host "コピー完了: $name"
    } catch {
        Write-Host "コピー失敗: $name - $_"
    }
}

Register-ObjectEvent $watcher "Created" -Action $action -SourceIdentifier "AC_Created"

$watcher.EnableRaisingEvents = $true
Write-Host "自動コピー監視中... $srcFolder -> $dstFolder"
while ($true) { Start-Sleep -Seconds 1 }

ファイル作成直後はまだ書き込み中のことがあります。500ミリ秒など短時間待ってから処理すると失敗を減らせますが、ファイルサイズによっては待ち時間の調整が必要です。

注意点

⚠️ 多数の変更が短時間に発生すると、内部バッファがあふれてイベントを取りこぼすことがあります。
FileSystemWatcherには内部バッファがあり、イベントが大量発生するとあふれてErrorイベントが発火します。ハンドリングの例です。

PowerShell
Register-ObjectEvent $watcher Error -Action {
    Write-Warning "監視エラーが発生しました"
}


$watcher.InternalBufferSize = 65536のようにバッファを大きくすることで多少は緩和できます(デフォルトは 8192)。

その他のはまりポイントをまとめておきます。

  • EnableRaisingEvents = $trueを忘れるとイベントが一切発火しない。
  • Changed イベントは1回の保存で複数回発火することがある。(エディタが一時ファイルを経由して保存するため)
  • スクリプト終了時にDispose()しないと、プロセスが残りフォルダのロックが続く場合がある。
  • スクリプトブロック内で外部変数を使うときは$using:変数名

Q&A

PowerShell を閉じても監視を続けたい場合は?

タスクスケジューラ等でスクリプトをバックグラウンド実行するか、Windowsサービス化するのが現実的です。手軽に試すならStart-Process powershell -WindowStyle Hidden -ArgumentList "-File C:\scripts\watcher.ps1"で非表示ウィンドウで起動する方法もあります。

常駐・定期実行の方法を選びたい場合は、タスクスケジューラー・PowerShell・WSL cronの使い分けも確認しておくと安心です。

特定の拡張子だけ監視したい。

$watcher.Filter = "*.csv"のようにフィルターで絞れます。
複数の拡張子を対象にしたい場合はFilter = "*.*"にしてアクション内でif ($info.Name -match "\.csv$|\.txt$")のように絞り込むとよいです。

サブフォルダも含めて監視したい。

$watcher.IncludeSubdirectories = $trueにするだけです。ただし監視範囲が広がるのでイベント数が増え、バッファあふれのリスクも上がります。

管理者権限は必要?

監視するフォルダへの読み取り権限があれば基本的には動きます。
C:\Windows など保護されたフォルダを対象にする場合は管理者として実行してください

まとめ

  • PowerShellは.NETのFileSystemWatcherクラスをそのまま使えるよ。
  • Register-ObjectEventでイベント駆動の非同期処理が書けるよ。
  • EnableRaisingEvents = $trueを忘れると何も起きないよ。
  • スクリプトブロック内の外部変数は$using:変数で参照してね。
  • 終了時はUnregister-EventDispose()で後片付けしておくのが安全だよ。

ポーリングより負荷が低く、リアルタイム性もそれなりに高いのがFileSystemWatcherの便利なところですね!
業務の自動化スクリプトにぜひ組み込んでみてください。

ログファイルの追記を確認したいだけなら、Get-Content -Waitを使う方法もあります。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!
目次