PowerShell Hashtable - how to select property - powershell

I need to get the value of an environment variable from a kubernetes pod. I have my values listed in a hash table.
I call
$hash["service-testurl"].spec.template.spec.containers.env
And it returns a table:
name value
---- -----
ADDR https://test.com
TOKEN 123456789
CERT_PATH public-certs/test
ENVIRONMENT dev
I need to get https://test.com into a variable in my ps1 script, but i'm not sure how to get this value. (consider that for each deployment the url will be different, like abc.com, def.com, ghj.com... so i can't filter by the name test.com)
I was looking for something like $hash["service-testurl"].spec.template.spec.containers.env.name["ADDR"].value
Running $hash["service-testurl"].spec.template.spec.containers.env.PSTypeNames returns
System.Object[]
System.Array
System.Object

To complement your own effective solution:
Even though your display output of $hash["service-testurl"].spec.template.spec.containers.env looks like the representation of a (single) hashtable, the value is actually:
(a) an array, as your diagnostic output with .pstypenames demonstrates,
(b) whose elements are (presumably) [pscustomobject] instances that each have a a .name and a .value property (an easy way to tell is that the display output's column headers are name and value, whereas with hashtables they would be Name and Value).
Leaving aside that the identifier ADDR is a property value rather than a property / key name in your case, you fundamentally cannot use key-based index notation (['ADDR']) on an array - that generally only works on a (single) hashtable (or, more generally, dictionary).[1]
In your case, you need to find the array element whose .name property value is 'ADDR', which then allows you to return its .value property value.
For collections already in memory, the intrinsic .Where() method (as used in your own solution) is a more efficient - and more flexible - alternative to filtering a collection via the Where-Object cmdlet.
It will often not matter in practice, but you can optimize a .Where() call to stop filtering once the first match has been found, if you expect or are only interested in one match:
$hash["service-testurl"].spec.template.spec.containers.env.Where(
{ $_.name -eq 'ADDR' },
'First'
).value
Note that .Where() always returns an array-like collection, even if only a single value is matched - see this answer for details. As such, the .value property access is attempted on that collection, which, however, still works as intended, courtesy of the PowerShell feature known as member-access enumeration.
Note how using (...) around the arguments is now a syntactic necessity.
While with only a single argument - the filter script block ({ ... }) - you can get away with not using (...) - .Where{ $_.name -eq 'ADDR' } as shorthand for .Where({ $_.name -eq 'ADDR' }) - omitting the (...) is problematic for two reasons:
Given that the Where-Object cmdlet can also be referred to as Where (via a built-in alias), the two command forms could be confused, and given that Where-Object allows and is typically used with a space separating the command name from its script-block argument (e.g, 1..3 | Where { $_ -eq 2 }, it is tempting to also try to use a space with the .Where() method, which does not work:
# !! BROKEN, due to space before "{"
(1..3).Where { $_ -eq 2 }
If you add another argument later, you need to remember to use (...)
[1] The fact that key-based index notation does not work with member-access enumeration, i.e. doesn't work on an array of hashtables (only dot notation does, which PowerShell supports for hashtables too) could be considered an inconsistency; e.g. #( #{ foo=1 } ).foo works (dot notation), but #( #{ foo=1 } )['foo'] does not, due to the array wrapper.
However, this inconsistency was declared to be by design - see GitHub issue #17514.

I was able to do it with something similar that #iRon proposed:
$hash["service-testurl"].spec.template.spec.containers.env.where{$_.name -eq 'ADDR'}.value
Thanks!

Related

PowerShell 5.1 Can someone please explain hashtable and splatting

Given:
PowerShell 5.1
I'm having a little trouble understanding hashtable and splatting. When splatting are use using a hash table to do that or is it something completely different?
I have the following code:
$hashtable1 = #{}
$hashtable1.add('PROD',#{ FirstName = 'John'; LastName = 'Smith'})
function Main() {
$sel = $hashtable1['PROD']
Function1 $sel
Function2 #sel
}
function Function1([hashtable] $x) {
"Value: $($x.LastName)"
$x.FirstName
}
function Function2([string] $firstName) {
"Value: $($firstName)"
}
Main
There's good information in the existing answers, but let me attempt a focused summary:
The answer to your actual question is:
Yes, #{ FirstName = 'John'; LastName = 'Smith' } is a hashtable too, namely in the form of a declarative hashtable literal - just like #{} is an empty hashtable literal (it constructs an instance that initially has no entries).
A hashtable literal consists of zero or more key-value pairs, with = separating each key from its value, and pairs being separated with ; or newlines.
Keys usually do not require quoting (e.g. FirstName), except if they contain special characters such as spaces or if they're provided via an expression, such as a variable reference; see this answer for details.
This contrasts with adding entries to a hashtable later, programmatically, as your $hashtable1.Add('PROD', ...) method call exemplifies (where PROD is the entry key, and ... is a placeholder for the entry value).
Note that a more convenient alternative to using the .Add() method is to use an index expression or even dot notation (property-like access), though note that it situationally either adds an entry or updates an existing one: $hashtable1['PROD'] = ... or $hashtable1.PROD = ...
The answer to the broader question implied by your question's title:
PowerShell's hashtables are a kind of data structure often called a dictionary or, in other languages, associative array or map. Specifically, they are case-insensitive instances of the .NET [hashtable] (System.Collections.Hashtable) type, which is a collection of unordered key-value pair entries. Hashtables enable efficient lookup of values by their associated keys.
Via syntactic sugar [ordered] #{ ... }, i.e. by placing [ordered] before a hashtable literal, PowerShell offers a case-insensitive ordered dictionary that maintains the entry-definition order and allows access by positional index in addition to the usual key-based access. Such ordered hashtables are case-insensitive instances of the .NET System.Collections.Specialized.OrderedDictionary type.
A quick example:
# Create an ordered hashtable (omit [ordered] for an unordered one).
$dict = [ordered] #{ foo = 1; bar = 'two' }
# All of the following return 'two'
$dict['bar'] # key-based access
$dict.bar # ditto, with dot notation (property-like access)
$dict[1] # index-based access; the 2nd entry's value.
Splatting is an argument-passing technique that enables passing arguments indirectly, via a variable containing a data structure encoding the arguments, which is useful for dynamically constructing arguments and making calls with many arguments more readable.
Typically and robustly - but only when calling PowerShell commands with declared parameters - that data structure is a hashtable, whose entry keys must match the names of the target command's parameters (e.g., key Path targets parameter -Path) and whose entry values specify the value to pass.
In other words: This form of splatting uses a hashtable to implement passing named arguments (parameter values preceded by the name of the target parameter, such as -Path /foo in direct argument passing).
A quick example:
# Define the hashtable of arguments (parameter name-value pairs)
# Note that File = $true is equivalent to the -File switch.
$argsHash = #{ LiteralPath = 'C:\Windows'; File = $true }
# Note the use of "#" instead of "$"; equivalent to:
# Get-ChildItem -LiteralPath 'C:\Windows' -File
Get-ChildItem #argsHash
Alternatively, an array may be used for splatting, comprising parameter values only, which are then passed positionally to the target command.
In other words: This form of splatting uses an array to implement passing positional arguments (parameter values only).
This form is typically only useful:
when calling PowerShell scripts or functions that do not formally declare parameters and access their - invariably positional - arguments via the automatic $args variable
when calling external programs; note that from PowerShell's perspective there's no concept of named arguments when calling external programs, as PowerShell's knows nothing about the parameter syntax in that case, and all arguments are simply placed on the process command line one by one, and it is up to the target program to interpret them as parameter names vs. values.
A quick example:
# Define an array of arguments (parameter values)
$argsArray = 'foo', 'bar'
# Note the use of "#" instead of "$", though due to calling an
# *external program* here, you may use "$" as well; equivalent to:
# cmd /c echo 'foo' 'bar'
cmd /c echo #argsArray
#postanote has some very good links about hashtables and splatting and are good reads. Taking your examples, you have two different functions. One to handle hashtables as a parameter, and the second one that can only handle a single string parameter. The hashtable cannot be used to pass parameters to the second function, e.g.:
PS C:\> Function2 $sel
Value: System.Collections.Hashtable
Conceptually, the real difference between using hashtables and splatting is not about how you are using them to pass information and parameters to functions, but how the functions and their parameters receive the information.
Yes, certain functions can have hashtables and arrays as parameters, but, typically in 98% of the cases, functions don't use hashtables as a named parameter to get its values.
For ex. Copy-Item doesn't use hash tables as a parameter. If it did, would you want to do this every time you want to copy anything:
$hashtable = #{
Path = "C:\Temp\myfile.txt",
Destination = "C:\New Folder\"
}
Copy-Item -Parameters $hashtable
No, instead, you want the parameters as strings, so you can make it a much easier one liner:
Copy-Item -Path "C:\Temp\myfile.txt" -Destination "C:\New Folder\"
It makes more sense to most people to deal with individual strings as opposed to a generic, large, hashtable "config" to pass. Also, by separating the parameters as separate strings, integers, floats, chars, etc. it is also easier to do validation, default values, mandatory/not mandatory parameters, etc.
Now despite saying all that, there is a situation where you have certain functions with lots of parameters (e.g. sending an email message), or you want to do something multiple times (e.g. copying/moving lots of files from a CSV file). In that case, using a hashtable, and/or arrays, and/or arrays of hashtables, would be useful.
This is where splating comes in. It takes a hashtable and instead of treating it like you are passing a single value (i.e. why Function2 $sel returns System.Collections.Hashtable), the # signt tells PowerShell that it is a collection of values, and to use the collection to try and match to the parameters of the function. That's why passing the hashtable to Function2 doesn't work, but splatting works e.g.:
PS C:\> Function2 #sel
Value: John
In this case, it takes the hashtable $sel and by using splatting #sel PowerShell now knows to not pass the hashtable as-is, but to open up the collection and to matche the $sel.FirstName to the -Firstname parameter.

How to pipe results into output array

After playing around with some powershell script for a while i was wondering if there is a version of this without using c#. It feels like i am missing some information on how to pipe things properly.
$packages = Get-ChildItem "C:\Users\A\Downloads" -Filter "*.nupkg" |
%{ $_.Name }
# Select-String -Pattern "(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)" |
# %{ #($_.Matches[0].Groups["packageId"].Value, $_.Matches[0].Groups["version"].Value) }
foreach ($package in $packages){
$match = [System.Text.RegularExpressions.Regex]::Match($package, "(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)")
Write-Host "$($match.Groups["packageId"].Value) - $($match.Groups["version"].Value)"
}
Originally i tried to do this with powershell only and thought that with #(1,2,3) you could create an array.
I ended up bypassing the issue by doing the regex with c# instead of powershell, which works, but i am curious how this would have been done with powershell only.
While there are 4 packages, doing just the powershell version produced 8 lines. So accessing my data like $packages[0][0] to get a package id never worked because the 8 lines were strings while i expected 4 arrays to be returned
Terminology note re without using c#: You mean without direct use of .NET APIs. By contrast, C# is just another .NET-based language that can make use of such APIs, just like PowerShell itself.
Note:
The next section answers the following question: How can I avoid direct calls to .NET APIs for my regex-matching code in favor of using PowerShell-native commands (operators, automatic variables)?
See the bottom section for the Select-String solution that was your true objective; the tl;dr is:
# Note the `, `, which ensures that the array is output *as a single object*
%{ , #($_.Matches[0].Groups["packageId"].Value, $_.Matches[0].Groups["version"].Value) }
The PowerShell-native (near-)equivalent of your code is (note tha the assumption is that $package contains the content of the input file):
# Caveat: -match is case-INSENSITIVE; use -cmatch for case-sensitive matching.
if ($package -match '(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)') {
"$($Matches['packageId']) - $($Matches['Version'])"
}
-match, the regular-expression matching operator, is the equivalent of [System.Text.RegularExpressions.Regex]::Match() (which you can shorten to [regex]::Match()) in that it only looks for (at most) one match.
Caveat re case-sensitivity: -match (and its rarely used alias -imatch) is case-insensitive by default, as all PowerShell operators are; for case-sensitive matching, use the c-prefixed variant, -cmatch.
By contrast, .NET APIs are case-sensitive by default; you'd have to pass the [System.Text.RegularExpressions.RegexOptions]::IgnoreCase flag to [regex]::Match() for case-insensitive matching (you may use 'IgnoreCase', which PowerShell auto-converts for you).
As of PowerShell 7.2.x, there is no operator that is the equivalent of the related return-ALL-matches .NET API, [regex]::Matches(). See GitHub issue #7867 for a green-lit but yet-to-be-implemented proposal to introduce one, named -matchall.
However, instead of directly returning an object describing what was (or wasn't) matched, -match returns a Boolean, i.e. $true or $false, to indicate whether matching succeeded.
Only if -match returns $true does information about a match become available, namely via the automatic $Matches variable, which is a hashtable reflecting the matching parts of the input string: entry 0 is always the full match, with optional additional entries reflecting what any capture groups ((...)) captured, either by index, if they're anonymous (starting with 1) or, as in your case, for named capture groups ((?<name>...)) by name.
Syntax note: Given that PowerShell allows use of dot notation (property-access syntax) even with hashtables, the above command could have used $Matches.packageId instead of $Matches['packageId'], for instance, which also works with the numeric (index-based) entries, e.g., $Matches.0 instead of $Matches[0]
Caveat: If an array (enumerable) is used as the LHS operand, -match' behavior changes:
$Matches is not populated.
filtering is performed; that is, instead of returning a Boolean indicating whether matching succeeded, the subarray of matching input strings is returned.
Note that the $Matches hashtable only provides the matched strings, not also metadata such as index and length, as found in [regex]::Match()'s return object, which is of type [System.Text.RegularExpressions.Match].
Select-String solution:
$packages |
Select-String '(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)' |
ForEach-Object {
"$($_.Matches[0].Groups['packageId'].Value) - $($_.Matches[0].Groups['version'].Value)"
}
Select-String outputs Microsoft.PowerShell.Commands.MatchInfo instances, whose .Matches collection contains one or more [System.Text.RegularExpressions.Match] instances, i.e. instances of the same type as returned by [regex]::Match()
Unless -AllMatches is also passed, .Matches only ever has one entry, hence the use of [0] to target that entry above.
As you can see, working with Select-Object's output objects requires you to ultimately work with the same .NET type as when you call [regex]::Match() directly.
However, no method calls are required, and discovering the properties of the output objects is made easy in PowerShell via the Get-Member cmdlet.
If you want to capture the matches in a jagged array:
$capturedStrings = #(
$packages |
Select-String '(?<packageId>[^\d]+)\.(?<version>[\w\d\.-]+)(?=.nupkg)' |
ForEach-Object {
# Output an array of all capture-group matches,
# *as a single object* (note the `, `)
, $_.Matches[0].Groups.Where({ $_.Name -ne '0' }).Value
}
)
This returns an array of arrays, each element of which is the array of capture-group matches for a given package, so that $capturedStrings[0][0] returns the packageId value for the first package, for instance.
Note:
$_.Matches[0].Groups.Where({ $_.Name -ne '0' }).Value programmatically enumerates all capture-group matches and returns an their .Value property values as an array, using member-access enumeration; note how name '0' must be excluded, as it represents the whole match.
With the capture groups in your specific regex, the above is equivalent to the following, as shown in a commented-out line in your question:
#($_.Matches[0].Groups['packageId'].Value, $_.Matches[0].Groups['version'].Value)
, ..., the unary form of the array-construction operator, is used as a shortcut for outputting the array (symbolized by ... here) as a whole, as a single object. By default, enumeration would occur and the elements would be emitted one by one. , ... is in effect a shortcut to the conceptually clearer Write-Output -NoEnumerate ... - see this answer for an explanation of the technique.
Additionally, #(...), the array subexpression operator is needed in order to ensure that a jagged array (nested array) is returned even in the event that only one array is returned across all $packages.

Powershell how to subtract a variable from another variable, both contain the Exchange Mailbox Object Identity

I have two variables both which contain the Get-Mailbox object "Identity" of user accounts. I need to subtract the contents of one from the other IE:
$termednofwd = (domain.local/OUname/SubOU/Users/first1 last1, domain.local/OUname/SubOU/first2 last2)
$termedfmr = (domain.local/OUname/SubOU/Users/first1 last1)
I want something that would subtract the contents of $termedfmr from $termednofwd giving something like the below. Compare-Object only lists the contents that are in both, I basically need to subtract what is in both from the first variable.
essentially:
$termednofwdnofmr = $termednofwd - $termedfmr resulting in this:
$termednofwdnofmr = (domain.local/OUname/SubOU/first2 last2)
In set theory terms, you're looking for the relative complement between two collections, which Compare-Object can provide, although it requires additional effort:
By default, Compare-Object provides the symmetric difference between two sets, i.e. it lists the union of relative complements; that is, given two sets A and B, it lists both those elements of B not present in A and those elements of A not present in B, and it uses the .SideIndicator property to indicate which is which:
'<=' indicates objects unique to set A (-ReferenceObject argument, or first positional argument), whereas
'=>' indicates elements unique to set B (-DifferenceObject argument, or second positional argument).
Therefore, you need to filter the output objects by their .SideIndicator values.
The -PassThru switch additionally ensures that the input objects are passed through (as opposed to wrapping them in a [pscustomobject] instance whose .InputObject contains them):
$termednofwd = 'domain.local/OUname/SubOU/Users/first1 last1',
'domain.local/OUname/SubOU/first2 last2'
$termedfmr = 'domain.local/OUname/SubOU/Users/first1 last1'
# Return the elements in $termednofwd that aren't also present in $termedfmr
Compare-Object $termednofwd $termedfmr -PassThru |
Where-Object SideIndicator -eq '<='
The above yields 'domain.local/OUname/SubOU/first2 last2', i.e. those element(s) in $termednofwd that aren't also present in $termedfmr.
Note: The above uses strings as input objects for brevity; in your case, since you're working with the objects returned by the Get-Mailbox cmdlet and want to compare based on their .Identity property values, you need to use:
# If you only need the identity values as results.
Compare-Object $termednofwd.Identity $termedfmr.Identity -PassThru |
Where-Object SideIndicator -eq '<='
# Alternatively, if you need the whole mailbox objects.
Compare-Object -Property Identity $termednofwd $termedfmr -PassThru |
Where-Object SideIndicator -eq '<='
See also: GitHub issue #4316, which proposes enhancing Compare-Object with set operations.
So this is essentially a practical example of Set theory i.e. I'd like all the items from Set 1 that isn't in Set 2. PowerShell doesn't have direct commandlets to do that but through the power of .Net you can lervage the Microsoft Linq libraries to do this work for you.
The only trick is that you need to cast your PowerShell variables to arrays of Objects to make the function call work right:
$results = [System.Linq.Enumerable]::Except([object[]]$mainArray, [object[]]$subArray)
Lastly, the items you are comparing in the two arrays have to be comparable. For simple things like strings this is always works. If the things you are comparing are more complex objects, they may not be comparable directly.

PowerShell: What is the point of ForEach-Object with InputObject?

The documentation for ForEach-object says "When you use the InputObject parameter with ForEach-Object, instead of piping command results to ForEach-Object, the InputObject value is treated as a single object." This behavior can easily be observed directly:
PS C:\WINDOWS\system32> ForEach-Object -InputObject #(1, 2, 3) {write-host $_}
1 2 3
This seems weird. What is the point of a "ForEach" if there is no "each" to do "for" on? Is there really no way to get ForEach-object to act directly on the individual elements of an array without piping? if not, it seems that ForEach-Object with InputObject is completely useless. Is there something I don't understand about that?
In the case of ForEach-Object, or any cmdlet designed to operate on a collection, using the -InputObject as a direct parameter doesn't make sense because the cmdlet is designed to operate on a collection, which needs to be unrolled and processed one element at a time. However, I would also not call the parameter "useless" because it still needs to be defined so it can be set to allow input via the pipeline.
Why is it this way?
-InputObject is, by convention, a generic parameter name for what should be considered to be pipeline input. It's a parameter with [Parameter(ValueFromPipeline = $true)] set to it, and as such is better suited to take input from the pipeline rather passed as a direct argument. The main drawback of passing it in as a direct argument is that the collection is not guaranteed to be unwrapped, and may exhibit some other behavior that may not be intended. From the about_pipelines page linked to above:
When you pipe multiple objects to a command, PowerShell sends the objects to the command one at a time. When you use a command parameter, the objects are sent as a single array object. This minor difference has significant consequences.
To explain the above quote in different words, passing in a collection (e.g. an array or a list) through the pipeline will automatically unroll the collection and pass it to the next command in the pipeline one at a time. The cmdlet does not unroll -InputObject itself, the data is delivered one element at a time. This is why you might see problems when passing a collection to the -InputObject parameter directly - because the cmdlet is probably not designed to unroll a collection itself, it expects each collection element to be handed to it in a piecemeal fashion.
Consider the following example:
# Array of hashes with a common key
$myHash = #{name = 'Alex'}, #{name='Bob'}, #{name = 'Sarah'}
# This works as intended
$myHash | Where-Object { $_.name -match 'alex' }
The above code outputs the following as expected:
Name Value
---- -----
name Alex
But if you pass the hash as InputArgument directly like this:
Where-Object -InputObject $myHash { $_.name -match 'alex' }
It returns the whole collection, because -InputObject was never unrolled as it is when passed in via the pipeline, but in this context $_.name -match 'alex' still returns true. In other words, when providing a collection as a direct parameter to -InputObject, it's treated as a single object rather than executing each time against each element in the collection. This can also give the appearance of working as expected when checking for a false condition against that data set:
Where-Object -InputObject $myHash { $_.name -match 'frodo' }
which ends up returning nothing, because even in this context frodo is not the value of any of the name keys in the collection of hashes.
In short, if something expects the input to be passed in as pipeline input, it's usually, if not always, a safer bet to do it that way, especially when passing in a collection. However, if you are working with a non-collection, then there is likely no issue if you opt to use the -InputObject parameter directly.
Bender the Greatest's helpful answer explains the current behavior well.
For the vast majority of cmdlets, direct use of the -InputObject parameter is indeed pointless and the parameter should be considered an implementation detail whose sole purpose is to facilitate pipeline input.
There are exceptions, however, such as the Get-Member cmdlet, where direct use of -InputObject allows you to inspect the type of a collection itself, whereas providing that collection via the pipeline would report information about its elements' types.
Given how things currently work, it is quite unfortunate that the -InputObject features so prominently in most cmdlets' help topics, alongside "real" parameters, and does not frame the issue with enough clarity (as of this writing): The description should clearly convey the message "Don't use this parameter directly, use the pipeline instead".
This GitHub issue provides an categorized overview of which cmdlets process direct -InputObject arguments how.
Taking a step back:
While technically a breaking change, it would make sense for -InputObject parameters (or any pipeline-binding parameter) to by default accept and enumerate collections even when they're passed by direct argument rather than via the pipeline, in a manner that is transparent to the implementing command.
This would put direct-argument input on par with pipeline input, with the added benefit of the former resulting in faster processing of already-in-memory collections.

Undefined property name appears valid

The following two commands produce different output.
Get-ChildItem | Sort-Object -Property Length
Get-ChildItem | Sort-Object -Property Len
Len is not a member of System.IO.FileInfo. Is PowerShell matching Len to the Length member? If not, then why is there no error message saying that Len is not a property?
No, its not member of System.IO.FileInfo as you can see by adding the -Debugswitch:
Get-ChildItem | Sort-Object -Property Len -Debug
Output looks like:
DEBUG: "Sort-Object" - "Len" cannot be found in "InputObject".
I guess the reason for that is the defensive implementation of the cmdlet:
If an object does not have one of the specified properties, the
property value for that object is interpreted by the cmdlet as Null
and is placed at the end of the sort order.
To complement Martin Brandl's helpful answer with more general information:
While PowerShell's elastic syntax only applies to parameter names (e.g., specifying just -p for -Property) , not values (arguments), you do have options for completing values:
At edit time: use tab completion:
This works on the command line as well as in Visual Studio Code with the PowerShell extension installed (where you'll get IntelliSense as well), as long as PowerShell can statically infer the output type(s)[1]
of the command in the previous pipeline segment.
At runtime:
Sort-Object and several other cmdlets allow you to use a wildcard expression to match property names:
Get-ChildItem | Sort-Object -Property Len* # "Len*" matches "Length"
Note that multiple properties may match, and that a given parameter must be explicitly designed to support wildcards (unlike in POSIX-like shells, it is not PowerShell itself that resolves the wildcards).
When accessing a nonexistent property on an object directly, no error is reported by default, and $null is returned:
(Get-Item /).Foo # Outputs $null as the value of nonexistent property "Foo"
By contrast, if Set-StrictMode -Version 2 or higher is in effect, a (statement-terminating) error is reported in that case, but note that Set-StrictMode does not apply when passing property names as arguments, such as to Sort-Object above.
As for a possible motivation for why Sort-Object doesn't enforce the existence of specified properties:
PowerShell allows you to pass objects that are any mix of types as input through the pipeline, with the objects getting passed one at a time.
(Similarly, PowerShell's default array type is [object[]], which allows you to create mixed-type arrays such as 1, 'hi', $True)
Even with (potentially) homogeneous input (such as the [System.IO.FileInfo] instances emitted by Get-ChildItem -File, for instance), a receiving command cannot detect that case up front, because it only ever sees one object at a time.
In general, cmdlets should be able to handle a mix of types among the input gracefully, and treating nonexistent properties as $null is overall the better choice, especially given that:
a cmdlet may still be able to act meaningfully on the input if at least a subset of the input objects have the property of interest (see below).
a cmdlet cannot know in advance whether that subset is empty.
Example with heterogeneous input:
Send an array of custom objects through the pipeline and sort it by property val, which one of the objects lacks:
[pscustomobject] #{ n = 'o1'; val = 2 },
[pscustomobject] #{ n = 'o2' },
[pscustomobject] #{ n = 'o3'; val = 1 } | Sort-Object val
Output:
n val
- ---
o3 1
o1 2
o2
Sorting was performed among all the input objects that do have a .val property, whereas those that don't were placed at the end, as stated in the quote from Sort-Object's documentation in Martin's answer.
[1] This should be true of all built-in cmdlets; to ensure that it works with custom functions, define them with [OutputType(<type>)] attributes - see this answer of mine for more.