Set Value of Nested Object Property by Name in PowerShell - powershell

I want to set value of nested object property using PowerShell. When you are trying to set the value of the first level properties, it's quiet simple:
$propertyName = "someProperty"
$obj.$propertyName = "someValue" # ← It works
For nested properties, it doesn't work:
$propertyName = "someProperty.someNestedProperty"
$obj.$propertyName = "someValue" # ← It doesn't work and raises an error.
How to set value of nested object property by name of property using PowerShell?
MCVE
For those who want to reproduce the problem, here is a simple example:
$Obj= ConvertFrom-Json '{ "A": "x", "B": {"C": "y"} }'
# Or simply create the object:
# $Obj= #{ A = "x"; B = #{C = "y"} }
$Key = "B.C"
$Value = "Some Value"
$Obj.$Key = $Value
Run the command and you will receive an error:
"The property 'B.C' cannot be found on this object. Verify that the
property exists and can be set."
Note: The code supports any level of nesting.

I created SetValue and GetValue functions to let you get and set a nested property of an object (including a json object) dynamically by name and they work perfectly!
They are recursive functions which resolve the complex property and get the nested property step by step by splitting the nested property name.
GetValue and SetValue of Nested properties by Name
# Functions
function GetValue($object, $key)
{
$p1,$p2 = $key.Split(".")
if($p2) { return GetValue -object $object.$p1 -key $p2 }
else { return $object.$p1 }
}
function SetValue($object, $key, $Value)
{
$p1,$p2 = $key.Split(".")
if($p2) { SetValue -object $object.$p1 -key $p2 -Value $Value }
else { $object.$p1 = $Value }
}
Example
In the following example, I set B.C dynamically using SetValue and get its value by name using the GetValue function:
# Example
$Obj = ConvertFrom-Json '{ "A": "x", "B": {"C": "y"} }'
# Or simply create the object:
# $Obj = #{ A = "x"; B = #{C = "y"} }
$Key = "B.C"
$Value = "Changed Dynamically!"
SetValue -object $Obj -key $Key -Value $Value
GetValue -object $Obj -key $Key

May I propose an upgrade to Reza's solution. With this solution, you can have many level of nested properties.
function GetValue($object, [string[]]$keys)
{
$propertyName = $keys[0]
if($keys.count.Equals(1)){
return $object.$propertyName
}
else {
return GetValue -object $object.$propertyName -key ($keys | Select-Object -Skip 1)
}
}
function SetValue($object, [string[]]$keys, $value)
{
$propertyName = $keys[0]
if($keys.count.Equals(1)) {
$object.$propertyName = $value
}
else {
SetValue -object $object.$propertyName -key ($keys | Select-Object -Skip 1) -value $value
}
}
Usage
$Obj = ConvertFrom-Json '{ "A": "x", "B": {"C": {"D" : "y"}} }'
SetValue $Obj -key "B.C.D".Split(".") -value "z"
GetValue $Obj -key "B.C.D".Split(".")

Your own solutions are effective, but do not support indexed access as part of the nested property-access path (e.g., B[1].C)
A simple alternative is to use Invoke-Expression (iex).
While it should generally be avoided, there are exceptional cases where it offers the simplest solution, and this is one of them:
Assuming you fully control or implicitly trust the property-access string:
$obj = ConvertFrom-Json '{ "A": "x", "B": [ {"C": "y"}, { "C": "z"} ] }'
$propPath = 'B[1].C'
# GET
Invoke-Expression "`$obj.$propPath" # -> 'z'
# SET
$value = 'Some Value'
Invoke-Expression "`$obj.$propPath = `$value"
If you don't trust the input, you can avoid unwanted injection of commands as follows:[1]
$safePropPath = $propPath -replace '`|\$', '`$&'
Invoke-Expression "`$obj.$safePropPath"
# ...
For a convenience function / ETS method that safely packages the functionality above, see this answer.
[1] The regex-based -replace operation ensures that any $ characters in the string are escaped as `$, to prevent them Invoke-Expression from treating them as variable references or subexpressions; similarly, preexisting ` instances are escaped by doubling them.

