Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 895
Kevin Avatar asked Sep 26 '12 20:09

Kevin


2 Answers

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
   }
}
like image 150
latkin Avatar answered Sep 30 '22 01:09

latkin


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

like image 24
cyborg Avatar answered Sep 30 '22 01:09

cyborg