Processing items within a powershell array - multiple items - powershell

I have been reading about arrays in powershell and hash tables, I know the basic workings of an array and how to use foreach loop to get items within the array, my challange here is slightly different. I would like to pay what I call a multi dimension array to a script, and process the items contained within the array.
What is my setup.
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
What am i trying to achieve ?
I am trying to achieve the structure of a database table within powershell. So literally treating each array item as a row.
So for example
(1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
could be thought of as a table like below
enter image description here
I then want to loop for each item in the array to get the values, similar to the below example
[0].serverid = 1, [0].servername = server1, [0].locationid = 3, [0].applicationID = 1
[1].serverid = 4, [1].servername = server2, [1].locationid = 6, [1].applicationID = 2
what have I done ?
$x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)
$k = 'serverid','servername','locationid','appid' # key names correspond to data positions in each array in $x
$h = #{}
For($i=0;$i -lt $x[0].length; $i++){
$x |
ForEach-Object{
[array]$h.($k[$i]) += [string]$_[$i]
}
}
$x
for ($i = 0; $i -lt $x.Count; $i++)
{
$myserverid = $x[$i][0]
$myservername = $x[$i][1]
$mylocationid = $x[$i][2]
$myappid = $x[$i][3]
write-host $myserverid
}
The Issues
If I set the following $x = (1,"Server1",3,1), then the loop is somewhat incorrect which is why I think the approach is wrong (more than one item works i.e $x = (1,"Server1",3,1),(4,"Server2",6,2),(3,"Server3",4,3)). The loop only works if you have more than one item within the array, hence why I want to re-examine the way the loop works.
Thanks in advance

Your approach relies on a nested (jagged) array: That is, you have an array of subarrays, each of which represents the tuple of values you want to assign to properties.
If there's only one subarray, you must create the nested array explicitly, using the unary form of , the array constructor operator:
# Construct a 1-element array that contains the 4-element subarray.
$x = , (1,"Server1",3,1)
With two or more, subarrays, you implicitly get a nested array:
# Construct a 3-element array, each element of which contains a 4-element subarray.
$x = (1,"Server1",3,1), (4,"Server2",6,2), (3,"Server3",4,3)
Note that in PSv5+ you could use a custom class to solve your problem:
class Custom {
[int] $serverid; [string] $servername;[int] $locationid; [int] $appid
Custom($propValueArray) {
$this.serverid = $propValueArray[0]; $this.servername = $propValueArray[1]; $this.locationid = $propValueArray[2]; $this.appid = $propValueArray[3]
}
}
# Use an array cast to construct [Custom] instances.
# Note the need for (...) around the array, because casts have high
# precedence in PowerShell.
[Custom[]] ((1,"Server1",3,1), (4,"Server2",6,2), (3,"Server3",4,3))
This would allow for processing such as:
# Construct all objects
$objects = [Custom[]] ((1,"Server1",3,1), (4,"Server2",6,2), (3,"Server3",4,3))
# Process each object.
foreach ($object in $objects) {
($myserverid = $object.serverid) # assign a property to a var; `()` also outputs
# ...
}

Related

Missing property name after reference operator

I have a map, that is originally c++ code, file read, and parsed into a map. The original code was an enum, and didn't have values for all items. $fileContent:
enum{
Error_A = 110,
Error_B,
Error_C,
Error_D,
Error_E,
Error_F,
Error_G = 118,
...
};
I have read the file contents and put it in a map like this (works fine):
function Get-Contents_b{
[cmdletbinding()]
Param ([string]$fileContent)
#Error_AA = 20
# create an ordered hashtable to store the results
$errorMap = [ordered]#{}
# process the lines one-by-one
switch -Regex ($fileContent -split '\r?\n') {
'^[\s]*([\w]+)[\s=]*([-\d]*)' { # Error...=12345
$key,$value = ($matches[1,2])|ForEach-Object Trim
$errorMap[$key] = $value
}
}
...
Then I want to iterate over the map, and for the ones with enum values dependent on single digit increase from the previous, I want to assign the value of the previous value plus one. I'm trying to do that below, but getting the $previousKey, using $key-1, and then getting the value from that, is giving the error shown in the comment.
foreach ($key in $errorMap.$keys)
{
$previousKey = $errorMap.[($key-1)] #missing property name after the reference operator
Write-Host $errorMap.$previousKey
if(($errorMap.$key).Value = "")
{
$errorMap.$key.Value = $errorMap.$previousKey.Value + 1
}
}
Any ideas how to fix this or get the previous value and assign the next empty value the previous value plus one?
This is with powershell 5.1 and VSCode.
You're mistakenly mixing member (property) access via the . operator with indexed access via [...] - you must use one or the other.
However, what you want is positional access to your keys (which only works with an ordered hashtable):
foreach ($keyIndex in 0..($errorMap.Count-1))
{
if ('' -eq $errorMap[$keyIndex]) {
$previousValue = $errorMap[$keyIndex - 1]
Write-Host $previousValue
$errorMap[$keyIndex] = 1 + $previousValue
}
}
Why not create the values in your Hashtable straight away instead of filling the empties afterwards?
function Get-Contents_b{
[cmdletbinding()]
Param ([string]$fileContent)
# create an ordered hashtable to store the results
$errorMap = [ordered]#{}
$currentValue = 0
# process the lines one-by-one
switch -Regex ($fileContent -split '\r?\n') {
'^\s+(\w+)\s*[,=]'{
$key, $value = ($_ -split '[,=]', 2).Trim()
if ([string]::IsNullOrWhiteSpace($value)) { $value = $currentValue }
$errorMap[$key] = [int]$value
$currentValue = [int]$value + 1
}
}
# return the map
$errorMap
}
Get-Contents_b $enum
Output:
Name Value
---- -----
Error_A 110
Error_B 111
Error_C 112
Error_D 113
Error_E 114
Error_F 115
Error_G 118

Convert all values of OrderedDictionary of the powershell to string line and print to the log file

I have some problems with converting values of the OrderedDictionary of the PowerShell to the string line. I have the following hash table [OrderedDictionary]:
I try outputting data of values to a string:
for ($i = 0; $i -lt $DataResult.Count; $i++) {
$DataResult["$i"].Values |
ForEach-Object {
Write-Output $_
}
}
But it doesn't work, could you help me
Given your code sample I think you have an array of [Ordered] dictionaries? If so you should be able to unroll the values quite easily:
#This is just demo data for my testing:
$Dictionaries = #(
[Ordered]#{
P1 = 'Something'
P2 = 'SomethingElse'
}
[Ordered]#{
P1 = 'Another'
P2 = 'AnotherAnother'
}
)
# Unroll the values:
$Values = $Dictionaries.Values
If you ignore the demo data it's really just 1 line. $Values would be of type [Object[]] the elements of which are in their original string type. If needed you can re-cast as a string array:
$Values = [String[]]$values
Or you can specify directly with the unrolling:
# Unroll the values:
$Values = [String[]]$Dictionaries.Values
Or Type constrain the variable:
# Unroll the values:
[String[]]$Values = $Dictionaries.Values
Note: This casting will convert element values to strings as well. I'm going on the basis that's desired.
I'd also point out you don't really need Write-Output anywhere. Firstly, that's already implicit in normal PowerShell operations. Secondly, you cannot pipe a traditional For loop (although you can assign its output to a variable). At any rate, if your intent is to simply continue feeding this down the pipeline, you could remove $Values from any of the above examples and PowerShell will implicitly and natively do just that.

Parsing a file to create an array of lines

