Split PSCustomObjects in to strings/array - powershell

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 {}
}

Related

How to reverse traverse a nested hashtable in 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)"
}
}

Is it possible to create specific constant elements of a PowerShell array?

Let's say I have array Grid that was initialized with
$Grid = #(#(1..3), #(4..6), #(7..9))
and I want to change Grid[0][0] to value "Test" but I want to make it unchangeable, is there a way I could to that?
So far I've tried playing around with classes that allow read-only or constant declarations through the class as opposed to using New-Variable/Set-Variable but it doesn't affect the index itself but the individual element as in
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0] # 1 \n 2 \n 3
$Grid[0][0].IsReadOnly # True
$Grid[0][0] = "test"
$Grid[0][0] # test
I assume this is due to $Grid[0][0] being read-only as opposed to constant and the behaviour I experienced supported that:
$test = [Array]::AsReadOnly(#(1,2,3,4))
$test[0]=1 # Errors
$test = "test"
$test # test
$Grid = #(#(1..3), #(4..6), #(7..9))
$Grid[0][0] = [Array]::AsReadOnly(#(1,2,3))
$Grid[0][0][0] = 1 # Errors
$Grid[0][0] = "test"
$Grid[0][0] # test
I'm not sure what to try next and I know that this is very simple with classes but I am not looking for that as a solution.
You'll have to make both dimensions of your nested array read-only to prevent anyone from overwriting $grid[0]:
$grid =
[array]::AsReadOnly(#(
,[array]::AsReadOnly(#(1,2,3))
,[array]::AsReadOnly(#(3,2,1))
))
(the unary , is not a typo, it prevents PowerShell from "flattening" the resulting read-only collection)
Now $grid should behave as you expect:
$grid[0] # 1,2,3
$grid[0][0] # 1
$grid[0][0] = 4 # error
$grid[0] = 4 # error
If you want to be able to prevent writing to individual "cells", you'll have to define a custom type:
using namespace System.Collections
class Grid {
hidden [int[,]] $data
hidden [bool[,]] $mask
Grid([int]$width,[int]$height){
$this.mask = [bool[,]]::new($width, $height)
$this.data = [int[,]]::new($width, $height)
}
[int]
Get([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
return $this.data[$x,$y]
}
Set([int]$x,[int]$y,[int]$value)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
if(-not $this.mask[$x,$y])
{
$this.data[$x,$y] = $value
}
else
{
throw [System.InvalidOperationException]::new("Cell [$x,$y] is currently frozen")
}
}
Freeze([int]$x,[int]$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $true
}
Unfreeze([int]$x,$y)
{
if(-not $this.CheckBounds($x,$y)){
throw [System.ArgumentOutOfRangeException]::new()
}
$this.mask[$x,$y] = $false
}
hidden [bool]
CheckBounds([int]$x,[int]$y)
{
return (
$x -ge $this.data.GetLowerBound(0) -and
$x -le $this.data.GetUpperBound(0) -and
$y -ge $this.data.GetLowerBound(1) -and
$y -le $this.data.GetUpperBound(1)
)
}
}
Now you can do:
$grid = [Grid]::new(5,5)
$grid.Set(0, 0, 1) # Set cell value
$grid.Get(0, 0) # Get cell value
$grid.Freeze(0, 0) # Now freeze cell 0,0
$grid.Set(0, 0, 2) # ... and this will now throw an exception
$grid.Set(0, 1, 1) # Setting any other cell still works
If you want native support for index expressions (ie. $grid[0,0]), the Grid class will need to implement System.Collections.IList

How to convert a string value to a dynamic data type in PowerShell?

Is it possible to assign a string value to a variable of a different type given that the data type is not known in advance? For example, in the sample below, how do I update the values of the $values hash without changing their data types:
$values = #{
"Boolean" = $true
"Int" = 5
"DateTime"= (Get-Date)
"Array" = #("A", "B", "C")
}
$stringValues = #{
"Boolean" = 'false'
"Int" = '10'
"DateTime"= '2019-01-02 14:45:59.146'
"Array" = '#("X", "Y", "Z")'
}
"INITIAL VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
"`nUPDATING..."
foreach ($key in $stringValues.Keys) {
$values[$key] = $stringValues[$key]
}
"`nUPDATED VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
OUTPUT:
INITIAL VALUES:
DateTime = 04/23/2019 16:54:13 (System.DateTime)
Array = A B C (System.Object[])
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
UPDATING...
UPDATED VALUES:
DateTime = 2019-01-02 14:45:59.146 (System.String)
Array = #("X", "Y", "Z") (System.String)
Boolean = false (System.String)
Int = 10 (System.String)
I need the updated values to match the original data types and not just get converted to System.String.
I am flexible on the contents of the strings. E.g. a string holding a boolean false value may be $false/false/[boolean]false/[boolean]$false/etc or a string holding an array may use a different formatting (basically, whatever is easier to convert the string to a proper data type).
In essence, I want to simulate whatever the ConvertFrom-Json cmdlet does when it sets the object property from a JSON string, only in my case, I do not have a JSON structure.
(In case someone wonders what I'm trying to do: I am trying to add an INI file parser to my ConfigFile module, and no, I cannot just use a hash to return the INI settings; I need to load the values into the corresponding PSVariables and for this to work, I need to convert strings to proper data types.)
So you want to cast/convert the new value to the type of the old value.
The idea needs to cast from a variable,
here is a related question powershell-type-cast-using-type-stored-in-variable
The answer suggest:
You can roughly emulate a cast using the following method:
[System.Management.Automation.LanguagePrimitives]::ConvertTo($Value, $TargetType)
The following changed routine shows: it isn't that simple, especially when the new data needs overloads/other parameters in the conversion.
"UPDATING..."
foreach ($key in $stringValues.Keys) {
$values[$key] = [System.Management.Automation.LanguagePrimitives]::ConvertTo(
$stringValues[$key], $values[$key].gettype())
}
My German locale error message:
Ausnahme beim Aufrufen von "ConvertTo" mit 2 Argument(en): "Der Wert "2019-01-02 14:45.59.146" kann nicht in den Typ
"System.DateTime" konvertiert werden. Fehler: "Die Zeichenfolge wurde nicht als gültiges DateTime erkannt.""
In Zeile:2 Zeichen:5
+ $values[$key] = [System.Management.Automation.LanguagePrimitives] ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : PSInvalidCastException
And the unsufficient result:
DateTime = 04/24/2019 09:49:19 (System.DateTime)
Array = #("X", "Y", "Z") (System.Object[])
Boolean = True (System.Boolean)
Int = 10 (System.Int32)
You may elaborate yourself on this idea, handling old types/new data more individually.
You can use a custom class in lieu of a hashtable; unlike hashtable keys, the properties of custom classes can be specifically typed.
With scalar values, you can then simply let PowerShell perform the from-string conversion for you - except that Boolean strings need special treatment (see comments in source code below).
With arrays, things are trickier. The solution below uses [System.Management.Automation.PSParser]::Tokenize() to parse the string, but is currently limited to recognizing string and number literals.
Note: It is tempting to use Invoke-Expression on the entire array, but that would be a security risk, because it opens to the door to arbitrary code execution. While there are legitimate uses - such as on a string known to represent a number below - Invoke-Expression should generally be avoided.
(If you don't want to define classes, you can derive the types from the values of hashtable $values and use [System.Management.Automation.LanguagePrimitives]::ConvertTo() to convert the strings to those types, as shown in LotPings' answer, though note that arrays and Booleans still need special treatment as shown below.)
# Define a custom [Values] class
# with specifically typed properties.
class Values {
[bool] $Boolean
[int] $Int
[datetime] $DateTime
[Array] $Array
}
# Instantiate a [Values] instance
$values = [Values] #{
Boolean = $true
Int = 5
DateTime= (Get-Date)
Array = #("A", "B", "C")
}
$stringValues = #{
Boolean = 'false'
Int = '10'
DateTime = '2019-01-02 14:45:59.146'
Array = '#("X", "Y", "Z")'
}
"INITIAL VALUES:"
foreach ($key in $values.psobject.properties.Name) {
($key + " = " + $values.$key + " (" + $values.$key.GetType().FullName + ")")
}
""
"UPDATING..."
foreach ($key in $stringValues.Keys) {
switch ($key) {
'Array' {
# Parse the string representation.
# Assumptions and limitations:
# The array is flat.
# It is sufficient to only support string and numeric constants.
# No true syntax validation is needed.
$values.$key = switch ([System.Management.Automation.PSParser]::Tokenize($stringValues[$key], [ref] $null).Where( { $_.Type -in 'String', 'Number' })) {
{ $_.Type -eq 'String' } { $_.Content; continue }
{ $_.Type -eq 'Number' } { Invoke-Expression $_Content; continue }
}
continue
}
'Boolean' { # Boolean scalar
# Boolean strings need special treatment, because PowerShell considers
# any nonempty string $true
$values.$key = $stringValues[$key] -notin 'false', '$false'
continue
}
default { # Non-Boolean scalar
# Let PowerShell perform automatic from-string conversion
# based on the type of the [Values] class' target property.
$values.$key = $stringValues[$key]
}
}
}
""
"UPDATED VALUES:"
foreach ($key in $values.psobject.properties.Name) {
($key + " = " + $values.$key + " (" + $values.$key.GetType().FullName + ")")
}
This yields:
INITIAL VALUES:
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
DateTime = 04/24/2019 14:45:29 (System.DateTime)
Array = A B C (System.Object[])
UPDATING...
UPDATED VALUES:
Boolean = True (System.Boolean)
Int = 10 (System.Int32)
DateTime = 01/02/2019 14:45:59 (System.DateTime)
Array = X Y Z (System.Object[])
Agreed on the Write-Host thing. It should really only be used to leverage color output and some specific format cases. Output to the screen is the default as you'll see in my response.
You could do the below, but that date string is a bit odd, well, for me, well, I've not seen anyone use that format. So, I modified it for US style, but change as needed for your language.
$values = #{
'Boolean' = $true
'Int' = 5
'DateTime'= (Get-Date)
'Array' = #('A', 'B', 'C')
}
$stringValues = #{
'Boolean' = 'false'
'Int' = '10'
'DateTime'= '2019-01-02 14:45:59'
'Array' = "#('X', 'Y', 'Z')"
}
'INITIAL VALUES:'
foreach ($key in $values.Keys)
{
"$key = $($values[$key]) $($values[$key].GetType())"
}
"`nUPDATING..."
foreach ($key in $stringValues.Keys)
{
switch ($key)
{
Boolean {[Boolean]$values[$key] = $stringValues['$'+$key]}
Int {[Int]$values[$key] = $stringValues[$key]}
DateTime {[DateTime]$values[$key] = $stringValues[$key]}
Array {[Array]$values[$key] = $stringValues[$key]}
default {'The value could not be determined.'}
}
}
"`nUPDATED VALUES:"
foreach ($key in $values.Keys)
{
"$key = $($values[$key]) $($values[$key].GetType())"
}
# Results
INITIAL VALUES:
DateTime = 04/24/2019 01:44:17 datetime
Array = A B C System.Object[]
Boolean = True bool
Int = 5 int
UPDATING...
UPDATED VALUES:
DateTime = 01/02/2019 14:45:59 datetime
Array = #("X", "Y", "Z") System.Object[]
Boolean = False bool
Int = 10 int
Thanks to #LotPings, #mklement0, and #postanote for giving me a few ideas, so here is the solution I will go with:
$values = #{
"Boolean" = $true
"Int" = 5
"DateTime"= (Get-Date)
"Array" = #("A", "B", "C")
}
$stringValues = #{
"Boolean" = 'false'
"Int" = '10'
"DateTime"= '2019-01-31 14:45:59.005'
"Array" = 'X,Y,Z'
}
"INITIAL VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
"`nUPDATING..."
foreach ($key in $stringValues.Keys) {
$value = $stringValues[$key]
if ($values[$key] -is [Array]) {
$values[$key] = $value -split ','
}
elseif (($values[$key] -is [Boolean]) -or ($values[$key] -is [Switch])) {
$values[$key] = $value -notin 'false', '$false', '0', ''
}
else {
$values[$key] = [System.Management.Automation.LanguagePrimitives]::ConvertTo($value, $values[$key].GetType())
}
}
"`nUPDATED VALUES:"
foreach ($key in $values.Keys) {
($key + " = " + $values[$key] + " (" + $values[$key].GetType().FullName + ")")
}
OUTPUT:
INITIAL VALUES:
DateTime = 04/25/2019 09:32:31 (System.DateTime)
Array = A B C (System.Object[])
Boolean = True (System.Boolean)
Int = 5 (System.Int32)
UPDATING...
UPDATED VALUES:
DateTime = 01/31/2019 14:45:59 (System.DateTime)
Array = X Y Z (System.String[])
Boolean = False (System.Boolean)
Int = 10 (System.Int32)
I adjusted the format of the array in the string value (which, as I mentioned in the question, was an option). The actual code that will use this will be a bit different, but the basic idea is here. The only caveat that I noticed is that the array data type gets changed from object[] to string[]. Ideally, I'd like to keep it as-is, but it would not change the functionality of the code, so it is fine. Thanks again to all for the ideas and corrections, and if you come up with better alternatives, feel free to post.

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!

