Powershell pass parameter to function to query subproperty of object [duplicate] - powershell

This question already has answers here:
Set Value of Nested Object Property by Name in PowerShell
(3 answers)
Access PSObject property indirectly with variable
(3 answers)
Closed 1 year ago.
What im trying to do is getting one specific value from nested JSON. Using array keys as expression.
Array with keys and values:
$AccountService = #{
'root.branch.setting1'= 'Val1'
'root.branch.setting2'= 'Val2'
'root.branch.setting3'= 'Val3'
}
Create JSON Object
$json = Get-Content 'C:\Users\ramosfer\Documents\test.json' | ConvertFrom-Json
Get every key from array using a loop to get the value from the JSON. Expecting something like this in the Expression ($json.root.branch.setting1)
$AccountService.GetEnumerator() | % {
$json | Select-Object #{Name="Val"; Expression={$json.$_}}
}
Use this $json.$_ and expect something like this
Val
---
Val1
Val2
Val3

The best way of resolving nested properties is to resolve them one at a time :)
A simpler example, for retrieving just one value:
$json = '{"root":{"branch":{"setting1":"Value 1","setting2":"Value 2","setting3":"Value 3"}}}' |ConvertFrom-Json
# define the "path" to the value we want
$path = 'root.branch.setting1'
# split path into individual property names
$path = $path.Split('.')
# start from the root object and then start "walking" down the object hierarchy
$cursor = $json
foreach($property in $path)
{
# advance cursor one level deeper
$cursor = $cursor.$property
}
# this will now contain "Value 1"
$cursor
You can turn this into a neat little function:
function Resolve-MemberChain
{
param(
[Parameter(Mandatory = $true, ValueFromPipeline = $true)]
[psobject[]]$InputObject,
[Parameter(Mandatory = $true, Position = 0)]
[string[]]$MemberPath,
[Parameter(Mandatory = $false)]
[string]$Delimiter
)
begin {
if($PSBoundParameters.ContainsKey('Delimiter')){
$MemberPath = $MemberPath.Split([string[]]#($Delimiter))
}
}
process {
foreach($obj in $InputObject){
$cursor = $obj
foreach($member in $MemberPath){
$cursor = $cursor.$member
}
$cursor
}
}
}
Then use it like this:
$json |Resolve-MemberChain 'root.branch.setting1' -Delimiter '.'
Or, as in your case, within a calculated property expression, like so:
$AccountService.GetEnumerator()|%{
$path = $_.Key
$json |Select #{Name='Val';Expression={$_ |Resolve-MemberChain $path -Delimiter .}}
}

Related

PowerShell: How can I pass a nested hash table to my function that accepts 3 parameters?

I created this function but I don't know how to make it work. here is the code:
function ProgramRegistry {
param (
[Parameter(Mandatory=$false)][HashTable]$HashTable,
[Parameter(Mandatory=$false)][String]$AlertPath,
[Parameter(Mandatory=$false)][String]$AlertName,
[Parameter(Mandatory=$false)][String]$AlertValue
)
foreach ($AlertPath in $HashTable.Values){
foreach($AlertName in $HashTable.Values){
foreach($AlertValue in $HashTable.Values){
New-Item -Path $AlertPath -Force | Out-Null
New-ItemProperty -Path $AlertPath -Name $AlertName -Value $AlertValue -PropertyType DWORD -Force
}
}
}
}
$keys = [ordered]#{
key1 = #{
AlertPath = 'Path'
AlertName = 'Name'
AlertValue = 'Value'
}
key2 = #{
AlertPath = 'Path'
AlertName = 'Name'
AlertValue = 'Value'
}
# and so on...
}
ModifyRegistry #keys
ModifyRegistry -AlertPath "path" -AlertName "name" -AlertValue "value"
I want to be able to call the function in 2 different ways (as shown in the script)
either by defining its 3 parameters explicitly in one line.
or by passing a nested hash table consisting of multiple objects each having the function's 3 parameters.
how can I achieve that?
I want to only modify the function and not the way I call it. I need to call it a bunch of times and want to keep the code for doing it as minimal as possible, like this ModifyRegistry #keys . it's okay if the function itself is complicated and long but I want calls to function to take very little code like that. instead of nested hash table, I could just call the function repeatedly but it'd be too much repeated code and that's what I want to avoid.
You may modify your function to accept a hashtable of hashtables. You just need to provide some logic to check if the hashtable received is a hashtable containing other hashtables that have the values you need or if it is instead a single hashtable containing the values you need. Also needed is to handle the other parameters still when not providing a hashtable. The example below shows how I would do this in an advanced function utilizing a begin, process, and end block. In the begin block we only need to create the collection object that we will use to sift out the inputs. The process block is repeated for each input object received when using the pipeline. If supplying the arguments to the function directly this process block will only run once. We will use this process block to determine and add our input objects to the $inputs arraylist we created. In the end block we will perform the actual processing on each of the objects that we've collected.
function ProgramRegistry {
[cmdletbinding()]
param (
# ValueFromPipeline attribute will allow piping hashtables to the function
[Parameter(Mandatory = $false, ValueFromPipeline)][HashTable]$HashTable,
[Parameter(Mandatory = $false)][String]$AlertPath,
[Parameter(Mandatory = $false)][String]$AlertName,
[Parameter(Mandatory = $false)][String]$AlertValue
)
begin {
# Create an arraylist to collect hashtables for later processing in end block
$inputs = [System.Collections.ArrayList]::new()
}
process {
if ($HashTable) {
if ($HashTable.ContainsKey('AlertPath')) {
# if single hashtable is received with 'AlertPath' key add to inputs for processing in end block
$inputs.Add($HashTable) | Out-Null
}
else {
foreach ($value in $HashTable.Values) {
# check if value of key is a hashtable
if ($value -is [hashtable]) {
# check if hashtable contains key "AlertPath" and if so add to $inputs for processing in end block
if ($value.ContainsKey('AlertPath')) {
$inputs.Add($value) | Out-Null
}
else {
Write-Warning "Invalid hashtable format - missing 'AlertPath' key"
}
} else {
Write-Warning "Object is not a hashtable"
}
}
}
}
else {
# process when not a hashtable by creating a hashtable and adding to $inputs
$inputs.Add(#{
AlertPath = $AlertPath
AlertName = $AlertName
AlertValue = $AlertValue
}) | Out-Null
}
}
end {
# Process hashtables collected in $inputs
foreach ($hash in $inputs) {
# your code here
[pscustomobject]#{
Path = $hash.AlertPath
Name = $hash.AlertName
Value = $hash.AlertValue
}
}
}
}
$keys = [ordered]#{
key1 = #{
AlertPath = 'Path1'
AlertName = 'Name1'
AlertValue = 'Value1'
}
key2 = #{
AlertPath = 'Path2'
AlertName = 'Name2'
AlertValue = 'Value2'
}
# and so on...
}
ProgramRegistry -HashTable $keys
# or
$keys | ProgramRegistry
# or even
ProgramRegistry -HashTable $keys.key1 #or $keys.key1 | ProgramRegistry
If pipeline and advanced function is not wanted you can still do something similar without begin, process, and end blocks. I use nested function 'processit' so that I don't have to repeat the processing logic multiple times
function ProgramRegistry {
param (
[Parameter(Mandatory = $false)][HashTable]$HashTable,
[Parameter(Mandatory = $false)][String]$AlertPath,
[Parameter(Mandatory = $false)][String]$AlertName,
[Parameter(Mandatory = $false)][String]$AlertValue
)
# create nested function to process each hash table.
function processit {
param([hashtable]$hash)
# add processing logic here
[pscustomobject]#{
Path = $hash.AlertPath
Name = $hash.AlertName
Value = $hash.AlertValue
}
}
if ($HashTable) {
if ($HashTable.ContainsKey('AlertPath')) {
# if single hashtable is received with 'AlertPath' key process it
processit -hash $HashTable
}
else {
foreach ($value in $HashTable.Values) {
# check if value of key is a hashtable
if ($value -is [hashtable]) {
# check if hashtable contains key "AlertPath" and if so process it
if ($value.ContainsKey('AlertPath')) {
processit -hash $value
}
else {
Write-Warning "Invalid hashtable format - missing 'AlertPath' key"
}
}
else {
Write-Warning 'Object is not a hashtable'
}
}
}
}
else {
processit #{AlertPath = $AlertPath; AlertName = $AlertName; AlertValue = $AlertValue }
}
}
Update in response to your question regarding using the key name as AlertName
function ProgramRegistry {
param (
[Parameter(Mandatory = $false)][HashTable]$HashTable,
[Parameter(Mandatory = $false)][String]$AlertPath,
[Parameter(Mandatory = $false)][String]$AlertName,
[Parameter(Mandatory = $false)][String]$AlertValue
)
# create nested function to process each hash table.
function processit {
param([hashtable]$hash)
# add processing logic here
[pscustomobject]#{
Path = $hash.AlertPath
Name = $hash.AlertName
Value = $hash.AlertValue
}
}
if ($HashTable) {
if ($HashTable.ContainsKey('AlertPath')) {
# if single hashtable is received with 'AlertPath' key process it
processit -hash $HashTable
}
else {
foreach ($item in $HashTable.GetEnumerator()) {
if ($item.Value -is [hashtable]) {
# check if the hashtable has AlertPath and AlertValue keys
if ($item.Value.ContainsKey('AlertPath') -and $item.Value.ContainsKey('AlertValue')) {
$hash = $item.Value
# check if hashtable contains an AlertName key.
if (-not $hash.ContainsKey('AlertName')){
# If not use parent key name
$hash.AlertName = $item.Key
}
processit -hash $hash
}
else {
Write-Warning "Invalid hashtable format - missing AlertPath and/or AlertValue key"
}
}
else {
Write-Warning "Item does not contain a hashtable"
}
}
}
}
else {
processit #{AlertPath = $AlertPath; AlertName = $AlertName; AlertValue = $AlertValue }
}
}
Calling the function
$items = #{
Alert1 = #{
AlertPath = 'Path1'
AlertValue = 'Value1'
}
Alert2 = #{
AlertPath = 'Path2'
AlertValue = 'Value2'
}
Alert3 = #{
AlertName = 'Overridden AlertName'
AlertPath = 'Path3'
AlertValue = 'Value3'
}
Alert4 = #{
AlertValue = 'Value2'
}
Alert5 = "just a string"
}
ProgramRegistry -HashTable $items
Output
WARNING: Item does not contain a hashtable
WARNING: Invalid hashtable format - missing AlertPath and/or AlertValue key
Path Name Value
---- ---- -----
Path3 Overridden AlertName Value3
Path2 Alert2 Value2
Path1 Alert1 Value1

