Unexpected results when reusing a custom object for the pipeline - powershell

A while ago I changed my Join-Object cmdlet which appeared to cause a bug which didn’t reveal in any of my testing.
The objective of the change was mainly code minimizing and trying to improve performance by preparing a custom PSObject and reusing this in the pipeline.
As the Join-Object cmdlet is rather complex, I have created a simplified cmdlet to show the specific issue:
(The PowerShell version is: 5.1.16299.248)
Function Test($Count) {
$PSObject = New-Object PSObject -Property #{Name = $Null; Value = $Null}
For ($i = 1; $i -le $Count; $i++) {
$PSObject.Name = "Name$i"; $PSObject.Value = $i
$PSObject
}
}
Directly testing the output gives exactly what I expected:
Test 3 | ft
Value Name
----- ----
1 Name1
2 Name2
3 Name3
Presuming that it shouldn't matter whether I assign the result to a variable (e.g. $a) or not, but it does:
$a = Test 3
$a | ft
Value Name
----- ----
3 Name3
3 Name3
3 Name3
So, apart from sharing this experience, I wonder whether this is programming flaw or a PowerShell bug/quirk?

Your original approach is indeed conceptually flawed in that you're outputting the same object multiple times, iteratively modifying its properties.
The discrepancy in output is explained by the pipeline's item-by-item processing:
Outputting to the console (via ft / Format-Table) prints the then-current state of $PSObject in each iteration, which gives the appearance that everything is fine.
Capturing in a variable, by contrast, reflects $PSObject's state after all iterations have completed, at which point it contains only the last iteration's values, Name3 and 3.
You can verify that output array $a indeed references the very same custom object three times as follows:
[object]::ReferenceEquals($a[0], $a[1]) # $True
[object]::ReferenceEquals($a[1], $a[2]) # $True
The solution is therefore to create a distinct [pscustomobject] instance in each iteration:
PSv3+ offers syntactic sugar for creating custom objects: you can cast a hashtable (literal) to [pscustomobject]. Since this also creates a new instance every time, you can use it to simplify your function:
Function Test($Count) {
For ($i = 1; $i -le $Count; $i++) {
[pscustomobject] #{ Name = "Name$i"; Value = $i }
}
}
Here's your own PSv2-compatible solution:
Function Test($Count) {
$Properties = #{}
For ($i = 1; $i -le $Count; $i++) {
$Properties.Name = "Name$i"; $Properties.Value = $i
New-Object PSObject -Property $Properties
}
}

Related

Constructing a table from raw data in powershell

