nam
nam

Reputation: 23749

Why PowerShell $Matches Hashtable automatic variable is not returning all matches?

I'm using Named Captures in PowerShell. But the following example is not returning all matches. All the lines in the following code return correct output - except the lines 7 and 8. I was expecting $Matches.1 and $Matches.2 to return v and A respectively. But these two lines of code return nothing.

Question: What I may be missing here or maybe not correctly understanding here?

Ref: Lates version 7.4 of PowerShell

 PS> $string = 'The last logged on user was A\v'
 PS> $string -match 'was (?<domain>.+)\\(?<user>.+)'
 PS> $Matches.domain #returns A
 PS> $Matches.user #returns v
 PS> $Matches.count #returns 3
 PS> $Matches.0 #returns was A\v
 PS> $Matches.1 #returns nothing
 PS> $Matches.2 #returns nothing
 PS> $Matches #returns the following output
Name Value
domain A
user v
0 was A\v

Upvotes: 2

Views: 147

Answers (1)

mklement0
mklement0

Reputation: 437688

The automatic $Matches variable is a hashtable whose entries are populated as follows:

  • The entry with key (index) 0 (type [int]) is always present, representing the whole match.

  • Additional entries are only populated if there are capture groups in the regex, as follows:

    • Any named capture group (e.g. (?<domain>.+)) is represented by an entry with a key of the same name (type [string]) - only (it doesn't also get a positional entry - see below).

      • For instance, $Matches.domain or, alternatively, $Matches['domain'], returns the substring matched by the example capture group, if any.
    • Any - remaining or only - unnamed and therefore positional capture groups (e.g. (.+)) are represented in order, with indices, namely as entries with [int]-typed keys starting with 1.

      • For instance, $Matches.1 or, alternatively, $Matches[1], would return the substring that the example capture group matched, assuming it is the first positional (unnamed) one in the regex.

It follows from the above that in your case $Matches only has 'domain' and 'user' entries beyond the always-present 0 entry, because only named capture groups, with these names, are present.

Also note that the fact that $Matches is implemented as a [hashtable] implies that its entries are inherently unordered - thus, you cannot rely on the enumeration order of the entries to be meaningful (via .GetEnumerator() for the entries as a whole, or via .Keys or .Values for the keys and values, respectively), and even the entries with keys that are conceptually indices - for the whole match and any unnamed / positional capture groups - are typically not reflected in their numerical order.


Important considerations of $Matches use:

  • It is automatically populated by:

    • -match, the regular-expression matching operator:

      • Only if the LHS is a single string (object) and a match is found, i.e. only when -match returns $true

        • Notably, a preexisting $Matches result is left untouched if $false is returned, so $Matches should only be consulted after receiving $true.

        • -match only ever looks for one match in the input string.

          • GitHub issue #7867 is a green-lit, but as yet unimplemented proposal to complement it with a -matchall operator.
        • A simple example:

           $str = 'Maroon 5'
           if ($str -match '(o)+. \d') {
             Write-Verbose -Verbose "Matched: $str - `$Matches contains:"
             $Matches
           } else {
             # NOTE: $Matches was NOT populated
             #       nor were previous results reset.
             Write-Verbose -Verbose "Did not match: $str"
           }
          
      • By contrast, with array-valued LHS, -match - like other comparison operators - acts as a filter that returns the sub-array of matching input strings, and doesn't populate $Matches at all (nor resets any previous results).

    • the switch statement, if used with the -Regex switch:

      • Specifically, string-based branch conditionals are then treated as regexes and the associated action block - which is only entered in case of a match - then has $Matches populated.

      • A simple example:

         switch -Regex ('Maroon 5', 'foo') {
           '(o)+. \d' { Write-Verbose -Verbose "Matched: $_ - `$Matches contains:"; $Matches }
           default    { Write-Verbose -Verbose "Did not match: $_"  }
         }
        

Upvotes: 5

Related Questions