-
-
Notifications
You must be signed in to change notification settings - Fork 151
Expand file tree
/
Copy pathWrite-HostColorized.ps1
More file actions
252 lines (223 loc) · 11.9 KB
/
Copy pathWrite-HostColorized.ps1
File metadata and controls
252 lines (223 loc) · 11.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
<#
Prerequisites: PowerShell version 2 or above.
License: MIT
Author: Michael Klement <mklement0@gmail.com>
DOWNLOAD, from PowerShell version 3 or above:
irm https://gist.github.com/mklement0/243ea8297e7db0e1c03a67ce4b1e765d/raw/Out-HostColored.ps1 | iex
The above directly defines the function below in your session and offers guidance for making it available in future
sessions too.
Alternatively, download this file manually and dot-source it (e.g.: . /Out-HostColored.ps1)
To learn what the function does:
* see the next comment block
* or, once downloaded, invoke the function with -? or pass its name to Get-Help.
#>
Function Write-HostColorized {
<#
.SYNOPSIS
Colors portions of the default host output that match given patterns.
.DESCRIPTION
Colors portions of the default-formatted host output based on either
regular expressions or literal substrings, assuming the host is a console or
supports colored output using console colors.
Matching is restricted to a single line at a time, but coloring multiple
matches on a given line is supported.
Two basic syntax forms are supported:
* Single-color, via -Pattern, -ForegroundColor and -BackgroundColor
* Multi-color (color per pattern), via a hashtable (dictionary) passed to
-PatternColorMap.
Note: Since output is sent to the host rather than the pipeline, you cannot
chain calls to this function.
.PARAMETER Pattern
One or more search patterns specifying what parts of the formatted
representations of the input objects should be colored.
* By default, these patterns are interpreted as regular expressions.
* If -SimpleMatch is also specified, the patterns are interpreted as literal
substrings.
.PARAMETER ForegroundColor
The foreground color to use for the matching portions.
Defaults to yellow.
.PARAMETER BackgroundColor
The optional background color to use for the matching portions.
.PARAMETER PatternColorMap
A hashtable (dictionary) with one or more entries in the following format:
<pattern-or-pattern-array> = <color-spec>
<pattern-or-pattern-array> is either a single string or an array of strings
specifying the regex pattern(s) or literal substring(s) (with -SimpleMatch)
to match.
NOTE: If you're specifying an array literally, you must enclose it in (...) or
@(...), and the individual patterns must all be quoted; e.g.:
@('foo', 'bar')
<color-spec> is a string that contains either a foreground [ConsoleColor]
color alone (e.g. 'red'), a combination with a background color separated by ","
(e.g., 'red,white') or just a background color (e.g, ',white').
NOTE: If *multiple* patterns stored in a given hashtable may match on a given
line and you want the *first* matching pattern to "win" predictably, be
sure to pass an [ordered] hashtable ([ordered] @{ Foo = 'red; ... })
See the examples for a complete example.
.PARAMETER CaseSensitive
Matches the patterns case-sensitively.
By default, matching is case-insensitive.
.PARAMETER WholeLine
Specifies that the entire line containing a match should be colored,
not just the matching portion.
.PARAMETER SimpleMatch
Interprets the -Pattern argument(s) as a literal substrings to match rather
than as regular expressions.
.PARAMETER InputObject
The input object(s) whose formatted representations to color selectively.
Typically provided via the pipeline.
.EXAMPLE
'A fool and his money', 'foo bar' | Out-HostColored foo
Prints the substring 'foo' in yellow in the two resulting output lines.
.EXAMPLE
Get-Date | Out-HostColored '\p{L}+' red white
Outputs the current date with all tokens composed of letters (p{L}) only in red
on a white background.
.EXAMPLE
Get-Date | Out-HostColored @{ '\p{L}+' = 'red,white' }
Same as the previous example, only via the dictionary-based -PatternColorMap
parameter (implied).
.EXAMPLE
'It ain''t easy being green.' | Out-HostColored @{ ('easy', 'green') = 'green'; '\bbe.+?\b' = 'black,yellow' }
Prints the words 'easy' and 'green' in green, and the word 'being' in black on yellow.
Note the need to enclose pattern array 'easy', 'green' in (...), which also necessitates
quoting its element.
.EXAMPLE
Get-ChildItem | select Name | Out-HostColored -WholeLine -SimpleMatch .txt
Highlight all text file names in green.
.EXAMPLE
'apples', 'kiwi', 'pears' | Out-HostColored '^a', 's$' blue
Highlight all "A"s at the beginning and "S"s at the end of lines in blue.
#>
# === IMPORTANT:
# * At least for now, we remain PSv2-COMPATIBLE.
# * Thus:
# * no `[ordered]`, `::new()`, `[pscustomobject]`, ...
# * No implicit Boolean properties in [CmdletBinding()] and [Parameter()] attributes (`Mandatory` instead of just `Mandatory`)
# ===
[CmdletBinding(DefaultParameterSetName = 'SingleColor')]
param(
[Parameter(ParameterSetName = 'SingleColor', Position = 0, Mandatory)] [string[]] $Pattern,
[Parameter(ParameterSetName = 'SingleColor', Position = 1)] [ConsoleColor] $ForegroundColor = [ConsoleColor]::Yellow,
[Parameter(ParameterSetName = 'SingleColor', Position = 2)] [ConsoleColor] $BackgroundColor,
[Parameter(ParameterSetName = 'PerPatternColor', Position = 0, Mandatory)] [System.Collections.IDictionary] $PatternColorMap,
[Parameter(ValueFromPipeline = $True)] $InputObject,
[switch] $WholeLine,
[switch] $SimpleMatch,
[switch] $CaseSensitive
)
begin {
Set-StrictMode -Version 1
if ($PSCmdlet.ParameterSetName -eq 'SingleColor') {
# Translate the indiv. arguments into the dictionary format suppoorted
# by -PatternColorMap, so we can process $PatternColorMap uniformly below.
$PatternColorMap = @{
$Pattern = $ForegroundColor, $BackgroundColor
}
}
# Otherwise: $PSCmdlet.ParameterSetName -eq 'PerPatternColor', i.e. a dictionary
# mapping patterns to colors was directly passed in $PatternColorMap
try {
# The options for the [regex] instances to create.
# We precompile them for better performance with many input objects.
[System.Text.RegularExpressions.RegexOptions] $reOpts =
if ($CaseSensitive) { 'Compiled, ExplicitCapture' }
else { 'Compiled, ExplicitCapture, IgnoreCase' }
# Transform the dictionary:
# * Keys: Consolidate multiple patterns into a single one with alternation and
# construct a [regex] instance from it.
# * Values: Transform the "[foregroundColor],[backgroundColor]" strings into an arguments
# hashtable that can be used for splatting with Write-Host.
$map = [ordered] @{ } # !! For stable results in repeated enumerations, use [ordered].
# !! This matters when multiple patterns match on a given line, and also requires the
# !! *caller* to pass an [ordered] hashtable to -PatternColorMap
foreach ($entry in $PatternColorMap.GetEnumerator()) {
# Create a Write-Host color-arguments hashtable for splatting.
if ($entry.Value -is [array]) {
$fg, $bg = $entry.Value # [ConsoleColor[]], from the $PSCmdlet.ParameterSetName -eq 'SingleColor' case.
} else {
$fg, $bg = $entry.Value -split ','
}
$colorArgs = @{ }
if ($fg) { $colorArgs['ForegroundColor'] = [ConsoleColor] $fg }
if ($bg) { $colorArgs['BackgroundColor'] = [ConsoleColor] $bg }
# Consolidate the patterns into a single pattern with alternation ('|'),
# escape the patterns if -SimpleMatch was passsed.
$re = New-Object regex -Args `
$(if ($SimpleMatch) {
($entry.Key | ForEach-Object { [regex]::Escape($_) }) -join '|'
} else {
($entry.Key | ForEach-Object { '({0})' -f $_ }) -join '|'
}),
$reOpts
# Add the tansformed entry.
$map[$re] = $colorArgs
}
} catch { throw }
# Construct the arguments to pass to Out-String.
$htArgs = @{ Stream = $True }
if ($PSBoundParameters.ContainsKey('InputObject')) {
# !! Do not use `$null -eq $InputObject`, because PSv2 doesn't create this variable if the parameter wasn't bound.
$htArgs.InputObject = $InputObject
}
# Construct the script block that is used in the steppable pipeline created
# further below.
$scriptCmd = {
# Format the input objects with Out-String and output the results line
# by line, then look for matches and color them.
& $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Out-String', 'Cmdlet') @htArgs | ForEach-Object {
# Match the input line against all regexes and collect the results.
$matchInfos = :patternLoop foreach ($entry in $map.GetEnumerator()) {
foreach ($m in $entry.Key.Matches($_)) {
@{ Index = $m.Index; Text = $m.Value; ColorArgs = $entry.Value }
if ($WholeLine) { break patternLoop }
}
}
# # Activate this for debugging.
# $matchInfos | Sort-Object { $_.Index } | Out-String | Write-Verbose -vb
if (-not $matchInfos) {
# No match found - output uncolored.
Write-Host -NoNewline $_
} elseif ($WholeLine) {
# Whole line should be colored: Use the first match's color
$colorArgs = $matchInfos.ColorArgs
Write-Host -NoNewline @colorArgs $_
} else {
# Parts of the line must be colored:
# Process the matches in ascending order of start position.
$offset = 0
foreach ($mi in $matchInfos | Sort-Object { $_.Index }) {
# !! Use of a script-block parameter is REQUIRED in WinPSv5.1-, because hashtable entries cannot be referred to like properties, unlinke in PSv7+
if ($mi.Index -lt $offset) {
# Ignore subsequent matches that overlap with previous ones whose colored output was already produced.
continue
} elseif ($offset -lt $mi.Index) {
# Output the part *before* the match uncolored.
Write-Host -NoNewline $_.Substring($offset, $mi.Index - $offset)
}
$offset = $mi.Index + $mi.Text.Length
# Output the match at hand colored.
$colorArgs = $mi.ColorArgs
Write-Host -NoNewline @colorArgs $mi.Text
}
# Print any remaining part of the line uncolored.
if ($offset -lt $_.Length) {
Write-Host -NoNewline $_.Substring($offset)
}
}
Write-Host '' # Terminate the current output line with a newline - this also serves to reset the console's colors on Unix.
}
}
# Create the script block as a *steppable pipeline*, which enables
# to perform regular streaming pipeline processing, without having to collect
# everything in memory first.
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} # begin
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
}