Sort and delete duplicates in multidimensional array

I have an multidimensional Array and want to sort it by the date and delete the duplicate entries (by date).
$arrayPlaces = [System.Collections.ArrayList]#()
and that is how the Array looks like:
$arrayPlaces.Add(($mailBarcodeInformation[2], $mailScannedStart.Substring(22), "", "", "", "", "", ""))
The empty fields are filled later. The second field $mailScannedStart.Substring(22) is the time and the Array should be sorted by that and the duplicates should also be removed.
I searched a lot but couldn't find any help.
A common approach is to add a custom object into an array and use the buit-in cmdlet sort -unique. It's much more readable:
$arrayPlaces = [System.Collections.ArrayList]#()
#.........
$arrayPlaces.Add([PSCustomObject]#{
barcode = $mailBarcodeInformation[2]
time = $mailScannedStart.Substring(22)
foo1 = ''
foo2 = ''
foo3 = ''
}) >$null
#...........
$sorted = $arrayPlaces | sort time -Unique
However, since you already use a .NET class you can switch to a fast .NET SortedDictionary, which automatically sorts and deduplicates the collection when items are added:
$arrayPlaces = [Collections.Generic.SortedDictionary[string,array]]#{}
Adding and overwriting the old value:
$key = $mailScannedStart.Substring(22)
$arrayPlaces[$key] = $mailBarcodeInformation[2], $key, "", "", "", "", "", ""
Checking for presence of an item using its key:
if ($arrayPlaces.ContainsKey($key)) { ....... }
Removing:
[void]$arrayPlaces.Remove('foo')
Accessing:
$items = $arrayPlaces['foo']
$item = $arrayPlaces['foo'][0]
Enumerating (faster):
foreach ($items in $arrayPlaces.Values) {
# .........
}
Enumerating/pipelining (slower):
$arrayPlaces.Values | foreach { $_[0] = $_[1] + $[2] }