As a PowerShell user, I would like to have a consistent way to check if a property exists on an object in a strict mode, whatever type of object it is.
Currently this works the same way for Hashtable/custom object/etc:
$x.Property
And this works to check if the property exists, if I am not in a strict mode:
if ($x.Property) { ... }
However in strict mode, there is no good alternative.
$x.PSObject.Properties
does not work for Hashtable
and other similar cases. Contains
/ContainsKey
works for hashtables and dictionaries, but not for objects.
I would like to have some special way to check that property exists that would work in the same way as $x.Property
-- for custom objects, hashtables and anything else.
Note that I don't agree with #9086 which suggests to completely remove existence checks in strict mode -- in a lot of cases they are very useful and catch valid issues. What I want is to be able to do a check easily, something like:
if ($x -hasproperty 'A') { ... }
^ That is an example, I am not too tied to any specific syntax.
Does this work for you ?
>$host.psobject.Properties.name -contains "UI"
True
>$host.psobject.Properties.name -contains "stuff"
False
If the primary goal is to be able to safely access a non-existent property even in strict mode, see the proposal to add null-soaking (null-conditional) access and null-coalescing at #3240
Strictly speaking, that won't allow you to distinguish between a non-existent property and one that happens to have a $null
value, though.
@jhoneill: Nice; an idiom that is slightly more efficient (though it may not matter in practice), because it avoids creating an array of names first:
# Works even with Set-StrictMode -Version Latest
PS> [bool] $host.psobject.Properties['UI']; [bool] $host.psobject.Properties['nosuch']
True
False
It doesn't provide the desired symmetry with hashtables, though (which exists on _getting_ the property / entry) - which I personally don't consider problematic, especially if null-soaking becomes available.
As an aside, @ashmind: if ($x.Property)
would also be $false
with any _existing_ property whose value happens to be "falsy", such as 0
; if ($null -ne $x.Property)
is a better test.
$host.psobject.Properties.name -contains "UI"
Not really, @{ UI=1 }.psobject.Properties.name -contains "UI"
=> False
see the proposal to add null-soaking (null-conditional) access and null-coalescing at #3240
Yep as long as it is not just for $null
it would work.
I think it's worth keeping this issue open for now, in case they only implement $null?.
there and we'll need some other solution for non-existent property.
@mklement0 : I hope that null-soaking makes into Preview 6 (before feature freeze)
@ashmind :
> ([PSCustomObject]@{UI=1}).PSObject.Properties.Name
UI
> ([PSCustomObject]@{UI=1}).GetType().Name
PSCustomObject
> @{UI=1}.GetType().Name
Hashtable
@kborowinski Fair enough, seems like I can always do this:
function Test-Property([object] $x, [string] $name) {
return (([PSCustomObject]$x).PSObject.Properties.Name -contains $name)
}
and it works for Hashtable/PSCustomObject/Class/etc.
This is very useful to know, though I think a simpler syntax would still be great.
@ashmind : I think that simpler syntax would be better as well, or that null-soaking makes into Preview 6
$host.psobject.Properties.name -contains "UI"
Not really,
@{ UI=1 }.psobject.Properties.name -contains "UI"
=>False
Hash tables are awkward because the . notation is not accessing a member.
$h.ui is a shortcut for $h["UI"]
If you're using strict mode I'd guess you know when $x is a hashtable ? So just avoid the "." syntax. Where . is accessing a _true_ member, going via properties works.
$x.PSObject.Properties.Name -contains $name
is slow because it creates an array of all the property names, then searches that array.
Better is:
$null -ne $x.PSObject.Properties.Item($name)
Good point, @felixfbecker, which is why I suggested the following, more concise variant above:
[bool] $x.psobject.Properties[$name]
We have a case where we are checking for properties and nested properties of a [PSCustomObject]
like this:
#region Set properties
$SplashScreenParams = @{
Image = $null
Timeout = $null
MaxWidth = $null
MaxHeight = $null
Topmost = $null
}
# Boolean values like Topmost can be true/false
# that's why we compare to $null instead of if($Topmost){$action}
foreach ($PropName in @($SplashScreenParams.Keys.GetEnumerator())) {
Write-Verbose "Get '$PropName' value"
$PropValue = $null
if ($null -ne $Settings.Default.$PropName) {
$PropValue = $Settings.Default.$PropName
Write-Verbose "Default setting '$PropValue'"
}
if ($null -ne $Settings.Country.$CountryCode.$PropName) {
$PropValue = $Settings.Country.$CountryCode.$PropName
Write-Verbose "Country '$CountryCode' setting '$PropValue'"
}
$MemberOfGroupDNs | Where-Object {
$Settings.Group.PSObject.Properties.Name -contains $_
} | ForEach-Object {
if ($null -ne $Settings.Group.$_.$PropName) {
$PropValue = $Settings.Group.$_.$PropName
Write-Verbose "Group '$_' setting '$PropValue'"
}
}
if ($null -ne $PropValue) {
$SplashScreenParams.$PropName = $PropValue
Write-Verbose "Set '$PropName' to '$PropValue'"
}
}
#endregion
This code is quite readable. When applying Set-StrictMode -Version Latest
this fails gloriously. So for this clean code to be compliant a property check like:
$null -ne $Settings.Country.$CountryCode.$PropName
needs to be replaced by
([bool] $Settings.Country.PSObject.Properties[$CountryCode]) -and
([bool] $Settings.Country.$CountryCode.PSObject.Properties[$PropName])`
This seems to be very long to write.
@DarkLite1
When it is a hash table DON'T use the . notation use [$propname] instead.
SplashScreen params is a hashtable
Settings.default should be hash table, if it IS a PS Custom object it was probably made from a hash table
Settings .country should be a hashTable of countryCode=HashTable_of_settings (again if it is a custom object I'm guessing it was made from a hash table)
etc.
An observation.
Strict mode can ensure a script doesn't reference non-existent properties, which is based on the assumption that the properties of all objects are known at design time.
This does not combine well with code which uses PSObject to create objects whose properties are not fixed.
Having flexible properties requires something to check for the existence of a property purely to prevent strict mode from causing an error and skipping a line of "good" code.
A better policy would be to create a custom type which always has the required properties.
I'll also say I think using strict mode at all - is no longer the good practice it was before things like PSScriptAnalyzer
Get-ChildItem("\","\users")
skips with an error, Get-ChildItem("\users")
runs. So does new-object ValidateSet(@(1,2,3))
Interesting PSScriptAnalyzer pulls up variables which are initialized without being used but not variables which are used without being initialized, which is the level 1 check for strict mode.
We considered this problem not long after Set-StrictMode
was added. The leading option at the time looked something like:
if (unstrict { $obj.Property }) { ... }
The idea was to disable strict mode completely even though this issue was the primary use case. You can have a close approximation of this today with:
function unstrict([ScriptBlock]$scriptBlock) {
Set-StrictMode -Off
& $scriptBlock
}
Unfortunately this never seemed important enough relative to all the other improvements/fixes so it remained a rough idea.
@jhoneill Thanks again for the great advice. I'll be using the $hash['property']
way from now on. One thing of note is that $Settings
is coming from a json
like so:
{
"ExcludedGroup": [
"CN=No Splash screen,DC=contoso,DC=net"
],
"Default": {
"Image": null,
"Timeout": 20,
"Topmost": false,
"MaxWidth": 1100,
"MaxHeight": 680
},
"Country": {
"GBR": {
"Image": "\\\\contoso.net\\SplashScreen\\HS_Splash_GBR.jpg",
"Timeout": 30,
"Topmost": true,
"MaxWidth": null,
"MaxHeight": null
}
},
"Group": {
"CN=Special splash,OU=Groups,DC=contoso,DC=net": {
"Image": "\\\\contoso.net\\SplashScreen\\Special_Splash.jpg",
"Timeout": null,
"Topmost": true,
"MaxWidth": null,
"MaxHeight": null
}
}
}
Imported with
$Settings = Get-Content -LiteralPath $SettingsFilePath -EA Stop | ConvertFrom-Json
This makes it a [PSCustomObject]
by default. I could consider converting it to a HashTable
it that would make more sense.
Back on topic: It's one thing to check if a property is available on the object another thing if it is $null
. In my case it only matters if there is a value in that property, all other stuff I'm not interested in.
@DarkLite1
Been there and done that with JSON/PSCustomObjects/HashTables - this might save you doing it from scratch
(amazing what I have in my PowerShell folder)
Note that in PowerShell Core ConvertFrom-Json
now has an -AsHashTable
switch.
Yeah, strict mode is a nice idea, but without the language features to support it I can't say I've ever seen it be useful by any measure. PSSA has been _much_ more useful in my opinion. 馃槙
Most helpful comment
@DarkLite1
When it is a hash table DON'T use the . notation use [$propname] instead.
SplashScreen params is a hashtable
Settings.default should be hash table, if it IS a PS Custom object it was probably made from a hash table
Settings .country should be a hashTable of countryCode=HashTable_of_settings (again if it is a custom object I'm guessing it was made from a hash table)
etc.
An observation.
Strict mode can ensure a script doesn't reference non-existent properties, which is based on the assumption that the properties of all objects are known at design time.
This does not combine well with code which uses PSObject to create objects whose properties are not fixed.
Having flexible properties requires something to check for the existence of a property purely to prevent strict mode from causing an error and skipping a line of "good" code.
A better policy would be to create a custom type which always has the required properties.
I'll also say I think using strict mode at all - is no longer the good practice it was before things like PSScriptAnalyzer
Get-ChildItem("\","\users")
skips with an error,Get-ChildItem("\users")
runs. So doesnew-object ValidateSet(@(1,2,3))
Interesting PSScriptAnalyzer pulls up variables which are initialized without being used but not variables which are used without being initialized, which is the level 1 check for strict mode.