Search code examples
powershellnestedhashtable

A more compact / elegant way to navigate complex / nested hashtables with Invoke-Expression?


I'm working on a function to acess/modify nested hashtables via string input of keys hierarchy like so:
putH() $hashtable "key.key.key...etc." "new value"

Given:

$c = @{
       k1 = @{
              k1_1 = @{
                      k1_1_1 = @{ key = "QQQQQ"}
                      }
             }
      }

so far i've come up with this function for modifying values:

function putH ($h,$hKEYs,$nVAL){
    if ($hKEYs.count -eq 1) {               
        $bID = $hKEYs                             #match the last remaining obj in $hkeys
    }
    else {
        $bID = $hKEYs[0]                          #match the first obj in $hekys
    }
    foreach ($tk in $h.keys){
        if ($tk -eq $bID){
            if ($hKEYs.count -eq 1){              #reached the last obj in $hkeys so modify
                $h.$tk = $nVAL
                break
            }  
            else {                                
                $trash,$hKEYs = $hKEYs                #take out the first obj in $hkeys
                $h.$tk = putH $h.$tk $hKEYs $nVAL     #call the function again for the nested hashtale
                break
            }
        }
    } 
return $h
}

and this function for getting values :

function getH ($h,$hKEYs){
if ($hKEYs.count -eq 1) {
    $bID = $hKEYs
}
else {
    $bID = $hKEYs[0]
}
foreach ($tk in $h.keys){
    if ($tk -eq $bID){
        if ($hKEYs.count -eq 1){
            $h = $h.$tk
            break
        }
        else {
        $trash,$hKEYs = $hKEYs
        $h = getH $h.$tk $hKEYs
        break
        }
    }
}
return $h
}

that i use like so:

$s = "k1.k_1.k1_1_1"   #custom future input
$s = $s.split(".")
putH $c ($s) "NEW_QQQQQ" 
$getval = getH $c ($s)

My question:
is there a more elegant way to achieve the function's results...say with invoke-expression?
i've tried invoke-expression - but can't access the hassstables trough it (no matter the combinations, nested quotes)

$s = "k1.k_1.k1_1_1"   #custom future input
iex "$c.$s"
    

returns

System.Collections.Hashtable.k1.k_1.k1_1_1

Solution

  • Don't use Invoke-Expression

    I'll answer your question at the bottom, but I feel obliged to point out that calling Invoke-Expression here is both dangerous and, more importantly, unnecessary.

    You can resolve the whole chain of nested member references by simply splitting the "path" into its individual parts ('A.B.C' -> @('A', 'B', 'C')) and then dereferencing them one-by-one (you don't even need recursion for this!):

    function Resolve-MemberChain 
    {
      param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [psobject[]]$InputObject,
    
        [Parameter(Mandatory = $true, Position = 0)]
        [string[]]$MemberPath,
    
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$Delimiter = '.'
      )
    
      begin {
        $MemberPath = $MemberPath.Split([string[]]@($Delimiter))
      }
    
      process {
        foreach($o in $InputObject){
          foreach($m in $MemberPath){
            $o = $o.$m
          }
          $o
        }
      }
    }
    

    Now you can solve your problem without iex:

    $ht = @{
      A = @{
        B = @{
          C = "Here's the value!"
        }
      }
    }
    
    $ht |Resolve-MemberChain 'A.B.C' -Delimiter '.'
    

    You can use the same approach to update nested member values - simply stop at the last step and then assign to $parent.$lastMember:

    function Set-NestedMemberValue
    {
      param(
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [psobject[]]$InputObject,
    
        [Parameter(Mandatory = $true, Position = 0)]
        [string[]]$MemberPath,
    
        [Parameter(Mandatory = $true, position = 1)]
        $Value,
    
        [Parameter(Mandatory = $false)]
        [ValidateNotNullOrEmpty()]
        [string]$Delimiter = '.'
      )
    
      begin {
        $MemberPath = $MemberPath.Split([string[]]@($Delimiter))
        $leaf = $MemberPath |Select -Last 1
        $MemberPath = $MemberPath |select -SkipLast 1
      }
    
      process {
        foreach($o in $InputObject){
          foreach($m in $MemberPath){
            $o = $o.$m
          }
          $o.$leaf = $Value
        }
      }
    }
    

    And in action:

    PS ~> $ht.A.B.C
    Here's the value!
    PS ~> $ht |Set-NestedMemberValue 'A.B.C' 'New Value!'
    PS ~> $ht.A.B.C
    New Value!
    

    Why isn't your current approach working?

    The problem you're facing with your current implementation is that the $c in $c.$s gets expanded as soon as the string literal "$c.$s" is evaluated - to avoid that, simply escape the first $:

    iex "`$c.$s"