Powershell: Check if a property exists on an object in a strict mode

Created on 23 Oct 2019  路  16Comments  路  Source: PowerShell/PowerShell

Summary of the new feature/enhancement

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.

Problem

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.

Request

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.

Issue-Enhancement

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

  • It skips lines and throws non terminating errors meaning code will continue having not executed some lines which would have worked.
  • Authors only learn of their mistakes via run-time errors.
  • Coverage is patchy 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.

All 16 comments

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

  • It skips lines and throws non terminating errors meaning code will continue having not executed some lines which would have worked.
  • Authors only learn of their mistakes via run-time errors.
  • Coverage is patchy 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

Convert-PSCustomObject.zip

(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. 馃槙

Was this page helpful?
0 / 5 - 0 ratings