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
Say I have JSON like:
{
"a" : {
"b" : 1,
"c" : 2
}
}
Now ConvertTo-Json will happily create PSObjects out of that. I want to access an item I could do $json.a.b and get 1 - nicely nested properties.
Now if I have the string "a.b" the question is how to use that string to access the same item in that structure? Seems like there should be some special syntax I'm missing like & for dynamic function calls because otherwise you have to interpret the string yourself using Get-Member repeatedly I expect.
No, there is no special syntax, but there is a simple workaround, using iex, the built-in alias[1] for the Invoke-Expression cmdlet:
$propertyPath = 'a.b'
# Note the ` (backtick) before $json, to prevent premature expansion.
iex "`$json.$propertyPath" # Same as: $json.a.b
# You can use the same approach for *setting* a property value:
$newValue = 'foo'
iex "`$json.$propertyPath = `$newValue" # Same as: $json.a.b = $newValue
Caveat: Do this only if you fully control or implicitly trust the value of $propertyPath.
Only in rare situation is Invoke-Expression truly needed, and it should generally be avoided, because it can be a security risk.
Note that if the target property contains an instance of a specific collection type and you want to preserve it as-is (which is not common) (e.g., if the property value is a strongly typed array such as [int[]], or an instance of a list type such as [System.Collections.Generic.List`1]), use the following:
# "," constructs an aux., transient array that is enumerated by
# Invoke-Expression and therefore returns the original property value as-is.
iex ", `$json.$propertyPath"
Without the , technique, Invoke-Expression enumerates the elements of a collection-valued property and you'll end up with a regular PowerShell array, which is of type [object[]] - typically, however, this distinction won't matter.
Note: If you were to send the result of the , technique directly through the pipeline, a collection-valued property value would be sent as a single object instead of getting enumerated, as usual. (By contrast, if you save the result in a variable first and the send it through the pipeline, the usual enumeration occurs). While you can force enumeration simply by enclosing the Invoke-Expression call in (...), there is no reason to use the , technique to begin with in this case, given that enumeration invariably entails loss of the information about the type of the collection whose elements are being enumerated.
Read on for packaged solutions.
Note:
The following packaged solutions originally used Invoke-Expression combined with sanitizing the specified property paths in order to prevent inadvertent/malicious injection of commands. However, the solutions now use a different approach, namely splitting the property path into individual property names and iteratively drilling down into the object, as shown in Gyula Kokas's helpful answer. This not only obviates the need for sanitizing, but turns out to be faster than use of Invoke-Expression (the latter is still worth considering for one-off use).
The no-frills, get-only, always-enumerate version of this technique would be the following function:
# Sample call: propByPath $json 'a.b'
function propByPath { param($obj, $propPath) foreach ($prop in $propPath.Split('.')) { $obj = $obj.$prop }; $obj }
What the more elaborate solutions below offer: parameter validation, the ability to also set a property value by path, and - in the case of the propByPath function - the option to prevent enumeration of property values that are collections (see next point).
The propByPath function offers a -NoEnumerate switch to optionally request preserving a property value's specific collection type.
By contrast, this feature is omitted from the .PropByPath() method, because there is no syntactically convenient way to request it (methods only support positional arguments). A possible solution is to create a second method, say .PropByPathNoEnumerate(), that applies the , technique discussed above.
Helper function propByPath:
function propByPath {
param(
[Parameter(Mandatory)] $Object,
[Parameter(Mandatory)] [string] $PropertyPath,
$Value, # optional value to SET
[switch] $NoEnumerate # only applies to GET
)
Set-StrictMode -Version 1
# Note: Iteratively drilling down into the object turns out to be *faster*
# than using Invoke-Expression; it also obviates the need to sanitize
# the property-path string.
$props = $PropertyPath.Split('.') # Split the path into an array of property names.
if ($PSBoundParameters.ContainsKey('Value')) { # SET
$parentObject = $Object
if ($props.Count -gt 1) {
foreach ($prop in $props[0..($props.Count-2)]) { $parentObject = $parentObject.$prop }
}
$parentObject.($props[-1]) = $Value
}
else { # GET
$value = $Object
foreach ($prop in $props) { $value = $value.$prop }
if ($NoEnumerate) {
, $value
} else {
$value
}
}
}
Instead of the Invoke-Expression call you would then use:
# GET
propByPath $obj $propertyPath
# GET, with preservation of the property value's specific collection type.
propByPath $obj $propertyPath -NoEnumerate
# SET
propByPath $obj $propertyPath 'new value'
You could even use PowerShell's ETS (extended type system) to attach a .PropByPath() method to all [pscustomobject] instances (PSv3+ syntax; in PSv2 you'd have to create a *.types.ps1xml file and load it with Update-TypeData -PrependPath):
'System.Management.Automation.PSCustomObject',
'Deserialized.System.Management.Automation.PSCustomObject' |
Update-TypeData -TypeName { $_ } `
-MemberType ScriptMethod -MemberName PropByPath -Value { #`
param(
[Parameter(Mandatory)] [string] $PropertyPath,
$Value
)
Set-StrictMode -Version 1
$props = $PropertyPath.Split('.') # Split the path into an array of property names.
if ($PSBoundParameters.ContainsKey('Value')) { # SET
$parentObject = $this
if ($props.Count -gt 1) {
foreach ($prop in $props[0..($props.Count-2)]) { $parentObject = $parentObject.$prop }
}
$parentObject.($props[-1]) = $Value
}
else { # GET
# Note: Iteratively drilling down into the object turns out to be *faster*
# than using Invoke-Expression; it also obviates the need to sanitize
# the property-path string.
$value = $this
foreach ($prop in $PropertyPath.Split('.')) { $value = $value.$prop }
$value
}
}
You could then call $obj.PropByPath('a.b') or $obj.PropByPath('a.b', 'new value')
Note: Type Deserialized.System.Management.Automation.PSCustomObject is targeted in addition to System.Management.Automation.PSCustomObject in order to also cover deserialized custom objects, which are returned in a number of scenarios, such as using Import-CliXml, receiving output from background jobs, and using remoting.
.PropByPath() will be available on any [pscustomobject] instance in the remainder of the session (even on instances created prior to the Update-TypeData call [2]); place the Update-TypeData call in your $PROFILE (profile file) to make the method available by default.
[1] Note: While it is generally advisable to limit aliases to interactive use and use full cmdlet names in scripts, use of iex to me is acceptable, because it is a built-in alias and enables a concise solution.
[2] Verify with (all on one line) $co = New-Object PSCustomObject; Update-TypeData -TypeName System.Management.Automation.PSCustomObject -MemberType ScriptMethod -MemberName GetFoo -Value { 'foo' }; $co.GetFoo(), which outputs foo even though $co was created before Update-TypeData was called.
This workaround is maybe useful to somebody.
The result goes always deeper, until it hits the right object.
$json=(Get-Content ./json.json | ConvertFrom-Json)
$result=$json
$search="a.c"
$search.split(".")|% {$result=$result.($_) }
$result
You can have 2 variables.
$json = '{
"a" : {
"b" : 1,
"c" : 2
}
}' | convertfrom-json
$a,$b = 'a','b'
$json.$a.$b
1
I'm calling the cmdlet Get-OctopusEnvironment:
https://octoposh.readthedocs.io/en/latest/cmdlets/get-octopusenvironment/#parameters from OctoPosh. The call works if I enter a literal like ENV1. (This is a sample value, obviously you need to enter a value that exists as a environment on the Octopus server)
However, I'm using a script with params and the $env and $num need to be combined to identify the environment that I wish to target.
I tried:
Calling it in another session
Setting the $env and $num separately and then calling the method
Calling the function with the literal value - which always works.
Param(
[string]$env,
[string]$num
)
Import-Module Octoposh
Set-OctopusConnectionInfo (Call details hidden)
$result = Get-OctopusEnvironment -EnvironmentName $env+$num
$result
Output:
<pre>
Name : ENV1
Id : Environments-1
Machines : {some server}
Deployments : {some octopus projects}
Resource : Octopus.Client.Model.EnvironmentResource
</pre>
UPDATE: this seems to work fine for some reason. And it solves my problem... GetEnvironment basically just does the same as I wanted to. It combines the params $env+$num and returns them.
if($env -eq "DEV" -or $env -eq "INT")
{
Set-OctopusConnectionInfo
$envName = .\GetEnvironment.ps1 $env $num
$result = Get-OctopusEnvironment -EnvironmentName $envName
}
I declare all variable in var.ps1 like this:
$a = "aa"
$b = "bb"
In the second script check.ps1 I try to create a function to check if the argument passed exist in var.ps1 something like this:
check "$a" "$b" "$c"
I need to use args in my function.
Could you please give me any suggestion?
It's unclear why you want this and I'd probably solve it another way, but to answer your question, you can try the sample below.
Remember to use literal strings (single quotes) for values with a leading $ to avoid them being treated as a variable. If not, PowerShell will try to replace the variables with it's value (or nothing if it's not defined), which means that Check-Variables won't get the variable names. The following solution accepts both 'a' and '$a'.
Var.ps1
$a = "aa"
$b = "bb"
Check.ps1
#Dot-sourcing var.ps1 which is located in the same folder as Check.ps1
#Loads variables into the current scope so every function in Check.ps1 can access them
. "$PSScriptRoot\var.ps1"
function Check-Variables {
foreach ($name in $args) {
#Remove leading $
$trimmedname = $name -replace '^\$'
if(-not (Get-Variable -Name $trimmedname -ErrorAction SilentlyContinue)) {
Write-Host "ERROR: Variable `$$trimmedname is not defined"
}
}
}
Check-Variables '$a' "b" '$c'
Demo:
PS> C:\Users\frode\Desktop\Check.ps1
ERROR: Variable $c is not defined
I have the following script
function dummy
{
param([string[]] myArray)
myArray | foreach {
#do something with $_
}
}
from powershell if I do the following everything is fine
. ./myscript.ps1
dummy 'val1','val2'
but i can't get this to work from jenkins with a global var I have defined
. ./myscript.ps1
dummy $env:myglobal
where $env:myglobal = 'val1','val2'
it appears to be passing the following
dummy "'val1','val2'"
and the dummy treats it as a single string instead of a string array
I'm probably oversimplifying, but basically environment variables are just strings (i.e. not arrays!).
What you'll need to do is parse the value using -split e.g.
function dummy {
Param (
[string[]]
$myArray
)
Process {
[array]$anotherArray = $myArray -split ","
$anotherArray| ForEach-Object {
#do something with $_
}
}
}