Powershell: The -Like and -NotLike comparison operators should allow for an array of values

Created on 30 Aug 2016  路  6Comments  路  Source: PowerShell/PowerShell

Steps to reproduce

Imagine having a list of computer names from AD, like ABC0001, ABC0002, DEF0001, XYZ0001 and so on, and you only want to keep ABC* and XYZ*.

Expected behavior

We want to filter so we'll use -Like, and try to use an array of comparison values.

Get-ADComputer | Where-Object ComputerName -like ABC*, XYZ*

ABC0001
ABC0002
XYZ0001
...

Actual behavior

Single comparisons work

Get-ADComputer  | Where-Object ComputerName -like ABC*

ComputerName
------------
ABC0001
ABC0002
ABC0003
ABC0004
ABC0005
Get-ADComputer | Where-Object ComputerName -like XYZ*

ComputerName
------------
XYZ0001
XYZ0002
XYZ0003
XYZ0004
XYZ0005

But combining them does not work

Where-Object ComputerName -like ABC*, XYZ*
PS>


With PowerShell being object based and having such a uniform syntax, it _feels_ like this should work, so people tend to be confused (like me) and when doing really complex filters, write garbage like this:

Or read from an arcane tome of RegEx Spells and come up with devilry like this example, from the wizard Dave Wyatt.

? { $_.e -match '^(atl|na|mdm|aus|okc|ssc|ga1|ok1|fns|acs)' }

Environment data

> $PSVersionTable

Name                           Value                                                                                   
----                           -----                                                                                   
PSVersion                      5.1.14393.103                                                                           
PSEdition                      Desktop                                                                                 
PSCompatibleVersions           {1.0, 2.0, 3.0, 4.0...}                                                                 
BuildVersion                   10.0.14393.103                                                                          
CLRVersion                     4.0.30319.42000                                                                         
WSManStackVersion              3.0                                                                                     
PSRemotingProtocolVersion      2.3                                                                                     
SerializationVersion           1.1.0.1                                                                                 



Issue-Discussion Issue-Enhancement Up-for-Grabs WG-Language

Most helpful comment

But combining them does not work

Where-Object ComputerName -like ABC*, XYZ*
PS>

With PowerShell being object based and having such a uniform syntax, it feels like this should work

It does work, it's telling you that you don't have a computer called "abc1, xyz2" or similar.

In this example -like is not an operator, it's a parameter of Where-Object so the pattern is parsed in parameter parsing mode. It becomes a single string pattern containing a comma and a space. It matches text "abc anything comma space xyz anything":

@{test='abc1, xyz2'} |Where-Object test -like abc*, xyz*

That is, it does work in line with the uniform syntax, it's just misleading you about which syntax applies.

This is different from the operator used with quotes -like "abc*", "xyz*" which is parsed as lzybkr describes - an array joined together using the value of $OFS as the separator (space by default), so the comma doesn't become a part of the pattern but a space does.

And with the operator, trying to use the example with no quotes @('zzz', 'abc1', 'xyz2') -like abc*, xyz* is a parse error.

If the suggested -likeany became a parameter on where-object then this slightly misleading pattern would apply to it in the same way:

|where test -likeany abc*                # abc* wildcard
|where test -likeany abc*, def*          # "abc*, def*" wildcard surprise
|where test -likeany "abc*", "def*"      # multiple wildcards "abc*" and "def*"

And the original issue would still be here: Where-Object test -likeany abc*, def* doesn't work as expected, because the expectation was not thinking about parameter parsing mode.

Presumably with a new operator there would be variants -clikeany, -ilikeany?
What about -notlikeany, -cnotlikeany, -inotlikeany?

Should that pattern extend to -matchany, -notmatchany, -containsany, -notcontainsany, -anyin, -notanyin) and their case variants? -likeall?

If not, then you get the weird situation where -likeany becomes the de-facto -containsany:

$collection -contains 'a'
#How do I test if it contains an 'a' or a 'b'? 

#you can use:
$collection -likeany 'a', 'b'

# > why are they named differently?
# contains is an object in a collection, likeany is a string match but it works for a collection of strings
# that will bite you one day.
# oh and it acts as a filter instead of returning a [bool]..

I would vote for a breaking change to make it work with -like, personally. Most, if not all, the existing operators work with an array on one side or the other, without needing a variant on the name specifically for the case of working with an array. And possibly adjust -contains @('a', 'b') similarly, so it doesn't join the array.


I see the original issue - matching multiple patterns - appears over and over on StackOverflow as a thing people trip over, expecting there to be a clean way and there isn't. This seems like a helpful fix for that. But..

an arcane tome of RegEx Spells and come up with devilry like this example

In defence of regex, it is a language specifically for describing patterns in text, and the use of ^, () and | is not as arcane and devilish as claimed; regex is a first class citizen in PowerShell already - in common operators like -split and -replace, and it's available in every popular language, many popular text editors, and many other data processing tools.

At what point is it reasonable to say "honestly, your next move should be: spend an hour with a regex tutorial, it's really not that bad"? Is there anything that can be done to make it less intimidating?

All 6 comments

I usually write it in this form to avoid repeating -and and -notlike

$patterns = @(
    'atl*',
    'na*',
    'mdm*'
    # etc
)

$w | ? {
    $e = $_.e
    -not ($patterns | ? ($e -like $_))
}

Changing the meaning of arrays could be a (probably rare but obscure) breaking change.

PS> "a b" -like ('a','b')
True

A new operator, e.g. -likeany might be more readable. Note that these examples are similar to part of the generalized splatting rfc, which actually suggest another syntax that wouldn't be breaking: $a -like @b.

For now, you can workaround this with -NotMatch and -Match. Instead of using

? { $_.e -match '^(atl|na|mdm|aus|okc|ssc|ga1|ok1|fns|acs)' }

You can write

? e -Match '^(atl|na|mdm|aus|okc|ssc|ga1|ok1|fns|acs)'
? e -Match "^($(('atl', 'na', 'mdm', 'aus', 'okc', 'ssc', 'ga1', 'ok1', 'fns', 'acs') -join '|'))"
# But it seems that you want -NotMatch
# as in your long "if"s?

This should be submitted as an RFC

But combining them does not work

Where-Object ComputerName -like ABC*, XYZ*
PS>

With PowerShell being object based and having such a uniform syntax, it feels like this should work

It does work, it's telling you that you don't have a computer called "abc1, xyz2" or similar.

In this example -like is not an operator, it's a parameter of Where-Object so the pattern is parsed in parameter parsing mode. It becomes a single string pattern containing a comma and a space. It matches text "abc anything comma space xyz anything":

@{test='abc1, xyz2'} |Where-Object test -like abc*, xyz*

That is, it does work in line with the uniform syntax, it's just misleading you about which syntax applies.

This is different from the operator used with quotes -like "abc*", "xyz*" which is parsed as lzybkr describes - an array joined together using the value of $OFS as the separator (space by default), so the comma doesn't become a part of the pattern but a space does.

And with the operator, trying to use the example with no quotes @('zzz', 'abc1', 'xyz2') -like abc*, xyz* is a parse error.

If the suggested -likeany became a parameter on where-object then this slightly misleading pattern would apply to it in the same way:

|where test -likeany abc*                # abc* wildcard
|where test -likeany abc*, def*          # "abc*, def*" wildcard surprise
|where test -likeany "abc*", "def*"      # multiple wildcards "abc*" and "def*"

And the original issue would still be here: Where-Object test -likeany abc*, def* doesn't work as expected, because the expectation was not thinking about parameter parsing mode.

Presumably with a new operator there would be variants -clikeany, -ilikeany?
What about -notlikeany, -cnotlikeany, -inotlikeany?

Should that pattern extend to -matchany, -notmatchany, -containsany, -notcontainsany, -anyin, -notanyin) and their case variants? -likeall?

If not, then you get the weird situation where -likeany becomes the de-facto -containsany:

$collection -contains 'a'
#How do I test if it contains an 'a' or a 'b'? 

#you can use:
$collection -likeany 'a', 'b'

# > why are they named differently?
# contains is an object in a collection, likeany is a string match but it works for a collection of strings
# that will bite you one day.
# oh and it acts as a filter instead of returning a [bool]..

I would vote for a breaking change to make it work with -like, personally. Most, if not all, the existing operators work with an array on one side or the other, without needing a variant on the name specifically for the case of working with an array. And possibly adjust -contains @('a', 'b') similarly, so it doesn't join the array.


I see the original issue - matching multiple patterns - appears over and over on StackOverflow as a thing people trip over, expecting there to be a clean way and there isn't. This seems like a helpful fix for that. But..

an arcane tome of RegEx Spells and come up with devilry like this example

In defence of regex, it is a language specifically for describing patterns in text, and the use of ^, () and | is not as arcane and devilish as claimed; regex is a first class citizen in PowerShell already - in common operators like -split and -replace, and it's available in every popular language, many popular text editors, and many other data processing tools.

At what point is it reasonable to say "honestly, your next move should be: spend an hour with a regex tutorial, it's really not that bad"? Is there anything that can be done to make it less intimidating?

I would vote for a breaking change to make it work with -like, personally.

I agree, given that:

  • this strikes me as a clear-cut Bucket 3: Unlikely Grey Area change

  • it avoids introducing yet another operator

  • extending the RHS to support arrays seems like a natural extension of the existing functionality.

This seems like a helpful fix for that.
But...

Depending on your requirements, sometimes regexes are your only choice.

But even if you have a decent grasp of regexes, it is convenient to use the less noisy and conceptually simpler wildcard patterns in cases where that is enough - which is often.

Wildcards are much more pervasive in PowerShell (think parameters that support them; e.g. Get-Process ba*) and therefore a familiar feature.


That said, we could similarly introduce array RHS support for the -match operator as well:

To me, there's something cleaner about writing

'foo vs. bar' -match 'foo', 'bar'

than (what I propose the above would be the equivalent of):

'foo vs. bar' -match '(?:foo|bar)'

For symmetry we'd have to add this to the potential future -matchall operator as well - see #7867; though perhaps the naming of that new operator should be revisited in this context.


On a side note, @HumanEquivalentUnit:

I get the difference in parsing modes (of argument-mode ABC*, XYZ* vs. expression-mode 'ABC*', 'XYZ*'), but, quoting aside, _both_ are parsed as _arrays_ that are converted to a single string in which the elements are concatenated with $OFS, which yields string 'ABC* XYZ*' by default.

# OK: Quoted argument, literal match
PS> @{test='abc1, xyz2'} |Where-Object test -like 'abc1, xyz2'
Name                           Value
----                           -----
test                           abc1, xyz2

# NO match with unquoted argument, because array abc1, xyz2 is converted
# to string 'abc1 xyzz2'
PS> @{test='abc1, xyz2'} |Where-Object test -like abc1, xyz2
# !! no match

# Therefore, a comma-less LHS value does match:
PS> @{test='abc1 xyz2'} |Where-Object test -like abc1, xyz2
Name                           Value
----                           -----
test                           abc1 xyz2

# Example with $OFS
PS> & { $OFS='#'; @{test='abc1#xyz2'} |Where-Object test -like abc1, xyz2 }
Name                           Value
----                           -----
test                           abc1#xyz2


Was this page helpful?
0 / 5 - 0 ratings