How to return [string[]] from a powershell function? [duplicate]

This question already has answers here:
Create a Single-Element Json Array Object Using PowerShell
(2 answers)
Controlling the type returned by a Function in PowerShell
(1 answer)
Closed 1 year ago.
I'm trying to write a fuction that returns [string[]] from a powershell function. But it always returns a generic object intead...
function MyGetFiles {
[OutputType('string[]')]
[CmdletBinding()]
param([string]$filter)
$found = New-Object -TypeName System.Collections.Generic.List[System.String]
$cwd = (Get-Location).Path
[string[]]$fileEntries = [IO.Directory]::GetFiles($cwd);
foreach ($file in $fileEntries) {
if ($file -match $filter) {
#write-host "FILE:" $file
$found.Add($file)
}
}
$arr= [string[]]$found.ToArray()
$arr.GetType() | out-host
return $arr
}
$wlf = MyGetFiles "\.wlf`$"
$wlf.GetType() | out-host
$wlf.Count
Script Output:
True True String[] System.Array
True True String System.Object
Error : The property 'Count' cannot be found on this object. Verify that the property exists.
At C:\Users\xx\Desktop\script.ps1:1675 char:1
Also, does anybody know how to get out-host to be the default instead of always pipeling to out-host?

how to read unique values from excel column using powershell

