1

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
kincuza
  • 25
  • 3
  • 1
    You need to escape `$c`: ```"`$c.$s"``` – Mathias R. Jessen Feb 14 '21 at 14:19
  • Thank you ! for three days i got this on my brain. If i understand it correctly : by NOT escaping the first $ - iex expands the two variables separately on both sides of the . then tries to find the whole string as a key ; and by escaping it it - expands $s then the whole resulting string
    Am i right?
    – kincuza Feb 14 '21 at 14:53
  • If you _don't_ escape `$c`, the double-quoted string will cause both variables to expand _before_ `iex` can do anything. – Mathias R. Jessen Feb 14 '21 at 15:25

1 Answers1

3

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"
mklement0
  • 382,024
  • 64
  • 607
  • 775
Mathias R. Jessen
  • 157,619
  • 12
  • 148
  • 206
  • @kincuza Sorry if it wasn't clear before, I've added a full example of how to _set_ a value using the same technique now. If avoiding `iex` is not elegant, then I'm afraid I don't know what you're asking :-) – Mathias R. Jessen Feb 14 '21 at 18:16
  • Don't get me wrong - i apreciate your answer even though i don't fully understand it yet - i'm a beginner in PowerShell (and programming in general) and probably have different concepts of elegance and simplicity regarding code - i use PowerShell scripts (that only i call), mainly, to update some settings files on my personal computer - that's why i don't mind the security risks and it's more convenient (and yes -more elegant) to me to use a one-liner iex than a function for it. (the functions that i created were out of frustration of not being able to use my usual iex method) – kincuza Feb 14 '21 at 19:25