Skip to content

Instantly share code, notes, and snippets.

@steviecoaster
Last active March 2, 2026 15:31
Show Gist options
  • Select an option

  • Save steviecoaster/fc7b98a155b6f75476be850436a12b33 to your computer and use it in GitHub Desktop.

Select an option

Save steviecoaster/fc7b98a155b6f75476be850436a12b33 to your computer and use it in GitHub Desktop.
Implement a FileSystemWatcher in PowerShell
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