Bin/Start-Watch.ps1
|
function Start-Watch { <# .SYNOPSIS Run a command repeatedly on an interval, refreshing the output in place. .DESCRIPTION A PowerShell equivalent of the Linux 'watch' command. Clears the screen, then runs the supplied command every -WaitSeconds, redrawing the result over the previous output so the console shows a live view rather than a growing scrollback. The header line - which shows the command, the interval, and the time - is rewritten on every cycle, so the timestamp always reflects when the data currently on screen was collected. Press Ctrl+C to stop. .PARAMETER Command The command to run, supplied as a string and executed with Invoke-Expression. Anything involving pipes, arguments, or operators must be wrapped in quotes. Note that Invoke-Expression runs whatever it is given: this is fine for a command you type yourself, but do not pass it a string built from untrusted input. .PARAMETER WaitSeconds Seconds to wait between runs. Defaults to 5. .PARAMETER Differences Switches from overwrite mode to change-log mode. Instead of replacing the previous output, each change is appended below the last with a timestamp, split into Added and Removed entries so the history of changes stays on screen. .PARAMETER NoClear Skip the initial screen clear and draw from the current cursor position instead, leaving whatever is already on screen intact. .EXAMPLE Start-Watch -Command Get-Process Runs Get-Process every 5 seconds, refreshing in place. .EXAMPLE Start-Watch -WaitSeconds 5 -Command "Resolve-DnsName -Name chat.lalavij.ir -Server 8.8.8.8" Re-resolves a DNS record every 5 seconds. The header timestamp updates with each lookup, so it always shows when the record below was last read. .EXAMPLE Start-Watch "Get-Process | Select-Object -First 12" -Differences -WaitSeconds 3 Monitors for changes every 3 seconds, appending each change with a timestamp rather than overwriting. .NOTES Based on Watch-Command by Matthew J. DeGarmo (@TechDufus), from the AdminToolkit project: https://github.com/TechDufus/AdminToolkit Adapted for ps-AdminTools: renamed to Start-Watch for consistency with the module's other live console tools, the header timestamp refreshes on every cycle rather than only at startup, the screen is cleared before the first run, the header is colourised, and stale text from a previous, longer frame is cleared. #> [CmdletBinding()] param( [Parameter(Mandatory, Position = 0)] [ValidateNotNullOrEmpty()] [string] $Command, [Parameter(Position = 1)] [ValidateRange(1, 86400)] [int] $WaitSeconds = 5, [switch] $Differences, [switch] $NoClear ) begin { # Header colours: label in cyan, timestamp in yellow so the value that changes each # cycle is the one that stands out. $LabelColour = [System.ConsoleColor]::Cyan $TimeColour = [System.ConsoleColor]::Yellow $Output = $null $PreviousOutput = $null $Difference = $null $PreviousLineCount = 0 try { if (-not $NoClear.IsPresent) { Clear-Host } $SaveX = [console]::CursorLeft $SaveY = [console]::CursorTop } catch { throw "Start-Watch needs an interactive console; it cannot run with redirected output." } } process { try { while ($true) { [console]::SetCursorPosition($SaveX, $SaveY) $Output = (Invoke-Expression -Command $Command -ErrorAction SilentlyContinue) # Built fresh each cycle, so the time shown is when the data below was actually # collected rather than when watching started. $HeaderLabel = "Watching: '$Command' | Interval: $WaitSeconds`s | Last run: " $HeaderTime = [datetime]::Now.ToString('yyyy-MM-dd HH:mm:ss') if ($PreviousOutput -and $Output -and $Differences.IsPresent) { $Difference = (Compare-Object $PreviousOutput $Output -PassThru) if ($Difference) { ($PreviousOutput | Out-String).Trim() "|-------------------------------| |-----------------|" "There was a change in the output: $([datetime]::Now)" "|-------------------------------| |-----------------|" $AddedDifferences = $Difference | Where-Object { $_.SideIndicator -eq "=>" } $RemovedDifferences = $Difference | Where-Object { $_.SideIndicator -eq "<=" } if ($AddedDifferences) { "Added:"; ($AddedDifferences | Out-String).Trim(); "" } if ($RemovedDifferences) { "Removed:"; ($RemovedDifferences | Out-String).Trim(); "" } "" # History has been appended above, so re-anchor below it and begin a fresh # redraw region from this point. $SaveX = [console]::CursorLeft $SaveY = [console]::CursorTop $PreviousLineCount = 0 } } if ($Differences.IsPresent) { $PreviousOutput = $Output } $Width = [console]::WindowWidth - 1 $Body = ($Output | Out-String).Trim() $BodyLines = @($Body -split "`r?`n") # --- header: two colours on one line --- if ($HeaderLabel.Length -ge $Width) { Write-Host $HeaderLabel.Substring(0, $Width) -ForegroundColor $LabelColour } else { Write-Host $HeaderLabel -ForegroundColor $LabelColour -NoNewline # Pad the timestamp out to the window width so a shorter line fully # overwrites whatever occupied that row previously. Write-Host $HeaderTime.PadRight($Width - $HeaderLabel.Length) -ForegroundColor $TimeColour } # --- body --- foreach ($Line in $BodyLines) { if ($Line.Length -gt $Width) { Write-Host $Line.Substring(0, $Width) } else { Write-Host $Line.PadRight($Width) } } $FrameLineCount = 1 + $BodyLines.Count # A previous frame may have been taller; blank the surplus rows so nothing stale # is left stranded below the current output. for ($i = $FrameLineCount; $i -lt $PreviousLineCount; $i++) { Write-Host (' ' * $Width) } $PreviousLineCount = [math]::Max($FrameLineCount, $PreviousLineCount) Start-Sleep -Seconds $WaitSeconds } } finally { $Output = $null $PreviousOutput = $null $Difference = $null } } end {} } |