arrayspowershellsortinghashtablepscustomobject

How can I remove $null and whitespace key/value pairs in a [System.Collections.Specialized.OrderedDictionary] using PowerShell 7?


There are plenty of examples of removing null and empty/whitespace keys from arrays, hashtables, and PSCustomObjects. I've adapted and merged several functions I found on this site to come up with this:

Function Remove-NullAndEmptyProperties {
    [CmdletBinding()]
    Param(
        # Object from which to remove the null values.
        [Parameter(ValueFromPipeline,Mandatory,Position=0)]
        $InputObject,
        # Instead of also removing values that are empty strings, include them
        # in the output.
        [Switch]$LeaveEmptyStrings,
        # Additional entries to remove, which are either present in the
        # properties list as an object or as a string representation of the
        # object.
        # I.e. $item.ToString().
        [Object[]]$AlsoRemove = @()
    )

    begin {
        $IsNonRefType = {
            param (
                [AllowNull()]
                $Val
            )
            return (($null -ne $Val) -and (-not($Val.GetType().IsValueType)))
        }
    }

    Process {

        try  {

            $IsValidType = (& $IsNonRefType -Val $InputObject)
            if(-not$IsValidType){
                Write-Error "Input object is an invalid type."
                return
            }

            if($InputObject.GetType().name -eq 'Hashtable'){
                $TmpString = $InputObject | ConvertTo-Json -Depth 15
                $TmpString = $TmpString -replace '"\w+?"\s*:\s*null,?'
                if(!$LeaveEmptyStrings){
                    $TmpString = $TmpString -replace '"\w+?"\s*:\s*"\s+",?'
                }
                $NewHash = $TmpString | ConvertFrom-Json
                $NewHash
            } else {
                # Iterate InputObject in case input was passed as an array
                ForEach ($obj in $InputObject) {
                    $obj | Select-Object -Property (
                        $obj.PSObject.Properties.Name | Where-Object {
                            -not (
                                # If prop is null, remove it
                                $null -eq $obj.$_ -or
                                # If -LeaveEmptyStrings is not specified and the property
                                # is an empty string, remove it
                                (-not $LeaveEmptyStrings.IsPresent -and
                                    [string]::IsNullOrWhiteSpace($obj.$_)) -or
                                # If AlsoRemove contains the property, remove it
                                $AlsoRemove.Contains($obj.$_) -or
                                # If AlsoRemove contains the string representation of
                                # the property, remove it
                                $AlsoRemove.Contains($obj.$_.ToString())
                            )
                        }
                    )
                }
            }
        }
        catch {
            Write-Output "A terminating error occured: $($PSItem.ToString())"
            $PSCmdlet.ThrowTerminatingError($PSItem)
        }
    }
}

These examples work fine:

$Hash = @{
    "Key1" = 'Keyboard'
    "Key2" = '  '
    "Key3" = 'Mouse'
    "Key4" = $null
    "Key5" = 'Computer'
}

$Hash | Remove-NullAndEmptyProperties

enter image description here

$PSC = [PSCustomObject]@{
    KeyA = "   "
    KeyB = "Windows"
    KeyC = "Applications"
    KeyD = $null
    KeyE = ""
    KeyF = "Documents"
}

$PSC | Remove-NullAndEmptyProperties

enter image description here

But if I try this:

$OHash = [ordered]@{
    "Key1" = 'Alpha'
    "Key2" = '  '
    "Key3" = 'Beta'
    "Key4" = $null
    "Key5" = 'Omega'
}

$OHash | Remove-NullAndEmptyProperties

I'm greeted with this:

enter image description here

How can I iterate over an Ordered Dictionary and remove the $null and whitespace keys/values?

I'd like this function to be as robust as possible.

Any solutions are greatly welcomed!


Solution

  • The way I would handle null or whitespace values in an OrderedDictionary is by enumerating the key / value pairs and using [string]::IsNullOrWhiteSpace to skip them:

    $OHash = [ordered]@{
        'Key1' = 'Alpha'
        'Key2' = ''
        'Key3' = 'Beta'
        'Key4' = $null
        'Key5' = 'Omega'
    }
    
    # toggle for testing
    [Switch] $LeaveEmptyStrings = $true
    
    # can use `-is [System.Collections.IDictionary]` here
    # to target any type implementing the interface (i.e.: hashtable)
    if ($OHash -is [System.Collections.Specialized.OrderedDictionary]) {
        $newDict = [ordered]@{}
        foreach ($pair in $OHash.GetEnumerator()) {
            if (-not $LeaveEmptyStrings.IsPresent -and [string]::IsNullOrWhiteSpace($pair.Value)) {
                continue
            }
            elseif ($null -eq $pair.Value) {
                continue
            }
    
            $newDict[$pair.Key] = $pair.Value
        }
        $newDict
    }
    

    The logic above attempts to create a copy of the input dictionary, it does not attempt to mutate it, however if the keys could have reference values that need to be dereferenced then you will need to serialize it before making the copy. ConvertTo-Json and ConvertFrom-Json is a good option.

    As aside, this logic can also be re-used in the PSCustomObject code path with very small variations in the code, you can use an OrderedDictionary to construct the new object then cast the [pscustomobject] accelerator:

    $PSC = [PSCustomObject]@{
        KeyA = '   '
        KeyB = 'Windows'
        KeyC = 'Applications'
        KeyD = $null
        KeyE = ''
        KeyF = 'Documents'
    }
    
    [switch] $LeaveEmptyStrings = $true
    
    if ($PSC -is [System.Management.Automation.PSCustomObject]) {
        $newObject = [ordered]@{}
        foreach ($property in $PSC.PSObject.Properties) {
            if (-not $LeaveEmptyStrings.IsPresent -and [string]::IsNullOrWhiteSpace($property.Value)) {
                continue
            }
            elseif ($null -eq $property.Value) {
                continue
            }
    
            $newObject[$property.Name] = $property.Value
        }
        [pscustomobject] $newObject
    }