I am frustrated beyond belief with Powershell at the moment, because I feel stupid for spending 2 whole work days figuring out a (most likely super simple) solution for the following problem: I would like to convert two arrays ($HeaderCells, $DataCells) into a table and nothing seems to work. I tried PSObjects, Arrays, Hash Tables, data tables... Here is my code:
$Table = #()
$HeaderCells = #("Company","Country")
$DataCells = #("Test Inc.","Misc Corp.","USA","UK")
foreach ($HeaderCell in $HeaderCells)
{
foreach ($DataCell in $DataCells)
{
$Table += New-Object -TypeName PSObject -Property #{$HeaderCell=$DataCell}
}
}
$Table
my Output is:
Company
-------
Test Inc.
USA
Misc Corp.
UK
I would like to get two columns (Company and Country), but no matter what I try (even the most nested for loops, I always end up overwriting variables or just getting errors.
My actual use case is actually a bit more complicated (extracting a table from a html page), but solving this part will allow me to continue, I hope.
My desired output would be to have the HeaderCells as the Headers and the DataCells as the Rows, so something like this:
Company Country
------- --------
Test Inc. USA
Misc Corp. UK
If we assume you already have a way to populate $HeaderCells and $DataCells with collections, you could take a more dynamic approach:
$DataCountPerGroup = $DataCells.Count/$HeaderCells.Count
$table = for ($i = 0; $i -lt $DataCountPerGroup; $i++) {
$hash = [ordered]#{}
$dataIncrement = 0
$HeaderCells | Foreach-Object {
$index = $dataIncrement * $DataCountPerGroup
$hash.Add($_,$DataCells[$i+$index])
$dataIncrement++
}
[pscustomobject]$hash
}
$table
This assumes that $DataCells.Count % $HeaderCells.Count is 0 and if not, you will need some error checking.
The idea is if you have n number of headers, then you will have n category groups of data with each of those having the exact same number of items.
The outer for loops through the number of items in a data category. This means if you have 4 companies (category 1), 4 countries (category 2), and 4 websites (category 3), the loop will iterate 0 through 3 (4 times). An ordered hash table is initialized at the beginning of the loop. $dataIncrement is a temporary variable to help us jump from the different categories within $DataCells. Once the hash table is populated, it can then be used to construct the custom object that will represent an entry in your table ($table).
A trivial example using the indices of your two arrays would process as follows:
$HeaderCells contains 3 items (indexes 0-2)
$DataCells contains 15 items (3 groups of 5 items)(indexes 0-14)
$i becomes 0.
$hash is initialized.
$hash adds key $HeaderCells[0] and value $DataCells[0].
$hash adds key $HeaderCells[1] and value $DataCells[5].
$hash adds key $HeaderCells[2] and value $DataCells[10].
A custom object is created using $hash and is added to $table.
$i becomes 1.
$hash is initialized.
$hash adds key $HeaderCells[0] and value $DataCells[1].
$hash adds key $HeaderCells[1] and value $DataCells[6].
$hash adds key $HeaderCells[2] and value $DataCells[11].
A custom object is created using $hash and is added to $table.
$i becomes 2.
$hash is initialized.
$hash adds key $HeaderCells[0] and value $DataCells[2].
$hash adds key $HeaderCells[1] and value $DataCells[7].
$hash adds key $HeaderCells[2] and value $DataCells[12].
A custom object is created using $hash and is added to $table.
By now, you can see the repeated processes that are happening. $i will continue to increment and the processes will continue with the same pattern until $i becomes 5. When $i becomes 5, the processing will break out of the loop.
I do not know what data you want to sort out there, but look at the example bellow. Basically I am creating a [PSCustomObject] and attach that as a row of an array.
$list=#(
"Apple Corp",
"Peach LLC",
"Ananas LLC",
"Tomato Corp"
)
$result = New-Object System.Collections.ArrayList
foreach ($name in $list){
if($name -like "*corp"){
$obj=[pscustomobject]#{
Company = $name
Country = 'USA'
}
$result += $obj
}else{
$obj=[pscustomobject]#{
Company = $name
Country = 'UK'
}
$result += $obj
}
}
And the here is the output :
Write-Output $result
Company Country
------- -------
Apple Corp USA
Peach LLC UK
Ananas LLC UK
Tomato Corp USA
This is just PoC to get the idea, the logic you want to implement is all up to you.
I'm a little confused by your example data, but here's how you might structure the code if your data was tweaked a bit to have a repeated list of [company1], [country1], [company2], [country2], etc.
$HeaderCells = #("Company","Country")
$DataCells = #("Test Inc.","USA","Misc Corp.","UK", "Canada Post", "CA")
$Table = #()
for ($i = 0; $i -lt $DataCells.Count; ) {
$row = new-object -TypeName PSObject
# Suggestion to replace the following:
# foreach ($HeaderCell in $HeaderCells)
# {
# $row | Add-Member -Type NoteProperty -Name $HeaderCell -Value $DataCells[ $i++ ]
# }
# with:
$HeaderCells | ForEach-Object {
$row | Add-Member -Type NoteProperty -Name $_ -Value $DataCells[ $i++ ]
}
$Table += $row
}
$Table
You can also try:
$DataCells = "Test Inc.","Misc Corp.","USA","UK"
$half = [math]::Floor($DataCells.Count / 2)
(0..($half - 1)) | ForEach-Object {
[PsCustomObject]#{
Company = $DataCells[$_]
Country = $DataCells[$_ + $half]
}
}
Output:
Company Country
------- -------
Test Inc. USA
Misc Corp. UK

