File Watcher
I wanted to run a script every time a file changed. There’s a bunch of tools to do this for Linux, but I’m on Windows (because reasons). I found a PowerShell module, installed it, and got to work.
function Enter-Watcher {
Param (
[PSDefaultValue(Help="*")]
$Filter="*",
$Path=".",
$SourceIdentifier="watcher-event",
[Parameter(Mandatory=$true, Position=0)]
$ScriptBlock
)
#Cleanup of previous uses. Remove past watchers of this type, to eliminate double-events
$e = "$SourceIdentifier-emit"
Remove-Job -name $e,$SourceIdentifier -Force 2>$null # -Force removes not-stopped jobs too
Remove-FileSystemWatcher -SourceIdentifier $SourceIdentifier
$fs = New-FileSystemWatcher -SourceIdentifier $SourceIdentifier -Path $Path -Filter $Filter -Action {
if($event.messageData.ChangeType -eq "Changed") {
$x = "$($event.SourceIdentifier)-emit"
New-Event -SourceIdentifier $x -MessageData $event.messageData.fullpath
}
}
$job = Register-EngineEvent -SourceIdentifier $e -Action $Scriptblock
while($true) {
receive-job $job
}
}
function Remove-AllWatchers {
Get-FileSystemWatcher | Remove-FileSystemWatcher
}
Call it like:
Enter-Watcher -Filter "*.ps1" -ScriptBlock {write-host $event.messageData}
The message data will be the full path. The job and watcher persist even after you ctrl-C, so you can do something else.
Notes
The complexity of Enter-Watcher
comes from two places:
- When vim saves a file, it creates three files events, which triggers the watcher three times. So we need to filter for only the final event. If you don’t have this problem, you can just do
-Action $ScriptBlock
. - Script blocks don’t form proper closures, so you can’t do
-Action {if(filter) { invoke-command $ScriptBlock }}
.
The simplest solution I found was to have the watcher emit a second event, then register an engine event to catch that and run a top-level scriptblock. It’s jankier than I’d like, and I don’t know if that’s my inexperience or PowerShell itself.
While writing this I learned that you don’t need the third party package, since you can just create a file watcher directly in PowerShell:
$watcher = New-Object System.IO.FileSystemWatcher
At some point I might rewrite Enter-Watcher
to remove the dependency.