# encoding: ascii
# api: powershell
# title: ColorPattern
# description: Color strings via Write-Host via Regular Expressions or simple matching via pipelining. The order precedence is set under the $patterns variable, so you can have overlapping matches, resulting in output such as, e.g.:
# version: 0.1
# author: Daniel Cheng
# license: CC0
# x-poshcode-id: 5689
# x-archived: 2015-01-18T09:20:36
# x-published: 2015-01-13T16:31:00
#
# 0 = {ForegroundColor = 'White'; BackgroundColor='Gray'}
# '\d+' = {ForegroundColor = ‘Gray’; BackgroundColor=‘Black’}
# This will result in all strings ‘0’ colored as white with a gray background, while coloring all other digits gray with a black background.
# $colorCollection (system.collections.generic.dictionary) will convert everything, including digits, to a string.
# The performance is lackluster as the script parses regex matches, then finds out the non-matches of the line, and adds it all under a System.Collections.Generic.SortedDictionary object. This is used to output the colors.
# Uncomment #$VerbosePreference = ‘continue’ to get more details as it parses the matches and line characters.
# Tested under Powershell v4.0, and may not work under previous versions without modifications. This was posted as a late reply to http://stackoverflow.com/questions/7362097/color-words-in-powershell-script-format-table-output/27912993#27912993.
#
# Daniel Cheng
#$VerbosePreference = 'continue'
$VerbosePreference = 'silent'
filter ColorPattern {
param ([object]$colors, [switch]$SimpleMatch)
[string]$line = $_
$collection = New-Object 'System.Collections.Generic.SortedDictionary[int, pscustomobject]'
$RegexOptions = [Text.RegularExpressions.RegexOptions]::IgnoreCase.value__ + [Text.RegularExpressions.RegexOptions]::Singleline.value__
if ($SimpleMatch){
$patternMatches = $colors.keys | % {[regex]::Escape($_)}
$reference = 'Value'
} else {
$patternMatches = $colors.keys
$reference = 'Pattern'
}
# detect RegEx matches and add to collection object
Write-Verbose "'$line'"
$measureparsing_match = (Measure-Command {
foreach ($pattern in $patternMatches){
Write-Verbose "regex pattern: $pattern"
foreach ($match in ([regex]::Matches($line, $pattern, $RegexOptions))){ # lazy matching
Write-Verbose "`tmatch index: $($match.Index) length: $($match.length)"
$currentset = ($match.Index)..($match.Index + $match.length - 1)
Write-Verbose "`tcurrent set: $currentset"
if (-not [bool]$collection.Count){
Write-Verbose "`t`tindex: $($match.Index) value: $($match.value) (inital add)"
[void]$collection.Add($match.Index, [PSCustomObject]@{Length = $match.Length; Value = $match.Value; Pattern = $pattern; Range = $currentset})
} else {
(,$collection.Values) | % {
$currentRange = $_.range
$intersect = Compare-Object -PassThru $currentset $currentRange -IncludeEqual -ExcludeDifferent
if ($intersect){
Write-Verbose "`t`tintersect: $([string]($intersect | % {[string]::Concat($_)})) (skipped)"
$nonintersect = Compare-Object -PassThru $currentset $intersect
Write-Verbose "`t`tnonintersect: $([string]($nonintersect | % {[string]::Concat($_)}))"
$nonintersect | % {
if ($currentRange -notcontains $_){
Write-Verbose "`t`tindex: $_ value: $($line[$_]) (adding intersect-fallout)"
[void]$collection.Add($_, [PSCustomObject]@{Length = $_.Length; Value = $line[$_]; Pattern = $pattern; Range = $currentset})
} else {
Write-Verbose "`t`t`tindex: $_ value: $($line[$_]) (skipped intersect-fallout)"
}
}
} else {
Write-Verbose "`t`tindex: $($match.index) value: $($match.value) (adding nonintersect)"
[void]$collection.Add($match.Index, [PSCustomObject]@{Length = $match.Length; Value = $match.Value; Pattern = $pattern; Range = $currentset})
}
} # end values
} #end if
} # end matching
} # end pattern
}).TotalMilliseconds
$measureparsing_nonmatch = (Measure-Command {
if ([bool]$collection.count){ # if there are no matches, skip!
Compare-Object -PassThru `
-ReferenceObject (
$collection.Keys | % { # all matched keys and their lengths
$word = $collection.item($_)
$currentlength = ($word.value).length
($_..($_ + ($currentlength - 1)))
}) `
-DifferenceObject (0..($line.Length - 1)) | # entire line
% {[void]$collection.Add($_, [PSCustomObject]@{Length = $_.length; Value = $line[$_]})} # add non matches to collection
}
}).TotalMilliseconds
Write-Verbose "match: $measureparsing_match ms. VS nonmatch: $measureparsing_nonmatch ms."
$collection.keys | % {
$word = $collection.item($_)
if ($word.pattern){
if ($colors.ContainsKey($word.$reference)){
$color = @{
ForegroundColor = $colors[$word.$reference].ForegroundColor;
BackgroundColor = $colors[$word.$reference].BackgroundColor
}
if ($word.value){
Write-Host -NoNewline $([string]::Concat($word.value)) @color
}
}
} else {
Write-Host -NoNewline $([string]::Concat($word.value))
}
}
Write-Host # needed for line feed
}
$Patterns = [ordered]@{
# higher in list takes precendence
'stopped' = @{ForegroundColor = 'Red'; BackgroundColor='DarkRed'}
'running' = @{ForegroundColor = 'Green'; BackgroundColor='DarkGreen'}
'paused' = @{ForegroundColor = 'Yellow'; BackgroundColor='DarkYellow'}
0 = @{ForegroundColor = 'White'; BackgroundColor='Gray'}
'\d+' = @{ForegroundColor = 'Gray'; BackgroundColor='Black'}
'\.' = @{ForegroundColor = 'Magenta'; BackgroundColor='DarkMagenta'}
'(a|e|i|o|u)' = @{ForegroundColor = 'Yellow'; BackgroundColor='DarkYellow'}
'\w+' = @{ForegroundColor = 'Cyan'; BackgroundColor='DarkCyan'}
}
# strongly typed collection.. we could probably do this better..
$colorCollection = New-Object 'system.collections.generic.dictionary[string, hashtable]'([StringComparer]::OrdinalIgnoreCase) # Ordinal
$Patterns.GetEnumerator() | % {[void]$colorCollection.Add($_.Name, $_.Value)}
Get-Service | Out-String -Stream | ColorPattern -colors $colorCollection
#Get-Service | Out-String -Stream | ColorPattern -colors $colorCollection -SimpleMatch