Related

Set a PSObject path using an array for the "dot" variable names

I have a PSObject that I have filled with a json structure. I need to be able to set the value of one of the entries in the tree using an array that has the names nodes of the json path. Here is an example that gets close, but does not ultimately work (but helps explain what I am looking for):
$json = #"
{
"Logging": {
"LogLevel": {
"Default": "Warning",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
}
}
"#
$settings = $json | ConvertFrom-Json
[System.Collections.ArrayList] $jsonPath = New-Object -TypeName "System.Collections.ArrayList"
$jsonPath.Add("Logging") | Out-Null
$jsonPath.Add("LogLevel") | Out-Null
$jsonPath.Add("Microsoft") | Out-Null
Write-Output "Old Value was $($settings.$($jsonPath[0]).$($jsonPath[1]).$($jsonPath[2]))"
# I need a way to set this value when there could be an unknown number of elements in the array.
$settings.$($jsonPath[0]).$($jsonPath[1]).$($jsonPath[2]) = "Debug"
Write-Output "New Value is $($settings.$($jsonPath[0]).$($jsonPath[1]).$($jsonPath[2]))"
This works if I know that the $jsonPath array will have 3 elements. But it could have many more or less.
I thought to iterate the array like this:
$result = $settings
foreach ($pathItem in $jsonPath)
{
$result = $result.$pathItem
}
$result = "Debug"
But this just sets the string value of $result. Not the value in $settings.
I feel like I need a way to get a reference of the $setting.$pathItem value (rather than the actual value), so that I can make sure I set that value on the $settings variable.
How can I update $settings using the values in the array as the dot de-referencers?
Assuming you fully control or implicitly trust the content of array (list) $jsonPath, Invoke-Expression - which is generally to be avoided - offers a simple solution:
$jsonPath = 'Logging', 'LogLevel', 'Microsoft'
Invoke-Expression "`$settings.$($jsonPath -join '.') = 'Debug'"
Note: If there's a chance $jsonPath contains nonstandard property names (e.g. with spaces), use the following instead:
Invoke-Expression "`$settings.$($jsonPath.ForEach({ '{' + $_ + '}' }) -join '.') = 'Debug'"
Iterating through the path array is a sound option in my opinion, you only need to change your logic a bit in order to update the property:
$jsonPath = 'Logging\LogLevel\Microsoft'.Split('\')
$settings = $json | ConvertFrom-Json
$ref = $settings
foreach($token in $jsonPath) {
# if this token is not the last in the array
if($token -ne $jsonPath[-1]) {
# we can safely get its value
$ref = $ref.$token
continue
}
# else, this is the last token, we need to update the property
$ref.$token = 'newValue'
}
$settings.Logging.LogLevel.Microsoft # has newValue

Create JSON with Keys from Powershell Array

I have this Powershell Array object with string values in it
[value1,value2,value3,value4,..etc]
I would like to convert it into a JSON object with a key called value that has the values in the array and makes it look like this
[
{ "value" : "value1" },
{ "value" : "value2" },
{ "value" : "value3" },
{ "value" : "value4" },
...
]
Is that possible in powershell? Keep in mind the array could be a length of 50 so it has to loop through the array
Thanks
You can do the following in PowerShell v3+:
# Starting Array $arr that you create
$arr = 'value1','value2','value3'
# Create an array of objects with property named value and value of each array value
# Feed created objects into the JSON converter
$arr | Foreach-Object {
[pscustomobject]#{value = $_}
} | ConvertTo-Json
You can do the following in PowerShell v2:
$json = New-Object -Type 'System.Text.Stringbuilder'
$null = $json.Append("[")
$arr | foreach-Object {
$line = " {{ ""value"" : ""{0}"" }}," -f $_
$null = $json.Append("`r`n$line")
}
$null = $json.Remove($json.Length-1,1)
$null = $json.Append("`r`n]")
$json.ToString()

Set method as property of object in Powershell

I have an array of objects like this
$all = #(
#{ Name = 'First'; Method = { FirstMethod 1 }; Description = "First Description"; }
#{ Name = 'Second'; Description = "Second Description" }
#{ Name = 'Third'; Method = { ThirdMethod }; Description = "Third Description" }
)
Which every obeject has a Name (string), a Description (string), and a Method (which contains a function and its optional)
While the FirstMethod and SecondMethod looks like this:
Function FirstMethod
{
param($number)
Write-Host "$number - some other things"
return $number
}
Function ThirdMethod
{
Write-Host "Second called"
return 'test'
}
And I am iterating through all the items in $all and trying to call Method parameter if it exists:
Function RunAll
{
foreach($item in $all)
{
If($item.Method)
{
Write-Host "It has method and its running it"
$returned_from_method = $item.Method
Write-Host "Value returned from method: $returned_from_method"
}
Else
{
Write-Host "Does not have a method!"
}
}
}
So basically what I need here is that: when the loop is in the First item in array $returned_from_method = $item.Method it should return 1 (because it calls FirstMethod and passes 1. And when the loop is in the Third item in array it should return test (because it calls ThirdMethod).
Is there anyway I can achieve this?
The code you posted does neither define (custom) objects nor methods. It defines a list of hashtables where one key has a scriptblock value. Using dot-access on that key will just return the definition of the scriptblock, not invoke it.
Demonstration:
PS C:\> $ht = #{Name='foo'; Method={FirstMethod 1}; Description='bar'}
PS C:\> $ht.Method
FirstMethod 1
Even if you convert the hashtable to an object, that behavior does not change:
PS C:\> $obj = [PSCustomObject]$ht
PS C:\> $obj.Method
FirstMethod 1
To actually invoke the scriptblock you need to call the scriptblock's Invoke() method:
PS C:\> $ht.Method.Invoke()
1 - some other things
1
PS C:\> $obj.Method.Invoke()
1 - some other things
1
Whether the function called in the scriptblock is defined before or after creation of the hashtable or object doesn't matter, as long as it is defined before the scriptblock is invoked. The code in your own answer seems to work only because you replaced the scriptblock (curly brackets) with a grouping expression (parentheses). That means, however, that the "method" is evaluated upon definition of the hashtable and only the return value of the function is stored with the key. The Write-Host output is written to the console immediately and not stored with the key.
PS C:\> $ht = #{Name='foo'; Method=(FirstMethod 1); Description='bar'}
1 - some other things
PS C:\> $ht.Method
1
For creating an object with an actual (script)method you need to add a property with the correct type:
PS C:\> $obj | Add-Member -Name 'Method2' -Type ScriptMethod -Value {FirstMethod 2}
PS C:\> $obj.Method2()
2 - some other things
2
The code for creating your objects should thus look somewhat like this:
$obj1 = [PSCustomObject]#{
Name = 'First'
Description = 'First Description'
}
$obj1 | Add-Member -Name 'Method' -Type ScriptMethod -Value {FirstMethod 1}
$obj2 = [PSCustomObject]#{
Name = 'Second'
Description = 'Second Description'
}
$obj3 = [PSCustomObject]#{
Name = 'Third'
Description = 'Third Description'
}
$obj3 | Add-Member -Name 'Method' -Type ScriptMethod -Value {ThirdMethod}
$all = $obj1, $obj2, $obj3
If anyone is struggling with this one here's the answer:
The methods FirstMethod and ThirdMethod should be declared before the array
and then i needed to add the function inside (), and not {}.
Function FirstMethod
{
param($number)
Write-Host "$number - some other things"
return $number
}
Function ThirdMethod
{
Write-Host "Second called"
return 'test'
}
$all = #(
#{ Name = 'First'; Method = ( FirstMethod 1 ); Description = "First Description"; }
#{ Name = 'Second'; Description = "Second Description" }
#{ Name = 'Third'; Method = ( ThirdMethod ); Description = "Third Description" }
)
And it works fine now!