i would like to read unique email id from a column and assign to a local variable, can anyone assist in doing that
my data will be like
i would like to get unique values from the excel and assign it to a variable using power shell
the variable should hold value in following way Nalin23#bridgestone.com;raj#bridgestone.com;kishan#bridgestone.com
To read the values from an Excel column and return it as array of values, you can use this helper function:
function Import-ExcelColumn {
# returns an array of Excel Column values
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Position = 0)]
[string]$Path,
[Parameter(Mandatory = $false, Position = 1)]
[int]$WorkSheetIndex = 1,
[Parameter(Mandatory = $false, Position = 2)]
[int]$ColumnIndex = 1
)
# constants from https://learn.microsoft.com/en-us/office/vba/api/excel.xldirection
$xlDown = -4121
$xlUp = -4162
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $false
$workbook = $excel.Workbooks.Open($Path)
$worksheet = $workbook.Worksheets.Item($WorkSheetIndex)
# get the first and last used row indices
$firstRow = $worksheet.Cells($worksheet.UsedRange.Rows.Count, 1).End($xlUp).Row
$lastRow = $worksheet.Cells($firstRow, 1).End($xlDown).Row
# collect the values in this column in variable $result
# start at $firstRow + 1 to skip the header itself
$result = for ($row = $firstRow + 1; $row -le $lastRow; $row++) {
$worksheet.Cells.Item($row, $ColumnIndex).Value2
}
$excel.Quit()
# IMPORTANT: clean-up used Com objects
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($worksheet) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($workbook) | Out-Null
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($excel) | Out-Null
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
# $result is an array. PowerShell 'unravels' arrays when returned from a function.
# to overcome this, prefix the returned array with a unary comma.
return ,$result
}
After that, in your case use it like this:
$emailAddresses = ((Import-ExcelColumn -Path 'D:\Test\Map1.xlsx' -ColumnIndex 2) | Select-Object -Unique) -join ';'
to get a string:
Nalin23#bridgestone.com;raj#bridgestone.com;kishan#bridgestone.com
Please show the code you have attempted as a reference for everyone answering the qustion.
With that said, the below code should work for a comma separate value (.csv) file:
# Get CSV object
$csv_object = Import-CSV $path_to_csv
# Find unique entries from email_id column
$unique_emails = $csv_object.email_id | Select -Unique
# Join them with ;
$delim_emails = $unqiue_emails -join ";"

