Last active
March 2, 2026 15:31
-
-
Save steviecoaster/fc7b98a155b6f75476be850436a12b33 to your computer and use it in GitHub Desktop.
Implement a FileSystemWatcher in PowerShell
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function Wait-File { | |
| <# | |
| .SYNOPSIS | |
| Monitors a file system path for changes and executes a script block in response to specified events. | |
| .DESCRIPTION | |
| The Wait-File function creates a FileSystemWatcher to monitor a specified directory for file system changes. | |
| It registers event handlers for specified events (Created, Changed, Deleted, Renamed) and executes a | |
| custom action when those events occur. The function runs continuously until manually stopped (Ctrl+C) | |
| or until an exit condition is satisfied (see -ExitCondition or -Once). | |
| .PARAMETER Path | |
| Specifies the path to the directory to monitor. The path must exist. | |
| .PARAMETER Filter | |
| Specifies the type of files to watch. Use wildcards to watch multiple file types. | |
| Examples: "*.txt", "*.log", "*.*" | |
| .PARAMETER IncludeSubdirectories | |
| When specified, monitors subdirectories within the specified path. | |
| .PARAMETER AttributeFilter | |
| Specifies the type of changes to monitor. Valid values include: | |
| - FileName: Watches for file name changes | |
| - DirectoryName: Watches for directory name changes | |
| - Attributes: Watches for attribute changes | |
| - Size: Watches for file size changes | |
| - LastWrite: Watches for last write time changes | |
| - LastAccess: Watches for last access time changes | |
| - CreationTime: Watches for creation time changes | |
| - Security: Watches for security changes | |
| .PARAMETER RegisterEvent | |
| Specifies which file system events to monitor along with their corresponding actions. | |
| Each hashtable must contain: | |
| - EventName: The event type to monitor ('Changed', 'Created', 'Deleted', or 'Renamed') | |
| - Action: A script block to execute when the event occurs | |
| The script block receives the event arguments automatically through $Event. | |
| .PARAMETER ExitCondition | |
| A script block that is evaluated whenever an event fires. If the block returns | |
| $true the monitoring loop will stop and Wait-File will return. The script block | |
| has access to the automatic `$Event` variable. | |
| .PARAMETER Once | |
| When specified, the watcher stops after the first event is handled (equivalent | |
| to an ExitCondition script block that always returns true). | |
| .EXAMPLE | |
| $events = @( | |
| @{ EventName = 'Changed'; Action = { Write-Host "File changed: $($Event.SourceEventArgs.Name)" } } | |
| ) | |
| Wait-File -Path "C:\Logs" -Filter "*.log" -AttributeFilter ([System.IO.NotifyFilters]::LastWrite) -RegisterEvent $events | |
| Monitors the C:\Logs directory for changes to .log files and displays a message when a file is modified. | |
| .EXAMPLE | |
| $events = @( | |
| @{ EventName = 'Created'; Action = { Write-Host "[CREATED] $($Event.SourceEventArgs.Name) at $(Get-Date)" } } | |
| @{ EventName = 'Deleted'; Action = { Write-Host "[DELETED] $($Event.SourceEventArgs.Name) at $(Get-Date)" } } | |
| ) | |
| Wait-File -Path "C:\Data" -Filter "*.*" -IncludeSubdirectories -AttributeFilter ([System.IO.NotifyFilters]::FileName) -RegisterEvent $events | |
| Monitors C:\Data and all subdirectories for file creation and deletion events with different actions for each event type. | |
| .EXAMPLE | |
| $events = @( | |
| @{ EventName = 'Created'; Action = { Write-Host "Package arrived: $($Event.SourceEventArgs.Name)" } } | |
| ) | |
| Wait-File -Path "C:\Packages" -Filter "*.zip" -AttributeFilter ([System.IO.NotifyFilters]::FileName) -RegisterEvent $events -Once | |
| Monitors C:\Packages for the first .zip file to be created, prints its name, then automatically stops monitoring. | |
| .EXAMPLE | |
| $events = @( | |
| @{ EventName = 'Created'; Action = { Write-Host "File ready: $($Event.SourceEventArgs.Name)" } } | |
| ) | |
| Wait-File -Path "C:\Output" -Filter "report.csv" -AttributeFilter ([System.IO.NotifyFilters]::FileName) -RegisterEvent $events -ExitCondition { $Event.SourceEventArgs.Name -eq 'report.csv' } | |
| Monitors C:\Output and stops automatically only when a file named 'report.csv' is created, ignoring any other creations. | |
| .NOTES | |
| - Press Ctrl+C to stop monitoring (unless an exit condition is used) | |
| - The function runs indefinitely until interrupted or an exit condition/Once flag | |
| causes it to exit automatically | |
| - Multiple events can be registered simultaneously | |
| - Event handlers are automatically cleaned up when the function exits | |
| .LINK | |
| Register-ObjectEvent | |
| System.IO.FileSystemWatcher | |
| #> | |
| [CmdletBinding()] | |
| Param( | |
| [Parameter(Mandatory)] | |
| [ValidateScript({ Test-Path $_ })] | |
| [String] | |
| $Path, | |
| [Parameter(Mandatory)] | |
| [String] | |
| $Filter, | |
| [Parameter()] | |
| [Switch] | |
| $IncludeSubdirectories, | |
| [Parameter(Mandatory)] | |
| [System.IO.NotifyFilters[]] | |
| $AttributeFilter, | |
| [Parameter(Mandatory)] | |
| [ValidateScript({ | |
| foreach ($item in $_) { | |
| if ($item -isnot [Hashtable]) { | |
| throw "Each item in RegisterEvent must be a hashtable" | |
| } | |
| if (-not $item.ContainsKey('EventName')) { | |
| throw "Each hashtable must contain an 'EventName' key" | |
| } | |
| if ($item.EventName -notin @('Changed','Created','Deleted','Renamed')) { | |
| throw "EventName must be one of: Changed, Created, Deleted, Renamed" | |
| } | |
| if (-not $item.ContainsKey('Action')) { | |
| throw "Each hashtable must contain an 'Action' key" | |
| } | |
| if ($item.Action -isnot [ScriptBlock]) { | |
| throw "Action must be a ScriptBlock" | |
| } | |
| } | |
| return $true | |
| })] | |
| [Hashtable[]] | |
| $RegisterEvent, | |
| [Parameter()] | |
| [ScriptBlock] | |
| $ExitCondition, | |
| [Parameter()] | |
| [Switch] | |
| $Once | |
| ) | |
| end { | |
| # Shared hashtable used as a signal between the event handler runspace and | |
| # this runspace. Because both sides hold a reference to the same object, | |
| # the handler can set Stop = $true and the main loop will see it. | |
| $signal = @{ Stop = $false } | |
| try { | |
| $fsw = [System.IO.FileSystemWatcher]::new($Path, $Filter) | |
| $fsw.IncludeSubdirectories = $IncludeSubdirectories | |
| $fsw.NotifyFilter = $AttributeFilter | |
| $handlers = [System.Collections.Generic.List[psobject]]::New() | |
| foreach($re in $RegisterEvent){ | |
| # Bundle everything the handler needs into MessageData so that | |
| # $using: (which doesn't work across Register-ObjectEvent runspaces) | |
| # is not required. | |
| $messageData = @{ | |
| Signal = $signal | |
| Action = $re.Action | |
| ExitCondition = $ExitCondition | |
| Once = [bool]$Once | |
| } | |
| $wrappedAction = { | |
| & $Event.MessageData.Action | |
| # If our ExitCondition scriptblock returns true, set the stop signal true, which closes the watcher | |
| if ($Event.MessageData.ExitCondition) { | |
| if (& $Event.MessageData.ExitCondition) { | |
| $Event.MessageData.Signal.Stop = $true | |
| } | |
| # If we used -Once, after the first occurance, stop the watcher | |
| } elseif ($Event.MessageData.Once) { | |
| $Event.MessageData.Signal.Stop = $true | |
| } | |
| } | |
| $handler = Register-ObjectEvent -InputObject $fsw -EventName $re.EventName -Action $wrappedAction -MessageData $messageData | |
| $handlers.Add($handler) | |
| } | |
| $fsw.EnableRaisingEvents = $true | |
| do { Wait-Event -Timeout 1 } while (-not $signal.Stop) | |
| } | |
| finally { | |
| $fsw.EnableRaisingEvents = $false | |
| $handlers | Foreach-Object { Unregister-Event -SourceIdentifier $_.Name } | |
| $handlers | Remove-Job | |
| $fsw.Dispose() | |
| } | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment