How to reverse traverse a nested hashtable in PowerShell - powershell

In a previous question, I was given a solution for finding the name of a key by its value. Unfortunately I neglected to consider the application of Security Groups
Old hashtable and working function
$Departments = #{
'Sales' = #{
'SAM' = 'Manager'
'SAP' = 'Person'
}
'IT' = #{
'ITM' = 'Manager'
'ITS' = 'Specialist'
'ITT' = 'Technician'
'ITC' = 'Consultant'
}
}
function Get-DepartmentOU {
Param (
[CmdletBinding()]
[Parameter(Mandatory = $true)]
[System.String]
$RoleCode
)
# Get the DictionaryEntry in the main Hashtable where the nested Hashtable value matches the role you are looking for.
$Department = $script:Departments.GetEnumerator() | Where-Object { $_.Value.ContainsKey($RoleCode) }
# Print the name of the DictionaryEntry (Your department) and retrieve the value from the Hashtable for the role.
"Department: $($Department.Name) Job Title: $($Department.Value[$RoleCode])"
}
$JobCode = "SAM"
$DepartmentInfo = Get-DepartmentOU -RoleCode $JobCode
$DepartmentInfo
OUTPUT: Department: Sales Job Title: Manager
The above works excellently, however I've now created a deeper hashtable and need to do the same thing, just another level to extract more information.
New hashtable
$Departments = #{
"Parts" = #{
"SG-Parts" = #{
"PAM" = "Parts Manager"
"PAA" = "Parts Advisor"
}
}
"Sales" = #{
"SG-Sales" = #{
"SAP" = "Sales Person"
"SAR" = "Receptionist"
}
"SG-Sales Managers" = #{
"SGM" = "General Manager"
"SAM" = "Sales Manager"
}
}
}
How should I change the working function to display the text contained in the key
OUTPUT: SG: SG-Sales Managers Department: Sales Job Title: Manager
This may help to visualize the data structure that #mathias-r.-jessen's 'flat role table/map' code generates:
$Rolemap = #{
"PAC" = #{
"Title" = "Parts Consultant"
"SG" = "SG-Parts"
"Department" = "Parts"
}
"PAA" = #{
"Title" = "Parts Advisor"
"SG" = "SG-Parts"
"Department" = "Parts"
}
"SGM" = #{
"Title" = "General Manager"
"SG" = "SG-Sales Managers"
"Department" = "Sales"
}
}
The original data structure is quicker for humans to modify as needed as it mimics a common small-scale Active Directory setup.
When using the original data format, each new role addition only has to be added to the correct 'group' within the hashtable. However, reversing the "SAM" = "Sales Manager" to "Sales Manager"="SAM" may help for legibility and logic / structure.
I used $RoleMap |Format-Custom and some manual typing to build the generated table visualization text.

Create a new flat role table from your OU-like hashtable structure:
$RoleMap = #{}
foreach($departmentCode in $Departments.psbase.Keys){
foreach($sgCode in $Departments[$departmentCode].psbase.Keys){
foreach($roleCode in $Departments[$departmentCode][$sgCode].psbase.Keys){
# Create a summary object that includes all 3 pieces of information
# Store in role table and use the "role code" as the key
$RoleMap[$roleCode] = [pscustomobject]#{
Title = $Departments[$departmentCode][$sgCode][$roleCode]
SG = $sgCode
Department = $departmentCode
}
}
}
}
Now you can avoid ...GetEnumerator() | Where-Object { ... } completely when resolving the role code:
function Get-DepartmentOU {
param(
[CmdletBinding()]
[Parameter(Mandatory = $true)]
[string]
$RoleCode
)
if($script:RoleMap.Contains($RoleCode)){
# `Where-Object` no longer needed!
$roleDescription = $script:RoleMap[$RoleCode]
"SG: $($roleDescription.SG) Department: $($roleDescription.Name) Job Title: $($roleDescription.Title)"
}
}

Related

ADD Dictionary to List. In Funtion: $user_list.add("user3", $user_name), I must type in "USER3". How to pass variable to this spot?