How can I pass dynamic parameters to powershell script and iterate over the list?

I want to create a powershell script that accepts dynamic parameters and I also want to iterate through them.
eg:
I call the powershell script in the following manner.
ParametersTest.ps1 -param1 value1 -param2 value2 -param3 value3
And I should be able to access my params inside the script as follows:
for($key in DynamicParams) {
$paramValue = DynamicParams[$key];
}
Is there anyway to do this in powershell? Thanks in advance.
There is nothing built-in like that (essentially you're asking for PowerShell parameter parsing in the absence of any definition of those parameters). You can emulate it, though. With $args you can get at all arguments of the function as an array. You can then iterate that and decompose it into names and values:
$DynamicParams = #{}
switch -Regex ($args) {
'^-' {
# Parameter name
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
$name = $_ -replace '^-'
}
'^[^-]' {
# Value
$value = $_
}
}
if ($name) {
$DynamicParams[$name] = $value
$name = $value = $null
}
To iterate over dynamic parameters you can either do something like you wrote
foreach ($key in $DynamicParams.Keys) {
$value = $DynamicParams[$key]
}
(note the foreach, not for, the latter of which cannot work like you wrote it) or just iterate normally over the hash table:
$DynamicParams.GetEnumerator() | ForEach-Object {
$name = $_.Key
$value = $_.Value
}

Merging hashtables in PowerShell: how?

I am trying to merge two hashtables, overwriting key-value pairs in the first if the same key exists in the second.
To do this I wrote this function which first removes all key-value pairs in the first hastable if the same key exists in the second hashtable.
When I type this into PowerShell line by line it works. But when I run the entire function, PowerShell asks me to provide (what it considers) missing parameters to foreach-object.
function mergehashtables($htold, $htnew)
{
$htold.getenumerator() | foreach-object
{
$key = $_.key
if ($htnew.containskey($key))
{
$htold.remove($key)
}
}
$htnew = $htold + $htnew
return $htnew
}
Output:
PS C:\> mergehashtables $ht $ht2
cmdlet ForEach-Object at command pipeline position 1
Supply values for the following parameters:
Process[0]:
$ht and $ht2 are hashtables containing two key-value pairs each, one of them with the key "name" in both hashtables.
What am I doing wrong?
Merge-Hashtables
Instead of removing keys you might consider to simply overwrite them:
$h1 = #{a = 9; b = 8; c = 7}
$h2 = #{b = 6; c = 5; d = 4}
$h3 = #{c = 3; d = 2; e = 1}
Function Merge-Hashtables {
$Output = #{}
ForEach ($Hashtable in ($Input + $Args)) {
If ($Hashtable -is [Hashtable]) {
ForEach ($Key in $Hashtable.Keys) {$Output.$Key = $Hashtable.$Key}
}
}
$Output
}
For this cmdlet you can use several syntaxes and you are not limited to two input tables:
Using the pipeline: $h1, $h2, $h3 | Merge-Hashtables
Using arguments: Merge-Hashtables $h1 $h2 $h3
Or a combination: $h1 | Merge-Hashtables $h2 $h3
All above examples return the same hash table:
Name Value
---- -----
e 1
d 2
b 6
c 3
a 9
If there are any duplicate keys in the supplied hash tables, the value of the last hash table is taken.
(Added 2017-07-09)
Merge-Hashtables version 2
In general, I prefer more global functions which can be customized with parameters to specific needs as in the original question: "overwriting key-value pairs in the first if the same key exists in the second". Why letting the last one overrule and not the first? Why removing anything at all? Maybe someone else want to merge or join the values or get the largest value or just the average...
The version below does no longer support supplying hash tables as arguments (you can only pipe hash tables to the function) but has a parameter that lets you decide how to treat the value array in duplicate entries by operating the value array assigned to the hash key presented in the current object ($_).
Function
Function Merge-Hashtables([ScriptBlock]$Operator) {
$Output = #{}
ForEach ($Hashtable in $Input) {
If ($Hashtable -is [Hashtable]) {
ForEach ($Key in $Hashtable.Keys) {$Output.$Key = If ($Output.ContainsKey($Key)) {#($Output.$Key) + $Hashtable.$Key} Else {$Hashtable.$Key}}
}
}
If ($Operator) {ForEach ($Key in #($Output.Keys)) {$_ = #($Output.$Key); $Output.$Key = Invoke-Command $Operator}}
$Output
}
Syntax
HashTable[] <Hashtables> | Merge-Hashtables [-Operator <ScriptBlock>]
Default
By default, all values from duplicated hash table entries will added to an array:
PS C:\> $h1, $h2, $h3 | Merge-Hashtables
Name Value
---- -----
e 1
d {4, 2}
b {8, 6}
c {7, 5, 3}
a 9
Examples
To get the same result as version 1 (using the last values) use the command: $h1, $h2, $h3 | Merge-Hashtables {$_[-1]}. If you would like to use the first values instead, the command is: $h1, $h2, $h3 | Merge-Hashtables {$_[0]} or the largest values: $h1, $h2, $h3 | Merge-Hashtables {($_ | Measure-Object -Maximum).Maximum}.
More examples:
PS C:\> $h1, $h2, $h3 | Merge-Hashtables {($_ | Measure-Object -Average).Average} # Take the average values"
Name Value
---- -----
e 1
d 3
b 7
c 5
a 9
PS C:\> $h1, $h2, $h3 | Merge-Hashtables {$_ -Join ""} # Join the values together
Name Value
---- -----
e 1
d 42
b 86
c 753
a 9
PS C:\> $h1, $h2, $h3 | Merge-Hashtables {$_ | Sort-Object} # Sort the values list
Name Value
---- -----
e 1
d {2, 4}
b {6, 8}
c {3, 5, 7}
a 9
I see two problems:
The open brace should be on the same line as Foreach-object
You shouldn't modify a collection while enumerating through a collection
The example below illustrates how to fix both issues:
function mergehashtables($htold, $htnew)
{
$keys = $htold.getenumerator() | foreach-object {$_.key}
$keys | foreach-object {
$key = $_
if ($htnew.containskey($key))
{
$htold.remove($key)
}
}
$htnew = $htold + $htnew
return $htnew
}
Not a new answer, this is functionally the same as #Josh-Petitt with improvements.
In this answer:
Merge-HashTable uses the correct PowerShell syntax if you want to drop this into a module
Wasn't idempotent. I added cloning of the HashTable input, otherwise your input was clobbered, not an intention
added a proper example of usage
function Merge-HashTable {
param(
[hashtable] $default, # Your original set
[hashtable] $uppend # The set you want to update/append to the original set
)
# Clone for idempotence
$default1 = $default.Clone();
# We need to remove any key-value pairs in $default1 that we will
# be replacing with key-value pairs from $uppend
foreach ($key in $uppend.Keys) {
if ($default1.ContainsKey($key)) {
$default1.Remove($key);
}
}
# Union both sets
return $default1 + $uppend;
}
# Real-life example of dealing with IIS AppPool parameters
$defaults = #{
enable32BitAppOnWin64 = $false;
runtime = "v4.0";
pipeline = 1;
idleTimeout = "1.00:00:00";
} ;
$options1 = #{ pipeline = 0; };
$options2 = #{ enable32BitAppOnWin64 = $true; pipeline = 0; };
$results1 = Merge-HashTable -default $defaults -uppend $options1;
# Name Value
# ---- -----
# enable32BitAppOnWin64 False
# runtime v4.0
# idleTimeout 1.00:00:00
# pipeline 0
$results2 = Merge-HashTable -default $defaults -uppend $options2;
# Name Value
# ---- -----
# idleTimeout 1.00:00:00
# runtime v4.0
# enable32BitAppOnWin64 True
# pipeline 0
In case you want to merge the whole hashtable tree
function Join-HashTableTree {
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[hashtable]
$SourceHashtable,
[Parameter(Mandatory = $true, Position = 0)]
[hashtable]
$JoinedHashtable
)
$output = $SourceHashtable.Clone()
foreach ($key in $JoinedHashtable.Keys) {
$oldValue = $output[$key]
$newValue = $JoinedHashtable[$key]
$output[$key] =
if ($oldValue -is [hashtable] -and $newValue -is [hashtable]) { $oldValue | ~+ $newValue }
elseif ($oldValue -is [array] -and $newValue -is [array]) { $oldValue + $newValue }
else { $newValue }
}
$output;
}
Then, it can be used like this:
Set-Alias -Name '~+' -Value Join-HashTableTree -Option AllScope
#{
a = 1;
b = #{
ba = 2;
bb = 3
};
c = #{
val = 'value1';
arr = #(
'Foo'
)
}
} |
~+ #{
b = #{
bb = 33;
bc = 'hello'
};
c = #{
arr = #(
'Bar'
)
};
d = #(
42
)
} |
ConvertTo-Json
It will produce the following output:
{
"a": 1,
"d": 42,
"c": {
"val": "value1",
"arr": [
"Foo",
"Bar"
]
},
"b": {
"bb": 33,
"ba": 2,
"bc": "hello"
}
}
I just needed to do this and found this works:
$HT += $HT2
The contents of $HT2 get added to the contents of $HT.
The open brace has to be on the same line as ForEach-Object or you have to use the line continuation character (backtick).
This is the case because the code within { ... } is really the value for the -Process parameter of ForEach-Object cmdlet.
-Process <ScriptBlock[]>
Specifies the script block that is applied to each incoming object.
This will get you past the current issue at hand.
I think the most compact code to merge (without overwriting existing keys) would be this:
function Merge-Hashtables($htold, $htnew)
{
$htnew.keys | where {$_ -notin $htold.keys} | foreach {$htold[$_] = $htnew[$_]}
}
I borrowed it from Union and Intersection of Hashtables in PowerShell
I wanted to point out that one should not reference base properties of the hashtable indiscriminately in generic functions, as they may have been overridden (or overloaded) by items of the hashtable.
For instance, the hashtable $hash=#{'keys'='lots of them'} will have the base hashtable property, Keys overridden by the item keys, and thus doing a foreach ($key in $hash.Keys) will instead enumerate the hashed item keys's value, instead of the base property Keys.
Instead the method GetEnumerator or the keys property of the PSBase property, which cannot be overridden, should be used in functions that may have no idea if the base properties have been overridden.
Thus, Jon Z's answer is the best.
To 'inherit' key-values from parent hashtable ($htOld) to child hashtables($htNew), without modifying values of already existing keys in the child hashtables,
function MergeHashtable($htOld, $htNew)
{
$htOld.Keys | %{
if (!$htNew.ContainsKey($_)) {
$htNew[$_] = $htOld[$_];
}
}
return $htNew;
}
Please note that this will modify the $htNew object.
Here is a function version that doesn't use the pipeline (not that the pipeline is bad, just another way to do it). It also returns a merged hashtable and leaves the original unchanged.
function MergeHashtable($a, $b)
{
foreach ($k in $b.keys)
{
if ($a.containskey($k))
{
$a.remove($k)
}
}
return $a + $b
}
I just wanted to expand or simplify on jon Z's answer. There just seems to be too many lines and missed opportunities to use Where-Object. Here is my simplified version:
Function merge_hashtables($htold, $htnew) {
$htold.Keys | ? { $htnew.ContainsKey($_) } | % {
$htold.Remove($_)
}
$htold += $htnew
return $htold
}