How to loop through arrays in hash table - passing parameters based on values read from a CSV file

Curious about how to loop through a hash table where each value is an array. Example:
$test = #{
a = "a","1";
b = "b","2";
c = "c","3";
}
Then I would like to do something like:
foreach ($T in $test) {
write-output $T
}
Expected result would be something like:
name value
a a
b b
c c
a 1
b 2
c 3
That's not what currently happens and my use case is to basically pass a hash of parameters to a function in a loop. My approach might be all wrong, but figured I would ask and see if anyone's tried to do this?
Edit**
A bit more clarification. What I'm basically trying to do is pass a lot of array values into a function and loop through those in the hash table prior to passing to a nested function. Example:
First something like:
$parameters = import-csv .\NewComputers.csv
Then something like
$parameters | New-LabVM
Lab VM Code below:
function New-LabVM
{
[CmdletBinding()]
Param (
# Param1 help description
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias("p1")]
[string[]]$ServerName,
# Param2 help description
[Parameter(Position = 1)]
[int[]]$RAM = 2GB,
# Param3 help description
[Parameter(Position=2)]
[int[]]$ServerHardDriveSize = 40gb,
# Parameter help description
[Parameter(Position=3)]
[int[]]$VMRootPath = "D:\VirtualMachines",
[Parameter(Position=4)]
[int[]]$NetworkSwitch = "VM Switch 1",
[Parameter(Position=4)]
[int[]]$ISO = "D:\ISO\Win2k12.ISO"
)
process
{
New-Item -Path $VMRootPath\$ServerName -ItemType Directory
$Arguments = #{
Name = $ServerName;
MemoryStartupBytes = $RAM;
NewVHDPath = "$VMRootPath\$ServerName\$ServerName.vhdx";
NewVHDSizeBytes = $ServerHardDriveSize
SwitchName = $NetworkSwitch;}
foreach ($Argument in $Arguments){
# Create Virtual Machines
New-VM #Arguments
# Configure Virtual Machines
Set-VMDvdDrive -VMName $ServerName -Path $ISO
Start-VM $ServerName
}
# Create Virtual Machines
New-VM #Arguments
}
}
What you're looking for is parameter splatting.
The most robust way to do that is via hashtables, so you must convert the custom-object instances output by Import-Csv to hashtables:
Import-Csv .\NewComputers.csv | ForEach-Object {
# Convert the custom object at hand to a hashtable.
$htParams = #{}
$_.psobject.properties | ForEach-Object { $htParams[$_.Name] = $_.Value }
# Pass the hashtable via splatting (#) to the target function.
New-LabVM #htParams
}
Note that since parameter binding via splatting is key-based (the hashtable keys are matched against the parameter names), it is fine to use a regular hashtable with its unpredictable key ordering (no need for an ordered hashtable ([ordered] #{ ... }) in this case).
Try this:
for($i=0;$i -lt $test.Count; $i++)
{$test.keys | %{write-host $test.$_[$i]}}
Weirdly, it outputs everything in the wrong order (because $test.keys outputs it backwards).
EDIT: Here's your solution.
Using the [System.Collections.Specialized.OrderedDictionary] type, you guarantee that the output will come out the same order as you entered it.
$test = [ordered] #{
a = "a","1";
b = "b","2";
c = "c","3";
}
After running the same solution code as before, you get exactly the output you wanted.

Powershell array of arrays [duplicate]

This question already has answers here:
Powershell create array of arrays
(3 answers)
Closed 5 years ago.
This is building $ret into a long 1 dimensional array rather than an array of arrays. I need it to be an array that is populated with $subret objects. Thanks.
$ret = #()
foreach ($item in $items){
$subret = #()
$subRet = $item.Name , $item.Value
$ret += $subret
}
there might be other ways but arraylist normally works for me, in this case I would do:
$ret = New-Object System.Collections.ArrayList
and then
$ret.add($subret)
The suspected preexisting duplicate question is indeed a duplicate:
Given that + with an array as the LHS concatenates arrays, you must nest the RHS with the unary form of , (the array-construction operator) if it is an array that should be added as a single element:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = #() # create an empty *array*
foreach ($item in $items) {
$subret = $item.Name, $item.Value # use of "," implicitly creates an array
$ret += , $subret # unary "," creates a 1-item array
}
# Show result
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
This yields:
2
---
n1
v1
---
n2
v2
The reason the use of [System.Collections.ArrayList] with its .Add() method worked too - a method that is generally preferable when building large arrays - is that .Add() only accepts a single object as the item to add, irrespective of whether that object is a scalar or an array:
# Sample input
$items = [pscustomobject] #{ Name = 'n1'; Value = 'v1'},
[pscustomobject] #{ Name = 'n2'; Value = 'v2'}
$ret = New-Object System.Collections.ArrayList # create an *array list*
foreach ($item in $items) {
$subret = $item.Name, $item.Value
# .Add() appends whatever object you pass it - even an array - as a *single* element.
# Note the need for $null = to suppress output of .Add()'s return value.
$null = $ret.Add($subret)
}
# Produce sample output
$ret.Count; '---'; $ret[0]; '---'; $ret[1]
The output is the same as above.
Edit
It is more convoluted to create an array of tuples than fill an array with PsObjects containing Name Value as the two properties.
Select the properties you want from $item then add them to the array
$item = $item | select Name, Value
$arr = #()
$arr += $item
You can reference the values in this array by doing this
foreach($obj in $arr)
{
$name = $obj.Name
$value = $obj.Value
# Do actions with the values
}