$user_list = #{
user1 = [ordered]#{
properties = ("666-555-2345", "1234 E Main St", "Dodge Charger");
misc_data = 34145}
user2 = [ordered]#{
properties = ("666-555-1234", "5678 N Elm St", "Plymouth Dart");
misc_data = 46112}
}
function add_new_user($user_name, $info, $misc){
$user_name = [ordered]#{
properties = $info;
misc_data = $misc}
$user_list.add(**"user3"**, $user_name)
}
add_new_user user3 ("666-555-1357", "9876 S Oak Rd", "Chevy PT Cruiser") 33879
$user_list
NOTE: This is a function, so it is not practical to actually type "User#" (within the function) for each new user.
I do not know how to make it take the passed variable for that first value of .ADD.
$user_list.add("user3", $user_name) =
$user_list.add($user_name, $user_name) =
You can update your hash table from your function like this:
$user_list = #{
user1 = [ordered]#{
properties = "666-555-2345", "1234 E Main St", "Dodge Charger"
misc_data = 34145
}
user2 = [ordered]#{
properties = "666-555-1234", "5678 N Elm St", "Plymouth Dart"
misc_data = 46112
}
}
function add_new_user($user_name, $info, $misc) {
if($user_list.ContainsKey($user_name)) {
return "$user_name already exists in `$userList"
}
$user_list[$user_name] = [ordered]#{
properties = $info
misc_data = $misc
}
}
I have added a condition to check if the user is already there so that instead of replacing the key / value pair it would return the message that the user already exists.
If you test it you would see the following:
PS /> add_new_user user3 ("666-555-1357", "9876 S Oak Rd", "Chevy PT Cruiser") 33879
PS /> $user_list
Name Value
---- -----
user1 {properties, misc_data}
user3 {properties, misc_data}
user2 {properties, misc_data}
PS /> add_new_user user2 ("666-555-1357", "9876 S Oak Rd", "Chevy PT Cruiser") 33879
user2 already exists in $userList

Split PSCustomObjects in to strings/array

Hi I have n sum of objects for each Node like this:
NodeName : 11111
System.AreaId : 2375
System.AreaPath : Project
System.TeamProject : Project
System.NodeName : Project
System.AreaLevel1 : Project
Every node can have different objects in it.
How can I split them to an arrays/strings without specifying the object name so I can create foreach separate object loop?
mklement0 beat me to what I was going to post. Since I have the code drafted already I will post it.
Like mklement0 said in comments, you can access object properties through use of .psobject.Properties. Below in the code I am using a switch statement to check if an object contains a specific property.
$objs = #(
[pscustomobject]#{
AreaId = 2375
AreaPath = ''
TeamProject = 'Project2'
NodeName = ''
AreaLevel1 = ''
},
[pscustomobject]#{
AreaId = 342
AreaPath = ''
TeamProject = 'Project2'
Color = 'Red'
}
)
switch ($objs) {
{ $_.psobject.properties.name -contains 'Color' } {
'Object contains Color property'
}
{ $_.psobject.properties.name -contains 'NodeName' } {
'Object contains NodeName property'
}
Default {}
}

Powershell function receiving multiple parameters from pipeline

