1

Why can I do this:

Example 1

Get-NetIPConfiguration -Detailed | 
  ? IPv4DefaultGateWay -NE $null

and

Example 2

Get-NetIPConfiguration -Detailed | 
  ? {$_.IPv4DefaultGateWay -NE $null -and $_.NetAdapter.Status -EQ "Up"}

but not this

Example 3

Get-NetIPConfiguration -Detailed | 
  ? IPv4DefaultGateWay -NE $null -and NetAdapter.Status -EQ "Up"

The error message I get is as follows:

Where-Object : Parameter set cannot be resolved using the specified named parameters.
At line:2 char:3
+   ? IPv4DefaultGateWay -NE $null -and NetAdapter.Status -EQ "Up"
+   ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : InvalidArgument: (:) [Where-Object], ParameterBindingException
    + FullyQualifiedErrorId : AmbiguousParameterSet,Microsoft.PowerShell.Commands.WhereObjectCommand

but obviously the Where-Object cmdlet is capable of handling compound conditions with the use of -and and -or, so the error message is confusing.


ALSO, Why can I do this

Example 4

Get-NetIPConfiguration -Detailed | 
  ? {$_.IPv4DefaultGateWay -NE $null -and $_.NetAdapter.Status -EQ "Up"} |
  % {
    New-Object -TypeName PSObject -Property @{
        ComputerName = $_.ComputerName
        IPAddress = $_.IPv4Address.IPv4Address
    }
  }

but not be able to do this

Example 5

Get-NetIPConfiguration -Detailed | 
  ? {$_.IPv4DefaultGateWay -NE $null -and $_.NetAdapter.Status -EQ "Up"} |
  Select-Object ComputerName, IPv4Address.IPv4Address

My objective is being met by using Example 4, but that's not what I am really asking here - It is the convention of addressing properties and properties of properties that I am more concerned about here!

hsbatra
  • 133
  • 1
  • 10
  • the 1st example is using a special mode of the `ForEach-Object` cmdlet that allows ONE property to be dealt with. the 2nd is the full-function version of the same cmdlet. – Lee_Dailey Jul 17 '21 at 10:40
  • You can only refer to the property as a whole meaning, that when you try to *dot reference* (*dot notation*) the property, one, its incorrect syntax if not in a script block (*and someone correct me if I'm wrong* ). Then two, you're trying to evaluate the properties of that value which have none besides it being the value itself. That's why when it's in a script block, you use *dot notation* to reference the property, and filter against that. In summary: *`Where-Object` is expecting properties as a whole and not the value it holds when filtering*. – Abraham Zinala Jul 17 '21 at 13:13
  • @AbrahamZinala - You said "Where-Object is expecting properties as a whole and not the value it holds when filtering.", however in Ex1, ?/Where/Where-Object can filter on IPv4DefaultGateway, but in Ex3 it errors out when NetAdapter.Status -EQ "Up" is added. IPv4DefaultGateway and NetAdapter are both properties of NetIPConfiguration, yet the behavior is different. That is the crux of my question. Why does Ex1 work without the $_ prefix, and Ex3 fails? I know that $_ is the Current Object passed in from the left of the pipe symbol, its why it can be implicitly referenced in Ex1 but not in Ex3? – hsbatra Jul 17 '21 at 15:33
  • It's just bad interpretation on my end. The property as a whole, contains the value, which `-Property` filters for. That is the point of `-FilterScript` for child properties within a property. `? name -eq 'something'` defaults to `-Property` which expects a property *as is*. This is why `-FilterScript` exists to allow more complex requirements to be done, even if that includes referencing a child property of a property; which seems to have been made intentionally this way. – Abraham Zinala Jul 17 '21 at 15:51

1 Answers1

4

The differences ultimately stem from PowerShell's two fundamental parsing modes, explained in this answer; in short:

  • argument mode is shell-like, for invoking commands with whitespace-separated arguments, with support for unquoted strings.

  • expression mode works like a traditional programming language, with quoted strings, operators, loop constructs, ...

In both your examples, commands are invoked (more specifically, cmdlets, namely Where-Object (whose built-in aliases are ? and where) and Select-Object (whose built-in alias is select), so what is being passed to them is parsed in argument mode:

  • Where-Object IPv4DefaultGateWay -NE $null uses argument mode for simplified syntax, i.e. a lower-ceremony argument-mode alternative to an expression-mode script block ({ ... }), also available with the ForEach-Object cmdlet.

    • The syntax is simpler:

      • No need for enclosure in { ... }
      • No need to refer to the input object at hand via the automatic $_ variable, just using the property name by itself is enough.
    • But it is limited:

      • You can only perform a single operation, on a single property.[1]
      • That property must be an immediate property (e.g., IPv4DefaultGateWay, not a nested one (e.g. NetAdapter.Status) - more on that below.
  • Select-Object too has the constraint that a given property name must be an immediate property.

    • The only way to work around that is via a calculated property, implemented via a hashtable whose Expression entry contains a script block that calculates the property value for each input object - see this answer.

Why nested property access (e.g., NetAdapter.Status) isn't supported in argument-parsing mode:

An argument such as NetAdapter.Status - whether quoted or not - is passed as a string to commands, and Where-Object and Select-Object interpret such a string passed to their -Property parameter as the verbatim name of a property, not as a nested property-access expression.

That is, the equivalent of the following command, where NetAdapter.Status is parsed in argument mode:

Get-NetIPConfiguration -Detailed |
  Select-Object -ExpandProperty NetAdapter.Status

is the following expression-mode property access:

Get-NetIPConfiguration -Detailed |
  ForEach-Object { $_.'NetAdapter.Status' }

Note the '...' around NetAdapter.Status, showing that this name was used verbatim as the single and immediate property name on the input object (which doesn't work as intended).


Design rationale:

The challenge is that in argument mode no distinction is made between NetAdapter.Status and 'NetAdapter.Status' and "NetAdapter.Status" - the target command sees verbatim NetAdapter.Status in all cases, so - unlike in expression mode - the original quoting / non-quoting cannot serve to distinguish these argument forms.

However, arguably it is much more useful for cmdlets that specifically accept property names (parameter -Property) to interpret arguments such as NetAdapter.Status as nested property accessor, given that property names with embedded . chars. are rare.

Changing this behavior would be a breaking change, however, given that the following currently works, but wouldn't any longer:

PS> '{ "foo.bar": 42 }' | ConvertFrom-Json | Select-Object foo.bar

foo.bar
-------
     42

[1] The two parsing modes are so different that it would be impossible to recreate all expression-mode features in argument mode; you couldn't model the complexities of expression mode with command arguments (parameter definitions). Simplified syntax is a compromise aimed at making simple, but common use cases syntactically easier.

mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Thank you for such a detailed and well-explained answer (with references to boot)! It never ceases to amaze me about the number of people who are experts in their areas who freely contribute their knowledge to educate the rest of us! Much obliged!! – hsbatra Jul 26 '21 at 16:53
  • 1
    I am glad to hear the answer is useful, @hsbatra; my pleasure. Receiving such nice feedback certainly helps with staying motivated. – mklement0 Jul 26 '21 at 16:55