Skip to content

Instantly share code, notes, and snippets.

@romero126
Last active May 28, 2025 05:53
Show Gist options
  • Select an option

  • Save romero126/be03f7d1f4c9c9ee93da78738f13dcd7 to your computer and use it in GitHub Desktop.

Select an option

Save romero126/be03f7d1f4c9c9ee93da78738f13dcd7 to your computer and use it in GitHub Desktop.
Monitor-Object

Example Expression

image

$splat = @{
    Title           = 'Monitor-Workflow';
    PropertyKey     = 'Value';
    Property        = 'Value', 'Iteration';
    InputPattern    = "(\d+)";
    RefreshInterval = 3;
    ProcessBlock    = {
        param(
            [object] $InputObject
        )

        $InputObject.Iteration++
    }
}
1..10 | Monitor-Object @splat
function Monitor-Object {
<#
.SYNOPSIS
Monitor-Object is a cmdlet that allows you to monitor a collection of objects
.DESCRIPTION
Monitor-Object is a cmdlet that allows you to monitor a collection of objects.
The cmdlet will display the entire collection to the screen
and allow you to process each object in the collection
with a scriptblock that you provide. The cmdlet will also allow you to
add and remove objects from the collection using hotkeys.
.PARAMETER ProcessBlock
The processblock will be used to process/refresh the objects in the collection.
Example: { param($InputObject) $InputObject }
.PARAMETER InputObject
Specify either a String or a PSObject that will be used to identify the
Example: "Hello World", [PSCustomObject]@{ Name = "Hello World" }
.PARAMETER PropertyKey
The PropertyKey is the primary key identifier that will be used to identify the each unique object in the pipeline
.PARAMETER Property
The property or properties that be defined as each object in the collection[]
.PARAMETER FormatTable
If FormatTable is specified, the collection will be formatted with Format-Table using the properties specified.
.PARAMETER Title
The title of the monitor object
.PARAMETER InputPattern
The InputPattern is a regex pattern that will be used to identify if the input is a valid object to be added to the collection.
.PARAMETER RefreshInterval
The RefreshInterval is the interval in seconds that the monitor will refresh the screen.
.PARAMETER Passthru
The Passthru switch will return the collection at the end of the cmdlet's execution.
.EXAMPLE
1..10 | Monitor-Object -Title 'Monitor-Item' -PropertyKey 'Value' -Property 'Name', 'Value', 'Iteration' -FormatTable 'Name', 'Value', 'Iteration' -InputPattern "(\d+)" -ProcessBlock {
param(
[object] $InputObject
)
if ($null = $InputObject.Name) {
$InputObject.Name = "Object $($InputObject.Value)"
}
$InputObject.Iteration += 1
}
#>
[CmdletBinding()]
param(
# There is a hard requirement for the script to handle an InputObject with ValueFromPipeline
[Parameter(Mandatory)]
[Alias('ScriptBlock', 'SB')]
[ScriptBlock]$ProcessBlock,
[Parameter(ValueFromPipeline)]
[Alias('Object')]
[object[]] $InputObject,
[Parameter(Mandatory)]
[alias('key')]
[System.String] $PropertyKey,
[Parameter()]
[System.String[]] $Property,
[Parameter()]
[PSCustomObject[]] $FormatTable,
[Parameter()]
[ValidateScript({ $_ -is [ScriptBlock] -or $_ -is [String] })]
[System.Object] $Title = "Monitor-Object",
[Parameter(Mandatory)]
[System.String] $InputPattern,
[Parameter()]
[Alias('ri')]
[int] $RefreshInterval = 30,
[Parameter()]
[switch] $Passthru
)
begin {
# this is not a complete list of ansi escape sequences
$ansi = @{
esc = "$([char]27)"
cursorHome = "$([char]27)[0;0H"
cursorReturn = "$([char]27)[0G"
eraseLine = "$([char]27)[2K"
eraseScreen = "$([char]27)[2J"
eraseScreenAfterCursor = "$([char]27)[0J"
colorReset = "$([char]27)[0m"
altBufferEnable = "$([char]27)[?1049h"
altBufferDisable = "$([char]27)[?1049l"
# colors ; # background colors
black = "$([char]27)[30m"; bgBlack = "$([char]27)[40m"
red = "$([char]27)[31m"; bgRed = "$([char]27)[41m"
green = "$([char]27)[32m"; bgGreen = "$([char]27)[42m"
yellow = "$([char]27)[33m"; bgYellow = "$([char]27)[43m"
blue = "$([char]27)[34m"; bgBlue = "$([char]27)[44m"
magenta = "$([char]27)[35m"; bgMagenta = "$([char]27)[45m"
cyan = "$([char]27)[36m"; bgCyan = "$([char]27)[46m"
white = "$([char]27)[37m"; bgWhite = "$([char]27)[47m"
default = "$([char]27)[39m"; bgDefault = "$([char]27)[49m"
# text style
bold = "$([char]27)[1m"
underline = "$([char]27)[4m"
blink = "$([char]27)[5m"
inverse = "$([char]27)[7m"
hidden = "$([char]27)[8m"
strike = "$([char]27)[9m"
noBold = "$([char]27)[22m"
noUnderline = "$([char]27)[24m"
noBlink = "$([char]27)[25m"
noInverse = "$([char]27)[27m"
noHidden = "$([char]27)[28m"
noStrike = "$([char]27)[29m"
}
# Specify common color schema
$ansi_ui = @{
TextColor = $ansi.white
WarningColor = $ansi.yellow
ExceptionColor = $ansi.red
SuccessColor = $ansi.green
EmphasisColor = $ansi.cyan
menu_textColor = $ansi.white
menu_bodyColor = "$($ansi.esc)[48;5;24m" + $ansi.white
menu_borderColor = "$($ansi.esc)[48;5;237m" + $ansi.white
}
# Set a default object for its current instance.
$this = @{}
$this.Collection = New-Object System.Collections.ArrayList
$this.HotKeyCollection = New-Object System.Collections.ArrayList
# Starting, Running, Suspend, Exit
$this.State = "Starting"
# Define actions based on whats in the ActionCollection
$this.Action = $null
$this.Interrupt = $null
# Define hotkeys
$this.HotKeyCollection = @(
@{
Key = "A"
Modifier = 'Control'
Description = "Add Clipboard to Pipeline"
Action = "AddClipboardToPipeline", "RefreshScreen"
},
@{
Key = "D"
Modifier = 'Control'
Description = "Remove Clipboard to Pipeline"
Action = "RemoveClipboardFromPipeline", "RefreshScreen"
}
@{
Key = "R"
Modifier = 'Control'
Description = "Refresh the object data"
Action = "RefreshScreen"
},
@{
Key = "X"
Modifier = 'Control'
Description = "Exit"
Action = "Exit"
}
)
$this.Methods = @{
#New
InvokeScriptBlock = {
param(
[ScriptBlock] $ScriptBlock,
[hashtable] $Parameters
)
# We should not process anything if the state has an interrupt
if ($this.State -eq "Exit") {
return
}
try {
if ($Parameters) {
& $ScriptBlock @Parameters
} else {
& $ScriptBlock
}
}
catch {
$this.State = "Exit"
# Bubble up the exception to the console
[System.Console]::WriteLine("$($ansi.altBufferDisable)$($ansi_ui.ExceptionColor)An Exception was thrown:")
[System.Console]::Write(($_ | Out-String))
[System.Console]::Write("$($ansi.colorReset)")
}
}
#New
Draw_Menu_Top = {
#set cursor to 0,0
[System.Console]::Write("$($ansi.cursorHome)")
# If the title is a scriptblock, invoke it
if ($Title -is [ScriptBlock]) {
& $this.Methods.InvokeScriptBlock -ScriptBlock $Title
return
}
# Draw top menu bar (This doesnt doing anything and its the equivalant of ASCII Art so you can see that you are in the cmdlet menu)
[System.Console]::Write("$($ansi_ui.menu_borderColor)$($ansi.eraseLine)`n")
[System.Console]::Write(("$($ansi_ui.menu_bodyColor)$($ansi.eraseLine)`n"*2))
# Write CommandName
# We dont like adding new lines to the title so we will replace them with spaces
$message = "$Title" -replace "`n", " "
if ($message.Count -ge $host.UI.RawUI.WindowSize.Width) {
$message = $message.Substring(0, $host.UI.RawUI.WindowSize.Width - 5) + '...'
}
$padding = (' ' * (($host.UI.RawUI.WindowSize.Width / 2) - $message.Length/2))
[System.Console]::Write("$($ansi_ui.menu_bodyColor)$($ansi.eraseLine)$padding $message`n")
[System.Console]::Write(("$($ansi_ui.menu_bodyColor)$($ansi.eraseLine)`n"*2))
[System.Console]::Write("$($ansi_ui.menu_borderColor)$($ansi.eraseLine)`n")
[System.Console]::Write("$($ansi.colorReset)")
}
# New
Draw_Menu_Body = {
[System.Console]::Write($ansi.eraseScreenAfterCursor)
if ($this.Collection.Count -eq 0) {
[System.Console]::Write("$($ansi_ui.WarningColor)No objects in the pipeline`n")
return
}
if ($FormatTable) {
& $this.Methods.InvokeScriptBlock `
-ScriptBlock {
param($Collection, $FormatTable)
$Collection | Format-Table -Property $FormatTable
} `
-Parameters @{
Collection = $this.Collection;
FormatTable = $FormatTable
}
} else {
& $this.Methods.InvokeScriptBlock `
-ScriptBlock {
param($Collection)
$Collection | Out-Default
} `
-Parameters @{
Collection = $this.Collection
}
}
}
# New Ish
Draw_Menu_Bottom = {
# Draw the hotkeys at the bottom of the screen
[System.Console]::Write("$($ansi_ui.TextColor)Hotkeys:`n")
foreach ($hotKey in $this.HotKeyCollection) {
[System.Console]::Write("`t$($ansi_ui.EmphasisColor)$($hotKey.Modifier)$($ansi_ui.TextColor)+$($ansi_ui.EmphasisColor+$hotKey.Key)$($ansi_ui.TextColor) : $($hotKey.Description)`n")
}
[System.Console]::Write("$($ansi.colorReset)")
}
# New
WaitRefreshInterval = {
if ($this.Interrupt -or $this.State -eq "Exit") {
return
}
# Refresh Interval
$timer = [Diagnostics.Stopwatch]::StartNew()
if ($RefreshInterval) {
do {
# Show Status
$timeRemaining = [System.Math]::Round($RefreshInterval - $timer.Elapsed.TotalSeconds, 1)
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Refreshing in $timeRemaining Seconds $($ansi.colorReset)")
# Check for keypress
& $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods.CheckKeyPress
# Check for Interrupts or Exit or Interrupt
if ($this.Interrupt -or $this.State -eq "Exit") {
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Skip Refreshing with $timeRemaining Seconds remaining$($ansi.colorReset)`n")
break
}
Start-Sleep -MilliSeconds 100 # 10 times a second
} until ($timer.Elapsed.TotalSeconds -ge $RefreshInterval)
$timer.Stop()
[System.Console]::Write("$($ansi.eraseLine)")
}
}
# New
CheckKeyPress = {
if ([System.Console]::KeyAvailable) {
$key = [System.Console]::ReadKey($true)
foreach ($hotKey in $this.HotKeyCollection) {
if ($key.Modifiers -eq $hotKey.Modifier -and $key.Key -eq $hotKey.Key) {
$this.Interrupt = $hotkey.Action
return
}
}
}
}
# New
RefreshScreen = {
$this.Interrupt = $null
& $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods.ProcessObjects
}
AddClipboardToPipeline = {
$clipboard = ( Get-Clipboard ) -Split "`n" | ForEach-Object { if ($_ -match $InputPattern) { $matches[0] } } | Select-Object -Unique
if ($clipboard.Count -eq 0) {
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)No objects found in the clipboard`n")
$this.Methods.Sleep.Invoke()
return
}
$existingItems = $this.Collection | Where-Object { $_.PSObject.Properties.Item($PropertyKey).Value -in $clipboard }
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.TextColor)Clipboard contains $($ansi_ui.EmphasisColor)$($clipboard.Count)$($ansi_ui.TextColor) unique objects $($ansi_ui.WarningColor)Skipping$($ansi_ui.TextColor) $($ansi_ui.EmphasisColor)$($clipboard.Count)$($ansi_ui.TextColor) objects`n")
foreach ($i in $clipboard) {
$itemExists = $this.Collection | Where-Object { $_.PSObject.Properties.Item($PropertyKey).Value -in $i }
if ($itemExists) {
[System.Console]::Write("$($ansi_ui.TextColor) Skipping item: $($ansi_ui.EmphasisColor)$i$($ansi_ui.TextColor)`n")
continue
}
[System.Console]::Write("$($ansi_ui.TextColor) Adding item: $($ansi_ui.EmphasisColor)$i$($ansi_ui.TextColor) to buffer`n")
$null = $this.Collection.Add(
([PSCustomObject]@{ $PropertyKey = $i } | Select-Object -Property $Property)
)
}
}
RemoveClipboardFromPipeline = {
$clipboard = ( Get-Clipboard ) -Split "`n" | ForEach-Object { if ($_ -match $InputPattern) { $matches[0] } } | Select-Object -Unique
$objectsToRemove = $this.Collection | Where-Object { $_.PSObject.Properties.Item($PropertyKey).Value -in $clipboard -or $_ -eq $clipboard }
if ($objectsToRemove.Count -eq 0) {
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)No objects found to remove`n")
$this.Methods.Sleep.Invoke()
return
}
[System.Console]::Write("`n`n`n$($ansi_ui.WarningColor)Objects to be removed$($ansi.colorReset)`n")
if ($FormatTable) {
$objectsToRemove | Format-Table -Property $FormatTable | Out-Host
} else {
$objectsToRemove | Out-Default
}
$decision = $Host.UI.PromptForChoice("Warning: Removing items on the list", "This will remove $($objectsToRemove.Count) items from the pipeline", @('&Yes', '&No'), 1)
if ($decision -ne 0) {
return
}
$objectsToRemove | ForEach-Object { $null = $this.Collection.Remove( $_ ) } -ErrorAction SilentlyContinue
}
ProcessObjects = {
# Endless snake of loops
for ($i = 0; $i -lt $this.Collection.Count; $i++) {
$obj = $this.Collection[$i]
# Check keypress and invoke hotkey action
& $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods.CheckKeyPress
# Check for Interrupts or Exit or Interrupt
if ($this.State -eq "Exit" -or $this.Interrupt) {
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Skip Processing on $i of $($this.Collection.Count)$($ansi.colorReset)")
return
}
# Invoke and handle the ProcessBlock
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.WarningColor)Processing on $i of $($this.Collection.Count) : ")
try {
& $ProcessBlock -InputObject $obj
}
catch {
[System.Console]::Write("`n$($ansi_ui.ExceptionColor)Error Processing $($i+1) of $($this.Collection.Count) : $($ansi.colorReset)`n")
$this.Methods.Sleep.Invoke()
}
}
[System.Console]::Write("$($ansi.cursorReturn+$ansi.eraseLine+$ansi_ui.SuccessColor)Completed Processing $($this.Collection.Count) Records$($ansi.colorReset)`n$($ansi.eraseScreenAfterCursor)")
}
Sleep = {
start-sleep 5
}
Exit = {
$this.State = "Exit"
}
}
if ($PropertyKey -notin $Property) {
$Property += $PropertyKey
}
}
process {
# Add the input object to the list
foreach ($obj in $InputObject) {
if ($obj -match $InputPattern) {
$null = $this.Collection.Add(
([PSCustomObject]@{ $PropertyKey = $matches[0] } | Select-Object -Property $Property)
)
continue
} elseif ($obj -is [PSCustomObject] -and $obj.PSObject.Properties.Item($PropertyKey).Value -match $InputPattern) {
$null = $this.Collection.Add($obj)
continue
}
}
}
end {
[System.Console]::Write($ansi.altBufferEnable)
try {
do {
# Draw the Menu
$this.Action = "Draw_Menu_Top", "Draw_Menu_Body", "Draw_Menu_Bottom", "WaitRefreshInterval", "ProcessObjects"
if ($this.State -eq "Starting") {
$this.Action = "Draw_Menu_Top", "Draw_Menu_Body", "Draw_Menu_Bottom", "ProcessObjects"
$this.State = "Running"
}
foreach ($action in $this.Action) {
if ($this.State -eq "Exit") {
break
}
if ($this.Methods.ContainsKey($action)) {
& $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods[$action]
} else {
Write-Warning "Action $action not found"
$this.Methods.Sleep.Invoke()
}
# Check keypress and invoke hotkey action
$this.Methods.CheckKeyPress.Invoke()
}
# Check for Interrupt and call the action
if ($this.Interrupt) {
# Store the interrupts in a variable to use later
$interrupts = $this.Interrupt
# Reset the interrupts to null so that we can properly run the actions
$this.Interrupt = $null
# Invoke all of the interrupts in the list
foreach ($interrupt in $interrupts) {
if ($this.Methods.ContainsKey($interrupt)) {
& $this.Methods.InvokeScriptBlock -ScriptBlock $this.Methods[$interrupt]
}
}
}
} until ($this.State -eq "Exit")
}
catch {
throw $_
}
finally {
# Clean up the console
[System.Console]::Write($ansi.altBufferDisable)
}
if ($Passthru) {
$this.Collection
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment