Search code examples
powershell

Retrieving $Matches from -match in Switch condition


I have a switch statement that is rather long (over 120 codes) and is a simple string match to where a certain string/code will cause a certain set of commands to execute (so no need for regex). But, as I was thinking about it, it occurred to me that I would like to pass usernames through same switch to do the same task per each user, using their username.

I attempted to do this by placing a -match inside the condition with a named capturing group regex to grab the username, but it failed:

$code1 = 'Descriptive text describing code 1'
$code2 = 'Descriptive text describing code 2'
$userName1 = 'user_UserName1'
$userName2 = 'user_UserName2'
$Codes = $code1, $code2, $userName1, $userName2
switch ($Codes) {
   $code1 {
      Write-Host 'Do work required by code1'
      continue
   }
   $code2 {
      Write-Host 'Do work required by code2'
      continue
   }
   {$_ -match '^user_(?<UserName>.*)$'} {
      Write-Host ('Processing user: {0}' -f $Matches.UserName)
      continue
   }
   default {'Other'}
}

Failed results:

Do work required by code1
Do work required by code2
Processing user:
Processing user:

Where I expected this:

Do work required by code1
Do work required by code2
Processing user: UserName1
Processing user: UserName2

And while the following does work, as you can see, I'm having to execute the -match twice:

$Codes = 'user_UserName1', 'user_UserName2'
switch ($Codes) {
   {$_ -match '^user_(?<UserName>.*)$'} {
      if($_ -match '^user_(?<UserName>.*)$') {
         Write-Host ('Processing user: {0}' -f $Matches.UserName)
      }
      continue
   }
   default {'Other'}
}

I'm guessing the condition is a script block executed in its own scope and $Matches isn't available outside that scope. I can't return $userName from within the script block since I'm sure the that is how the $true/$false value is received by switch.

Is there a simple method to avoid matching once for switch and then again to make $Matches available?

Final Code Based on Santiago's answer and comments:

Using information from here, created an extension class for extending the string type to include reEscape method. This allows me to simply add .reEscape() to a string to create the regex escaped version of the string. For example, 'This is a test.'.reEscape() creates "This\ is\ a\ test\.":

class MyExtensions {
   static [string]reEscape([PSObject]$source) {
      return [regex]::Escape($source)
   }
}
Update-TypeData -TypeName 'System.String' -MemberName 'reEscape' -MemberType CodeMethod -Value ([MyExtensions].GetMethod('reEscape')) -Force

For testing, added characters that regex will need escaped ((, ), and .) to $code1 and $code2:

$code1 = 'Descriptive text (with special characters) describing code 1.'
$code2 = 'Descriptive text (with special characters) describing code 2.'
$userName1 = 'user_UserName1'
$userName2 = 'user_UserName2'
$Codes = $code1, $code2, $userName1, $userName2

Upgraded the switch statement to include the -regex switch, and added .reEscape() to $code1 and $code2:

switch -Regex ($Codes) {
   $code1.reEscape() {
      Write-Host 'Do work required by code1'
      continue
   }
   $code2.reEscape() {
      Write-Host 'Do work required by code2'
      continue
   }
   '^user_(?<UserName>.*)$' {
      Write-Host ('Processing user: {0}' -f $Matches.UserName)
      continue
   }
   default { 'Other' }
}

In conclusion, I'm not excited by the idea of having to add .reEscape() to the end of strings to make them safe for regex, but it's an acceptable solution.


Solution

  • I'm guessing the condition is a script block executed in its own scope and $Matches isn't available outside that scope.

    That's correct, simply proven by:

    & { $null = 'user_UserName1' -match '^user_(?<UserName>.*)$' }; $Matches
    
    # `$Matches` isn't populated in the caller scope
    

    Is there a simple method to avoid matching once for switch and then again to make $Matches available?

    switch has a -Regex parameter and the $Matches variable is populated when using it.

    # NOTE: $var = [regex]::Escape('....')
    #       can be used on variable assignment for literal match on switch conditions
    
    $code1 = 'Descriptive text describing code 1'
    $code2 = 'Descriptive text describing code 2'
    $userName1 = 'user_UserName1'
    $userName2 = 'user_UserName2'
    $Codes = $code1, $code2, $userName1, $userName2
    
    switch -Regex ($Codes) {
        $code1 {
            Write-Host 'Do work required by code1'
            continue
        }
        $code2 {
            Write-Host 'Do work required by code2'
            continue
        }
        '^user_(?<UserName>.*)$' {
            Write-Host ('Processing user: {0}' -f $Matches.UserName)
            continue
        }
        default {
            'Other'
        }
    }