Which operator provides quicker output -match -contains or Where-Object for large CSV files

I am trying to build a logic where I have to query 4 large CSV files against 1 CSV file. Particularly finding an AD object against 4 domains and store them in variable for attribute comparison.
I have tried importing all files in different variables and used below 3 different codes to get the desired output. But it takes longer time for completion than expected.
CSV import:
$AllMainFile = Import-csv c:\AllData.csv
#Input file contains below
EmployeeNumber,Name,Domain
Z001,ABC,Test.com
Z002,DEF,Test.com
Z003,GHI,Test1.com
Z001,ABC,Test2.com
$AAA = Import-csv c:\AAA.csv
#Input file contains below
EmployeeNumber,Name,Domain
Z001,ABC,Test.com
Z002,DEF,Test.com
Z003,GHI,Test1.com
Z001,ABC,Test2.com
Z004,JKL,Test.com
$BBB = Import-Csv C:\BBB.csv
$CCC = Import-Csv C:\CCC.csv
$DDD = Import-Csv c:\DDD.csv
Sample code 1:
foreach ($x in $AllMainFile) {
$AAAoutput += $AAA | ? {$_.employeeNumber -eq $x.employeeNumber}
$BBBoutput += $BBB | ? {$_.employeeNumber -eq $x.employeeNumber}
$CCCoutput += $CCC | ? {$_.employeeNumber -eq $x.employeeNumber}
$DDDoutput += $DDD | ? {$_.employeeNumber -eq $x.employeeNumber}
if ($DDDoutput.Count -le 1 -and $AAAoutput.Count -le 1 -and $BBBoutput.Count -le 1 -and $CCCoutput.Count -le 1) {
#### My Other script execution code here
} else {
#### My Other script execution code here
}
}
Sample code 2 (just replacing with -match instead of Where-Object):
foreach ($x in $AllMainFile) {
$AAAoutput += $AAA -match $x.EmployeeNumber
$BBBoutput += $BBB -match $x.EmployeeNumber
$CCCoutput += $CCC -match $x.EmployeeNumber
$DDDoutput += $AllMainFile -match $x.EmployeeNumber
if ($DDDoutput.Count -le 1 -and $AAAoutput.Count -le 1 -and $BBBoutput.Count -le 1 -and $CCCoutput.Count -le 1) {
#### My Other script execution code here
} else {
#### My Other script execution code here
}
}
Sample code 3 (just replacing with -contains operator):
foreach ($x in $AllMainFile) {
foreach ($c in $AAA){ if ($AllMainFile.employeeNumber -contains $c.employeeNumber) {$AAAoutput += $c}}
foreach ($c in $BBB){ if ($AllMainFile.employeeNumber -contains $c.employeeNumber) {$BBBoutput += $c}}
foreach ($c in $CCC){ if ($AllMainFile.employeeNumber -contains $c.employeeNumber) {$CCCoutput += $c}}
foreach ($c in $DDD){ if ($AllMainFile.employeeNumber -contains $c.employeeNumber) {$DDDoutput += $c}}
if ($DDDoutput.Count -le 1 -and $AAAoutput.Count -le 1 -and $BBBoutput.Count -le 1 -and $CCCoutput.Count -le 1) {
#### My Other script execution code here
} else {
#### My Other script execution code here
}
}
I am expecting to execute the script as quick and fast as possible by comparing and lookup all 4 CSV files against 1 input file. Each files contains more than 1000k objects/rows with 5 columns.
Performance
Before answering the question, I would like to clear some air about measuring the performance of PowerShell cmdlets. Native PowerShell is very good in streaming objects and therefore could save a lot of memory if streamed correctly (do not assign a stream to a variable or use brackets). PowerShell is also capable of invoking almost every existing .Net methods (like Add()) and technologies like LINQ.
The usual way of measuring the performance of a command is:
(Measure-Command {<myCommand>}).TotalMilliseconds
If you use this on native powershell streaming cmdlets, they appear not to perform very well in comparison with statements and dotnet commands. Often it is concluded that e.g. LINQ outperforms native PowerShell commands well over a factor hundred. The reason for this is that LINQ is reactive and using a deferred (lazy) execution: It tells it has done the job but it is actually doing it at the moment you need any result (besides it is caching a lot of results which is easiest to exclude from a benchmark by starting a new session) where of Native PowerShell is rather proactive: it passes any resolved item immediately back into the pipeline and any next cmdlet (e.g. Export-Csv) might than finalize the item and release it from memory.
In other words, if you have a slow input (see: Advocating native PowerShell) or have a large amount data to process (e.g. larger than the physical memory available), it might be better and easier to use the Native PowerShell approach.
Anyways, if you are comparing any results, you should test is in practice and test it end-to-end and not just on data that is already available in memory.
Building a list
I agree that using the Add() method on a list is much faster that using += which concatenates the new item with the current array and then reassigns it back to the array.
But again, both approaches stall the pipeline as they collect all the data in memory where you might be better off to intermediately release the result to the disk.
HashTables
You will probably find the most performance improvement in using a hash table as they are optimized for a binary search.
As it is required to compare two collections to each other, you can't stream both but as explained, it might be best and easiest you use 1 hash table for one side and compare this to each item in a stream at the other side and because you want to compare the AllData which each of the other tables, it is best to index that table into memory (in the form of a hash table).
This is how I would do this:
$Main = #{}
ForEach ($Item in $All) {
$Main[$Item.EmployeeNumber] = #{MainName = $Item.Name; MainDomain = $Item.Domain}
}
ForEach ($Name in 'AAA', 'BBB', 'CCC', 'DDD') {
Import-Csv "C:\$Name.csv" | Where-Object {$Main.ContainsKey($_.EmployeeNumber)} | ForEach-Object {
[PSCustomObject](#{EmployeeNumber = $_.EmployeeNumber; Name = $_.Name; Domain = $_.Domain} + $Main[$_.EmployeeNumber])
} | Export-Csv "C:\Output$Name.csv"
}
Addendum
Based on the comment (and the duplicates in the lists), it appears that actually a join on all keys is requested and not just on the EmployeeNumber. For this you need to concatenate the concerned keys (separated with a separator that is not used in the data) and use that as key for the hash table.
Not in the question but from the comment it appears also that full-join is expected. For the right-join part this can be done by returning the right object in case there is no match found in the main table ($Main.ContainsKey($Key)). For the left-join part this is more complex as you will need to track ($InnerMain) which items in main are already matched and return the leftover items in the end:
$Main = #{}
$Separator = "`t" # Chose a separator that isn't used in any value
ForEach ($Item in $All) {
$Key = $Item.EmployeeNumber, $Item.Name, $Item.Domain -Join $Separator
$Main[$Key] = #{MainEmployeeNumber = $Item.EmployeeNumber; MainName = $Item.Name; MainDomain = $Item.Domain} # What output is expected?
}
ForEach ($Name in 'AAA', 'BBB', 'CCC', 'DDD') {
$InnerMain = #($False) * $Main.Count
$Index = 0
Import-Csv "C:\$Name.csv" | ForEach-Object {
$Key = $_.EmployeeNumber, $_.Name, $_.Domain -Join $Separator
If ($Main.ContainsKey($Key)) {
$InnerMain[$Index] = $True
[PSCustomObject](#{EmployeeNumber = $_.EmployeeNumber; Name = $_.Name; Domain = $_.Domain} + $Main[$Key])
} Else {
[PSCustomObject](#{EmployeeNumber = $_.EmployeeNumber; Name = $_.Name; Domain = $_.Domain; MainEmployeeNumber = $Null; MainName = $Null; MainDomain = $Null})
}
$Index++
} | Export-Csv "C:\Output$Name.csv"
$Index = 0
ForEach ($Item in $All) {
If (!$InnerMain[$Index]) {
$Key = $Item.EmployeeNumber, $Item.Name, $Item.Domain -Join $Separator
[PSCustomObject](#{EmployeeNumber = $Null; Name = $Null; Domain = $Null} + $Main[$Key])
}
$Index++
} | Export-Csv "C:\Output$Name.csv"
}
Join-Object
Just FYI, I have made a few improvements to Join-Object cmdlet (use and installation are very simple, see: In Powershell, what's the best way to join two tables into one?) including an easier changing of multiple joins which might come in handy for a request as this one. Although I still do not have full understanding of what you exactly looking for (and have minor questions like: how could the domains differ in a domain column if it is an extract from one specific domain?).
I take the general description "Particularly finding an AD object against 4 domains and store them in variable for attribute comparison" as leading.
In here I presume that the $AllMainFile is actually just an intermediate table existing out of a concatenation of all concerned tables (and not really necessarily but just confusing as it might contain to types of duplicates the employeenumbers from the same domain and the employeenumbers from other domains). If this is correct, you can just omit this table using the Join-Object cmdlet:
$AAA = ConvertFrom-Csv #'
EmployeeNumber,Name,Domain
Z001,ABC,Domain1
Z002,DEF,Domain2
Z003,GHI,Domain3
'#
$BBB = ConvertFrom-Csv #'
EmployeeNumber,Name,Domain
Z001,ABC,Domain1
Z002,JKL,Domain2
Z004,MNO,Domain4
'#
$CCC = ConvertFrom-Csv #'
EmployeeNumber,Name,Domain
Z005,PQR,Domain2
Z001,ABC,Domain1
Z001,STU,Domain2
'#
$DDD = ConvertFrom-Csv #'
EmployeeNumber,Name,Domain
Z005,VWX,Domain4
Z006,XYZ,Domain1
Z001,ABC,Domain3
'#
$AAA | FullJoin $BBB -On EmployeeNumber -Discern AAA |
FullJoin $CCC -On EmployeeNumber -Discern BBB |
FullJoin $DDD -On EmployeeNumber -Discern CCC,DDD | Format-Table
Result:
EmployeeNumber AAAName AAADomain BBBName BBBDomain CCCName CCCDomain DDDName DDDDomain
-------------- ------- --------- ------- --------- ------- --------- ------- ---------
Z001 ABC Domain1 ABC Domain1 ABC Domain1 ABC Domain3
Z001 ABC Domain1 ABC Domain1 STU Domain2 ABC Domain3
Z002 DEF Domain2 JKL Domain2
Z003 GHI Domain3
Z004 MNO Domain4
Z005 PQR Domain2 VWX Domain4
Z006 XYZ Domain1

Export data in the order it was added - PowerShell Export-Csv

I have this code
function get-data()
{
$rec=[PSCustomObject]#()
$DLGP = "" | Select "Name","Grade","Score"
foreach($record in $data)
{
$DLGP.Name=$record.name
$DLGP.Grade=$record.grade
$DLGP.Score=$record.score
$rec += $DLGP
}
return $rec
}
$mydata=get-data
$mydata | Export-Csv -Path $outputPath -NoTypeInformation
The problem is, the data is not exported in the order that I have added it to $rec
How can I get it exported in the order it was added?
Without even using your function, a simple
$mydata = $data | select Name,Grade,Score
would yield the desired result.
Your function is IMO overcomplicated:
$Data = #"
Name,Grade,Score,Dummy
Anthoni,A,10,v
Brandon,B,20,x
Christian,C,30,y
David,D,40,z
"# | ConvertFrom-Csv
function get-data() {
ForEach($record in $data) {
[PSCustomObject]#{
Name =$record.name
Grade=$record.grade
Score=$record.score
}
}
}
$mydata=get-data
$mydata # | Export-Csv -Path $outputPath -NoTypeInformation
Returns here the pretty same order:
Name Grade Score
---- ----- -----
Anthoni A 10
Brandon B 20
Christian C 30
David D 40
LotPings' helpful answer provides effective solutions.
As for what you tried:
By constructing only a single [pscustomobject] instance outside the loop:
$DLGP = "" | Select "Name","Grade","Score"
and then only updating that one instance's properties in each loop iteration:
$DLGP.Name=$record.name
# ....
you effectively added the very same [pscustomobject] instance multiple times to the result array instead of creating a distinct object in each iteration.
Since the same object was being updated repeatedly, that object ended up having the properties of the last object in the input collection, $data.
As an aside:
[PSCustomObject] #() is effectively the same as #(): the [PSCustomObject] is ignored and you get an [object[]] array.
To type the array as one containing [PSCustomObject] instances, you'd have to use an array-typed cast: [PSCustomObject[]] #().
However, given that instances of any type can be cast to [PSCustomObject] - which is really the same as [psobject] - this offers no type safety and no performance benefit.
Also, since your $rec variable isn't type-constrained (it is defined as $rec = [<type>] ... rather than [<type>] $rec = ...) and you're using += to "add to" the array (which invariably requires creation of a new instance behind the scenes), $rec would revert to an [object[]] array after the first += operation.

Sorting and Comparing Arraylists

I'm pretty new to Powershell and have a problem.
It seems I can get neither the Compare-Object nor the Sort-Object funtions to work.
I got two Arraylists which I fill with 10 objects of type "Table".
#Create Arraylists
$list1 = new-object System.Collections.ArrayList
$list2 = new-object System.Collections.ArrayList
#Declare Table-Class
class Table {
[String] $name
[Int] $number
}
#Fill list1
[Int] $i = 0
while ($i -lt 10) {
$entry = new-Object Table
$entry.name = "Name";
$entry.number = $i;
$list1.Add($entry)
$i++
}
#Fill list2
[Int] $j = 10
while ($j -gt 0) {
$entry = new-Object Table
$entry.name = "name";
$entry.number = $j;
$list2.Add($entry)
$j--
}
Now I want to compare these two ArrayLists like this:
Compare-Object $list1 $list2 | ForEach-Object {$_.InputObject}
This doesn't seem to work and I think it's because I'm not really doing anything with the comparison. If someone could help me with the correct Syntax I'd be really happy.
Anyway, I think this comparison would somehow return a $false boolean.
If that were true, I'd like to sort $list2 by $entry.number.
I'm attempting this like that:
$list2 = $list2 | Sort-Object -Property { $_[1] }
But my list doesn't change at all. I already tried several different "solutions" but it seems none of these is a real "solution".
I'd be really glad if someone could help me or at least point me in the right direction.
EDIT: For future readers, here is the working syntax:
#sort list by the property number
$list2 = $list2 | sort-object -Property number
#count differences and if differences greater than 0, do stuff
[Int] $diff
$properties = $list1 | Get-Member -MemberType Property | Select-Object -ExpandProperty Name
foreach ($property in $properties) {
$diff = 0
$diff = (Compare-Object $list1 $list2 -Property "$property").count
if ($diff -ne 0) {
#do stuff
}
else {
#do something else
}
}
Your sorting does not work because -Property expects you to pass the name of an actual property. In your case the class two properties: $name and $number. You can also check what properties an object has by using $list2 | Get-Member. So in order to sort, you can use:
$list2 = $list2 | Sort-Object -Property number
Now the reason Compare-Object is not working is because it's implemented differently that one might expect. See this answer explaining how it works.
One other thing you should keep in mind: Calling $list2.Add($entry) actually returns an integer (the index of the inserted element). PowerShell returns all uncaptured output. This can cause some unexpected behavior/output if you are not careful. So you should get in the habit of writing [void] $list2.Add($entry), unless you really want to return those indexes.

Powershell Multidimensional Arrays

I have a way of doing Arrays in other languagues like this:
$x = "David"
$arr = #()
$arr[$x]["TSHIRTS"]["SIZE"] = "M"
This generates an error.
You are trying to create an associative array (hash). Try out the following
sequence of commands
$arr=#{}
$arr["david"] = #{}
$arr["david"]["TSHIRTS"] = #{}
$arr["david"]["TSHIRTS"]["SIZE"] ="M"
$arr.david.tshirts.size
Note the difference between hashes and arrays
$a = #{} # hash
$a = #() # array
Arrays can only have non-negative integers as indexes
from powershell.com:
PowerShell supports two types of multi-dimensional arrays: jagged arrays and true multidimensional arrays.
Jagged arrays are normal PowerShell arrays that store arrays as elements. This is very cost-effective storage because dimensions can be of different size:
$array1 = 1,2,(1,2,3),3
$array1[0]
$array1[1]
$array1[2]
$array1[2][0]
$array1[2][1]
True multi-dimensional arrays always resemble a square matrix. To create such an array, you will need to access .NET. The next line creates a two-dimensional array with 10 and 20 elements resembling a 10x20 matrix:
$array2 = New-Object 'object[,]' 10,20
$array2[4,8] = 'Hello'
$array2[9,16] = 'Test'
$array2
for a 3-dimensioanl array 10*20*10
$array3 = New-Object 'object[,,]' 10,20,10
To extend on what manojlds said above is that you can nest Hashtables. It may not be a true multi-dimensional array but give you some ideas about how to structure the data. An example:
$hash = #{}
$computers | %{
$hash.Add(($_.Name),(#{
"Status" = ($_.Status)
"Date" = ($_.Date)
}))
}
What's cool about this is that you can reference things like:
($hash."Name1").Status
Also, it is far faster than arrays for finding stuff. I use this to compare data rather than use matching in Arrays.
$hash.ContainsKey("Name1")
Hope some of that helps!
-Adam
Knowing that PowerShell pipes objects between cmdlets, it is more common in PowerShell to use an array of PSCustomObjects:
$arr = #(
[PSCustomObject]#{Name = 'David'; Article = 'TShirt'; Size = 'M'}
[PSCustomObject]#{Name = 'Eduard'; Article = 'Trouwsers'; Size = 'S'}
)
Or for older PowerShell Versions (PSv2):
$arr = #(
New-Object PSObject -Property #{Name = 'David'; Article = 'TShirt'; Size = 'M'}
New-Object PSObject -Property #{Name = 'Eduard'; Article = 'Trouwsers'; Size = 'S'}
)
And grep your selection like:
$arr | Where {$_.Name -eq 'David' -and $_.Article -eq 'TShirt'} | Select Size
Or in newer PowerShell (Core) versions:
$arr | Where Name -eq 'David' | Where Article -eq 'TShirt' | Select Size
Or (just get the size):
$arr.Where{$_.Name -eq 'David' -and $_.Article -eq 'TShirt'}.Size
Addendum 2020-07-13
Syntax and readability
As mentioned in the comments, using an array of custom objects is straighter and saves typing, if you like to exhaust this further you might even use the ConvertForm-Csv (or the Import-Csv) cmdlet for building the array:
$arr = ConvertFrom-Csv #'
Name,Article,Size
David,TShirt,M
Eduard,Trouwsers,S
'#
Or more readable:
$arr = ConvertFrom-Csv #'
Name, Article, Size
David, TShirt, M
Eduard, Trouwsers, S
'#
Note: values that contain spaces or special characters need to be double quoted
Or use an external cmdlet like ConvertFrom-SourceTable which reads fixed width table formats:
$arr = ConvertFrom-SourceTable '
Name Article Size
David TShirt M
Eduard Trouwsers S
'
Indexing
The disadvantage of using an array of custom objects is that it is slower than a hash table which uses a binary search algorithm.
Note that the advantage of using an array of custom objects is that can easily search for anything else e.g. everybody that wears a TShirt with size M:
$arr | Where Article -eq 'TShirt' | Where Size -eq 'M' | Select Name
To build an binary search index from the array of objects:
$h = #{}
$arr | ForEach-Object {
If (!$h.ContainsKey($_.Name)) { $h[$_.Name] = #{} }
If (!$h[$_.Name].ContainsKey($_.Article)) { $h[$_.Name][$_.Article] = #{} }
$h[$_.Name][$_.Article] = $_ # Or: $h[$_.Name][$_.Article]['Size'] = $_.Size
}
$h.david.tshirt.size
M
Note: referencing a hash table key that doesn't exist in Set-StrictMode will cause an error:
Set-StrictMode -Version 2
$h.John.tshirt.size
PropertyNotFoundException: The property 'John' cannot be found on this object. Verify that the property exists.
Here is a simple multidimensional array of strings.
$psarray = #(
('Line' ,'One' ),
('Line' ,'Two')
)
foreach($item in $psarray)
{
$item[0]
$item[1]
}
Output:
Line
One
Line
Two
Two-dimensional arrays can be defined this way too as jagged array:
$array = New-Object system.Array[][] 5,5
This has the nice feature that
$array[0]
outputs a one-dimensional array, containing $array[0][0] to $array[0][4].
Depending on your situation you might prefer it over $array = New-Object 'object[,]' 5,5.
(I would have commented to CB above, but stackoverflow does not let me yet)
you could also uses System.Collections.ArrayList to make a and array of arrays or whatever you want.
Here is an example:
$resultsArray= New-Object System.Collections.ArrayList
[void] $resultsArray.Add(#(#('$hello'),2,0,0,0,0,0,0,1,1))
[void] $resultsArray.Add(#(#('$test', '$testagain'),3,0,0,1,0,0,0,1,2))
[void] $resultsArray.Add("ERROR")
[void] $resultsArray.Add(#(#('$var', '$result'),5,1,1,0,1,1,0,2,3))
[void] $resultsArray.Add(#(#('$num', '$number'),3,0,0,0,0,0,1,1,2))
One problem, if you would call it a problem, you cannot set a limit. Also, you need to use [void] or the script will get mad.
Using the .net syntax (like CB pointed above)
you also add coherence to your 'tabular' array...
if you define a array...
and you try to store diferent types
Powershell will 'alert' you:
$a = New-Object 'byte[,]' 4,4
$a[0,0] = 111; // OK
$a[0,1] = 1111; // Error
Of course Powershell will 'help' you
in the obvious conversions:
$a = New-Object 'string[,]' 2,2
$a[0,0] = "1111"; // OK
$a[0,1] = 111; // OK also
Another thread pointed here about how to add to a multidimensional array in Powershell. I don't know if there is some reason not to use this method, but it worked for my purposes.
$array = #()
$array += ,#( "1", "test1","a" )
$array += ,#( "2", "test2", "b" )
$array += ,#( "3", "test3", "c" )
Im found pretty cool solvation for making arrays in array.
$GroupArray = #()
foreach ( $Array in $ArrayList ){
$GroupArray += #($Array , $null)
}
$GroupArray = $GroupArray | Where-Object {$_ -ne $null}
Lent from above:
$arr = ConvertFrom-Csv #'
Name,Article,Size
David,TShirt,M
Eduard,Trouwsers,S
'#
Print the $arr:
$arr
Name Article Size
---- ------- ----
David TShirt M
Eduard Trouwsers S
Now select 'David'
$arr.Where({$_.Name -eq "david"})
Name Article Size
---- ------- ----
David TShirt M
Now if you want to know the Size of 'David'
$arr.Where({$_.Name -eq "david"}).size
M