I'm writing a function as follows:
Function Display-ItemLocation {
Param(
[ Parameter (
Mandatory = $True,
Valuefrompipeline = $True ) ]
[ String ]$stringItem,
[ Parameter (
Mandatory = $False,
Valuefrompipeline = $True ) ]
[ String ]$stringLocation = 'unknown'
)
Echo "The location of item $stringItem is $stringLocation."
}
Display-ItemLocation 'Illudium Q-36 Explosive Space Modulator' 'Mars'
Display-ItemLocation 'Plumbus'
It works fine as written.
The location of item Illudium Q-36 Explosive Space Modulator is Mars.
The location of item Plumbus is unknown.
I'd like to be able to pre-load an array with multiple data pairs and send it via pipeline into the function.
$Data = #(
#('Bucket','Aisle 1'),
#('Spinach Pie','Freezer 4')
)
$Data | Display-ItemLocation
I can't find the magic syntax to get this to work. Can the function accept a pair of values at the same time from the pipeline?
Define your pipeline-binding parameters as binding by property name - ValuefromPipelineByPropertyName - and then pipe (custom) objects that have such properties:
Function Display-ItemLocation {
Param(
[ Parameter (
Mandatory,
ValuefromPipelineByPropertyName ) ]
[ String ]$stringItem,
[ Parameter (
Mandatory = $False,
ValuefromPipelineByPropertyName ) ]
[ String ]$stringLocation = 'unknown'
)
process { # !! You need a `process` block to process *all* input objects.
Echo "The location of item $stringItem is $stringLocation."
}
}
As an aside: Display is not an approved verb in PowerShell.
Now you can pipe to the function as follows; note that the property names must match the parameter names:
$Data = [pscustomobject] #{ stringItem = 'Bucket'; stringLocation = 'Aisle 1' },
[pscustomobject] #{ stringItem = 'Spinach Pie'; stringLocation = 'Freezer 4' }
$Data | Display-ItemLocation
The above yields:
The location of item Bucket is Aisle 1.
The location of item Spinach Pie is Freezer 4.
The above uses [pscustomobject] instances, which are easy to construct ad hoc.
Note that hash tables (e.g., just #{ stringItem = 'Bucket'; stringLocation = 'Aisle 1' }) do not work - although changing that is being discussed in this GitHub issue.
In PSv5+ you could alternatively define a custom class:
# Define the class.
class Product {
[string] $stringItem
[string] $stringLocation
Product([object[]] $itemAndLocation) {
$this.stringItem = $itemAndLocation[0]
$this.stringLocation = $itemAndLocation[1]
}
}
# Same output as above.
[Product[]] (
('Bucket', 'Aisle 1'),
('Spinach Pie', 'Freezer 4')
) | Display-ItemLocation
Thanks to #mklement0 for leading me to this solution. I came up with two options to resolve my dilemma using an array.
Option 1: Use .ForEach to pass the parameters in the usual way.
$Data = #(
#('Bucket','Aisle 1'),
#('Spinach Pie','Freezer 4')
)
$Data.ForEach({Format-ItemLocation "$($_[0])" "$($_[1])"})
Option 2: Using the pipeline (which is what I was after), I modified the function as mklement0 suggested to enable the ValuefromPipelineByPropertyName and to include a Process { } block.
Function Format-ItemLocation {
Param (
[ Parameter (
Mandatory = $True,
ValuefromPipelineByPropertyName = $True ) ]
[ String ]$stringItem,
[ Parameter (
Mandatory = $False,
ValuefromPipelineByPropertyName = $True ) ]
[ String ]$stringLocation = 'unknown'
)
Process {
"The location of item $stringItem is $stringLocation."
}
}
I pass the array via pipeline to a middle step to assign the parameter names to a [PSCustomObject]. This greatly reduces the amount of text that would bulk up the code, and it's the reason I was searching for a more elegant solution.
$Data = #(
#('Bucket','Aisle 1'),
#('Spinach Pie','Freezer 4')
)
$Data |
ForEach-Object { [PSCustomObject]#{stringItem=$_[0];stringLocation=$_[1]} } |
Format-ItemLocation
I changed the function name to Format-* as recommended.

Tidying up a powershell script

So I need help tidying up a script that I have. The purpose of this script is to make 18 different sql files based on the data below 18 different column headers. What my script does now is make 1 sql file based on which column I choose to input via "Read-Host". This is my current script
function get-header
{Read-Host "Type the Column header betwen B-Z for which sql files needs to be created"
}
function get-column
{
Read-Host "Type the Column number"
}
do
{
$val = get-header
}
while(!($val))
do
{$col = get-column
}
while(!($col))
switch ($val)
{
"B"{$column = "1"}
"C"{$column = "2"}
"D"{$column = "3"}
"E"{$column = "4"}
"F"{$column = "5"}
"G"{$column = "6"}
"H"{$column = "7"}
"I"{$column = "8"}
"J"{$column = "9"}
"K"{$column = "10"}
"L"{$column = "11"}
"M"{$column = "12"}
"N"{$column = "13"}
"O"{$column = "14"}
"P"{$column = "15"}
"Q"{$column = "16"}
"R"{$column = "17"}
"S"{$column = "18"}
"T"{$column = "19"}
"U"{$column = "20"}
"V"{$column = "21"}
"W"{$column = "22"}
"X"{$column = "23"}
"Y"{$column = "24"}
"Z"{$column = "25"}
default { $column = 'Unknown' }
}
if ($column -eq 'Unknown')
{
Write-Warning "Not a valid input"
return
}
$csv = Import-Csv "Indices Updates - September 2018.csv" -Header 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26
$date = (Get-Date -Format "MM/dd/yyyy").Replace("-","/")
$sql = #("INSERT INTO benchmark_values(created_at,benchmark_id,date,amount,created_by_username)
foreach($data in $csv)
{
$secondcolumn = [int]$column + 1
$sql += "('$date',$col,'$($data.1)',$($data.$secondcolumn),'BPylla'),"
}
$sql | Out-File "sqldata.sql"
Now I want to get rid of read-host entirely because I dont want to input any values. I also will give an example of what the csv file looks like and what the sql file should look like.
So the goal is to produce different sql files from each column of information using the the sql format posted. I already have the template for that in my script now, I just need the script to create all the sql files based on headers and still input the data below the headers in the new sql files. Any help would be greatly appreciated! Thanks!

How to use "*" in nodeName

Say I have 2 roles in my DSC setup and I have variable amount of nodes in my setup:
$configdata = #{
AllNodes = #(
#{
NodeName = "*Web*" # < problem lies here
# can be prodWeb## or devWeb##
Role = "IIS", "basic"
}
#{
NodeName = "*"
Role = "basic"
}
)
}
DSC resource:
Configuration CFG
{
$AllNodes.where{ $_.Role.Contains("Basic") }.NodeName
{
...
}
$AllNodes.where{ $_.Role.Contains("IIS") }.NodeName
{
...
}
}
Can I achieve that?
The AllNodes entry in configuration data is an array of hashtables. Each hashtable needs to have a key NodeName. The value will get substituted when the expression evaluates. So a nodename like web* will not work
So, basically what I've done is this:
Configuration Windows
{
node $allnodes.NodeName {
switch ($Node.Role) {
"Role1" {
...
}
"Role2" {
...
}
"Role3" {
...
}
}
}
}
My configuration data:
#{ AllNodes = #( #{ NodeName = "web"; Role = "Role1", "Role2" } ) }
And for another set of nodes:
#{ AllNodes = #( #{ NodeName = "other"; Role = "Role1", "Role3" } ) }
I'm using Azure Automation to assign configurations to nodes, so it doesn't check the node name, it just applies whatever roles the configuration had at compile time.