Skip to Content

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:

  1. 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.
  2. 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.