Kevin
Kevin

Reputation: 350

I would like to color highlight sections of a pipeline string according to a regex in Powershell

This is a question of technique, but as an exercise my intention is to write a PS to accept piped input, with a regex as a parameter, and highlight any text matching the regex.

The part I'm not able to find any info on is that it's easy to match text, capturing to a buffer, or to replace text. But I need to replace matched text with color control, the original text, then resume the previous color. I can't seem to find any way to generate color output other than with write-output, and can't do separate colors in a single write, which would mean:

-matching the regex

-write-host out all text prior to the match in default color, with -NoNewLine

-write-host the match, with -NoNewLine

-write-host the remainder

This seems messy, and gets even more messy if we want to support multiple matches. Is there a more eloquent way to do this?

Upvotes: 7

Views: 2569

Answers (3)

KeithJ
KeithJ

Reputation: 169

Alternatively I found using ANSI/VT100 formatting more simple and does exactly what I needed with a much larger range of colors:

$esc=[char]27
$fileContents="abc455315testing123455315abc"
$keywordSearch="testing123"

$fileContents -replace $keywordSearch,"$esc[38;2;0;200;255m$keywordSearch$esc[0m"

Note this only works in a PowerShell console window not in PowerShell ISE. This wikipedia page also was helpful; specifically this line with regards to choosing a color:

ESC[ 38;2;⟨r⟩;⟨g⟩;⟨b⟩ m Select RGB foreground color

Upvotes: 1

cyborg
cyborg

Reputation: 5748

This is an extension of latkin's answer. Here I'm extending the Match object such that it can be processed for this purpose - and others - more easily.

function Split-Match {
    param([Parameter(Mandatory = $true)]
    $match
    )
    $sections = @()
    $start = 0
    $text = $m.Line
    foreach ($m in $match.Matches) {
        $i = $m.Index
        $l = $m.Length

        $sections += $false, $text.Substring($start, $i - $start)
        $sections += $true, $text.Substring($i, $l)
        $start = $i + $l
    }
    $sections += $false, $text.Substring($start)
    $match | Add-Member -Force Sections $sections
    $match
}

function Write-Match {
    param([Parameter(Mandatory = $true)]
    $match
    )
    $fg = "White"
    $bg = "Black"
    foreach($s in $match.Sections) {
        if ($s.GetType() -eq [bool]) {
            if ($s) {
                $fg = "White"
                $bg = "Red"
            } else {
                $fg = "White"
                $bg = "Black"
            }
        } else {
            Write-Host -NoNewline -ForegroundColor $fg -BackgroundColor $bg $s
        }
    }
    Write-Host
}

$string = @'
Match this A
Not this B
Not this C
But this A
'@
$m = $string | select-string -CaseSensitive -AllMatches "A"
$m = Split-Match $m
Write-Match $m

output

Upvotes: 1

latkin
latkin

Reputation: 16792

Write-Host is the right way to do this. Use the .Index and .Length properties of the resulting Match object to determine where exactly the matched text is. You just need to be a bit careful keeping track of indices :)

This works for multiple matches, and is not terribly untidy IMO:

function ColorMatch
{
   param(
      [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
      [string] $InputObject,

      [Parameter(Mandatory = $true, Position = 0)]
      [string] $Pattern
   )

   begin{ $r = [regex]$Pattern }
   process
   {
       $ms = $r.Matches($inputObject)
       $startIndex = 0

       foreach($m in $ms)
       {
          $nonMatchLength = $m.Index - $startIndex
          Write-Host $inputObject.Substring($startIndex, $nonMatchLength) -NoNew
          Write-Host $m.Value -Back DarkRed -NoNew
          $startIndex = $m.Index + $m.Length
       }

       if($startIndex -lt $inputObject.Length)
       {
          Write-Host $inputObject.Substring($startIndex) -NoNew
       }

       Write-Host
   }
}

Upvotes: 6

Related Questions