This seems so incredibly simple but I am missing something. I just need to add an array to array[0], array[1], etc.
I am taking a vcard file and trying to read all the lines of one vcard and put them in an array and then place that array in an array so array[0] will be vcard 1, array[1] will be the next, etc.
$c = Get-Content -Path C:\temp\Contacts_Backup.vcf
$counter=0
$contact=#()
$allcontacts=#()
Foreach ($line in $c){
$contact += $line
if ($line -eq 'END:VCARD'){
$allcontacts[$counter++] = $contact
$contact=#()
}
}
Result:
Unable to index into an object of type System.String.
tl;dr:
You cannot "grow" an array by assigning to a nonexistent index; if you start with #() - an empty array - you must use += to "append" elements (arrays are fixed-size collections, so what really happens is that a new array must be allocated every time that contains the old elements followed by the new one).
Using += is therefore inefficient in loops, and there are two alternatives:
Use a .NET extensible list type to build an array-like collection more efficiently.
Preferably - because it is both more convenient and faster - let PowerShell create the array for you, simply by capturing the output from a foreach loop in a variable
($array = #(foreach (...) { ... }))
Details below.
Your code indeed has a problem, though the symptom it would produce differs from what your question currently states; using a simplified example:
PS> $allcontacts=#(); $allcontacts[0] = 'one', 'two'
Index was outside the bounds of the array. # ERROR
...
That is, #() creates an empty array, which you cannot implicitly "extend" by accessing a non-existent index.
Using +=, as you do with your $contacts array, does work:
$allcontacts=#(); $allcontacts += , ('one', 'two')
Note the use of array-construction operator , to ensure that the RHS operand is added as a whole as a single new element; without it, multiple elements would be added, one for each element.
However, while "extending" an array with += works, in reality you're creating a new array behind the scenes every time, because arrays are by definition fixed-size collections.
With larger collections, this can become a performance issue, and it is better to use a list data type instead, such as [System.Collections.Generic.List[object]][1]:
$allcontacts = New-Object Collections.Generic.List[object]
$allcontacts.Add(('one', 'two'))
Note the need to enclose the array to add - as a single list element - in (...) so that the .Add() method recognizes it as a single argument.
Taking a step back: You can let PowerShell collect the $contact sub-arrays in the overall $allcontacts array by simply capturing the output from the entire foreach command:
$c = Get-Content -Path C:\temp\Contacts_Backup.vcf
$contact=#()
$allcontacts = #(foreach ($line in $c){
$contact += $line
if ($line -eq 'END:VCARD'){
# Output the $contact array as a *single* object,
# using ",", the array-construction operator
, $contact
# Reset for the next contact.
$contact=#()
}
})
$allcontacts will end up as a regular PowerShell array, typed [object[]].
Use of the array-subexpression operator (#(...)) is only necessary if you need to ensure that $allcontacts is an array even if the *.vcf file contains only one contact definition.
[1] A non-generic alternative is [System.Collections.ArrayList], but its downside is that its .Add() method returns a value, requiring you to suppress that value with, e.g., $null = $arrayList.Add(...) so as not to pollute PowerShell's output stream.
This should do exactly what you want:
Add-Type -AssemblyName System.Collections
[System.Collections.Generic.List[object]]$allContacts = #()
[System.Collections.Generic.List[string]]$contact = #()
$filePath = 'C:\temp\Contacts_Backup.vcf'
$endMarker = 'END:VCARD'
foreach($line in [System.IO.File]::ReadLines($filePath))
{
if( $line -eq $endMarker ) {
$allContacts.Add( $contact.ToArray() )
$contact.Clear()
}
else {
$contact.Add( $line )
}
}
# Ready. Show result.
foreach( $vcf in $allContacts ) {
"Contact: "
$vcf
}

Adding a hash-table to a hash-table

I'm trying to add a hash-table to a hash-table using powershell. However, Im getting the following error:
Item has already been added. Key in dictionary: 'Dev' Key being added: 'Dev'
Here's my code:
$colors = #("black","white","yellow","blue")
$Applications=#{}
Foreach ($i in $colors)
{
$Applications += #{
Colour = $i
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
}
What am I doing wrong?
I think what you want is something more like this:
$colors = #("black","white","yellow","blue")
$Applications=#{}
Foreach ($i in $colors)
{
$Applications[$i] = #{
Colour = $i
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
}
I will also point out that Hashtables often need to be handled defensively. Each key must be unique but values do not need to be. Here is the typical method of handling that:
$colors = #("black","white","yellow","blue")
$Applications=#{}
Foreach ($i in $colors)
{
if($Applications.ContainsKey($i)){
#Do things here if there is already an entry for this key
}else{
$Applications[$i] = #{
Colour = $i
Prod = 'SrvProd05'
QA = 'SrvQA02'
Dev = 'SrvDev12'
}
}
}
EBGreen's helpful answer offers a solution for what you likely meant to do.
To complement that with an explanation of why your code failed:
When you use + to "add" two hashtables, their entries are merged: in other words: the entries of the RHS are added to the LHS hashtable.
(Technically, a new instance is created with the merged entries.)
However - by sensible design - merging is only performed if the hashtables have no keys in common; otherwise, you'll get the error message you saw, complaining about duplicate keys.
If this safeguard weren't in place, you would lose data if the values associated with duplicate entries differ.
Since your loop repeatedly tried to merge a hashtable with the same keys directly into an existing hashtable, your 2nd loop iteration invariably failed.
You can verify this more simply:
$Applications = #{} # create empty hashtable.
# Merge a hashtable literal into $Applications.
# This works fine, because the two hashtables have no keys in common.
$Applications += #{ first = 1; second = 2 }
# $Application now contains the following: #{ first = 1; second = 2 }
# If you now try to add a hashtable with the same set of keys again,
# the operation invariably fails due to duplicate keys.
$Applications += #{ first = 10; second = 20 } # FAILS
# By contrast, adding a hashtable with unique keys works fine:
$Applications += #{ third = 3; fourth = 4 } # OK
# $Application now contains: #{ first = 1; second = 2; third = 3; fourth = 4 }
I was looking for a way to do some sort of "list of enum" and since I'm fairly new to PowerShell, the only thing I could think of is "HashTable of HashTables!".
Could not find a way to do it all "inline", and I got some "can't add a null key to a hashtable" error. After some experimentation I ended up with this, working exactly as I wanted :
$Enums = #{}
$Enums.Frequency = #{
Daily = "daily"
Weekly = "weekly"
Monthly = "monthly"
}
$Enums.SomethinElse = #{
Thing1 = "dflskdjf"
Thing2 = 12
Thing3 = $null
}
You can then use it simply
$Enums.SomethinElse.Thing2
or (if you add it to a Global Variable)
$Global:Enums.Frequency.Daily
It's not exactly the same as the example provided but directly in line with the topic, so I hope it helps someone ending up here :)

Building automatic strings based on a variable

I have a variable called: $backendSubnet
This variable currently contains 4 entries, when I do $backendSubnet.Count it returns '4'
This number of entries will change each time the script is run. What I need to do it automatically break out (based on the number of entries) in this instance there's 4 - to be used in another CmdLet, see.....
-Subnet $backendSubnet1,$backendSubnet2,$backendSubnet3,$backendSubnet2
I need to automate breaking this out arranged like this above.
I have tried the following, bit I think I am on the wrong track:
$max = $backendSubnet.Count -1;0..$max | % {$backendSubnetArray += $backendSubnet[$_]}
I would suggest initializing a new array to a predefined size, then copy N elements from the $backendSubnet into the new array. Then rinse and repeat.
for ($i = 0; $i -lt $backendSubnet.Count; $i += $numToCopy)
{
$numToCopy = [Math]::Min($backendSubnet.Count - $i, $max)
$subset = new-object object[] $numToCopy
[Array]::Copy($backendSubnet, $i, $subset, 0, $numToCopy)
SomeCmdlet -Subnet $subset
}