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
When working with an array of values, indexof can be used to find the position of the value in the array.
#this returns '1', correctly identifying 'blue' in position '1' of the array
$valueArray = #('cup','blue','orange','bicycle')
[array]::indexof($valueArray,'blue')
I would like to use this command to find the position of a file (image) in an array of objects generated with Get-ChildItem, however the returned position is always '-1' no matter where the object I have called for actually is. Note that image123.jpg is in the middle of the array.
$imageArray = Get-ChildItem "C:\Images"
[array]::indexof($imageArray,'image123.jpg')
I have noticed that if I change the array to filenames only, it works returning the actual position of the filename.
$imageArray = Get-ChildItem "C:\Images" | select -expand Name
[array]::indexof($imagesToReview,'image123.jpg')
Is this just the nature of using indexof or is there a way to find the correct position of the image file in the array without converting?
The easiest solution here is the following:
$imageArray = Get-ChildItem "C:\Images"
[array]::indexof($imageArray.Name,'image123.jpg')
Explanation:
[array]::IndexOf(array array,System.Object value) searches an array object for an object value. If no match is found, it returns the array lower bound minus 1. Since the array's first index is 0, then it returns the result of 0-1.
Get-ChildItem -Path SomePath returns an array of DirectoryInfo and FileInfo objects. Each of those objects has various properties and values. Just using $imageArray to compare to image123.jpg would be comparing a System.IO.FileInfo object to a String object. PowerShell won't automatically convert a FileInfo object into a string while correctly parsing to find your target value.
When you choose to select a property value of each object in the array, you are returning an array of those property values only. Using $imageArray | Select -Expand Name and $imageArray.Name return an array of Name property values. Name contains a string in your example. This means you are comparing a String to a String when using [array]::IndexOf($imageArray.Name,'image123.jpg').
The way that .NET by default compares things is just not as forgiving as PowerShell is!
[array]::IndexOf($array, $reference) will go through the array and return the current index when it encounters an item for which the following is true:
$item.Equals($reference)
... which is NOT necessarily the same as doing
$item -eq $reference
For simple values, like numbers and dates and so on, Equals() works exactly like -eq:
PS C:\> $a = 1
PS C:\> $b = 1
PS C:\> $a.Equals($b) # $true
... which is the reason your first example works as expected!
For more complex objects though, Equals() works a bit differently. Both values MUST refer to the same object, it's not enough that they have similar or even identical values:
PS C:\> $a = New-Object object
PS C:\> $b = New-Object object
PS C:\> $a.Equals($b) # $false
In the example above, $a and $b are similar (if not identical) - they're both empty objects - but they are not the same object.
Similarly, if we test with your input values, they aren't the same either:
PS C:\> $a = Get-Item "C:\"
PS C:\> $b = "C:\"
PS C:\> $a.Equals($b) # $false
One of the reasons they can't be considered the same, as AdminOfThings excellently explains, is type mismatch - but PowerShell's comparison operators can help us here!
You'll notice that this works:
PS C:\> $a = Get-Item "C:\"
PS C:\> $b = "C:\"
PS C:\> $b -eq $a
True
That's because the behavior of -eq depends on the left-hand operand. In the example above, "C:\" is a string, so PowerShell converts $a to a string, and all of a sudden the comparison is more like "C:\".Equals("C:\")!
With this in mind, you could create your own Find-IndexOf function to do $reference -eq $item (or any other comparison mechanism you'd like) with a simple for() loop:
function Find-IndexOf
{
param(
[array]$Array,
[object]$Value
)
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx]){
return $idx
}
}
return -1
}
Now you'd be able to do:
PS C:\> $array = #('','PowerShell is case-insensitive by default')
PS C:\> $value = 'POWERsheLL iS cASe-InSenSItIVe BY deFAuLt'
PS C:\> Find-IndexOf -Array $array -Value $value
1
Or:
PS C:\> $array = Get-ChildItem C:\images
PS C:\> $value = 'C:\images\image123.png'
PS C:\> Find-IndexOf -Array $array -Value $value
5
Adding comparison against a specific property on each of the array items (like the file's Name in your example), we end up with something like this:
function Find-IndexOf
{
param(
[array]$Array,
[object]$Value,
[string]$Property
)
if($Property){
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx].$Property){
return $idx
}
}
}
else {
for($idx = 0; $idx -lt $Array.Length; $idx++){
if($Value -eq $Array[$idx]){
return $idx
}
}
}
return -1
}
Find-IndexOf -Array #(Get-ChildItem C:\images) -Value image123.png -Property Name
I am trying to use $a variable in this script for working with intermediate steps so that I don't have to use $array[$array.Count-1] repeatedly. Similarly for $prop as well . However, values are being overwritten by last value in loop.
$guests = Import-Csv -Path C:\Users\shant_000\Desktop\UploadGuest_test.csv
$output = gc '.\Sample Json.json' | ConvertFrom-Json
$array = New-Object System.Collections.ArrayList;
foreach ($g in $guests) {
$array.Add($output);
$a = $array[$array.Count-1];
$a.Username = $g.'EmailAddress';
$a.DisplayName = $g.'FirstName' + ' ' + $g.'LastName';
$a.Password = $g.'LastName' + '123';
$a.Email = $g.'EmailAddress';
foreach ($i in $a.ProfileProperties.Count) {
$j = $i - 1;
$prop = $a.ProfileProperties[$j];
if ($prop.PropertyName -eq "FirstName") {
$prop.PropertyValue = $g.'FirstName';
} elseif ($prop.PropertyName -eq "LastName") {
$prop.PropertyValue = $g.'LastName';
}
$a.ProfileProperties[$j] = $prop;
}
$array[$array.Count-1] = $a;
}
$array;
All array elements are referencing one actual variable: $output.
Create an entirely new object each time by repeating JSON-parsing:
$jsontext = gc '.\Sample Json.json'
..........
foreach ($g in $guests) {
$a = $jsontext | ConvertFrom-Json
# process $a
# ............
$array.Add($a) >$null
}
In case the JSON file is very big and you change only a few parts of it you can use a faster cloning technique on the changed parts (and their entire parent chain) via .PSObject.Copy():
foreach ($g in $guests) {
$a = $output.PSObject.Copy()
# ............
$a.ProfileProperties = $a.ProfileProperties.PSObject.Copy()
# ............
foreach ($i in $a.ProfileProperties.Count) {
# ............
$prop = $a.ProfileProperties[$j].PSObject.Copy();
# ............
}
$array.Add($a) >$null
}
As others have pointed out, appending $object appends a references to the same single object, so you keep changing the values for all elements in the list. Unfortunately the approach #wOxxOm suggested (which I thought would work at first too) doesn't work if your JSON datastructure has nested objects, because Copy() only clones the topmost object while the nested objects remain references to their original.
Demonstration:
PS C:\> $o = '{"foo":{"bar":42},"baz":23}' | ConvertFrom-Json
PS C:\> $o | Format-Custom *
class PSCustomObject
{
foo =
class PSCustomObject
{
bar = 42
}
baz = 23
}
PS C:\> $o1 = $o
PS C:\> $o2 = $o.PSObject.Copy()
If you change the nested property bar on both $o1 and $o2 it has on both objects the value that was last set to any of them:
PS C:\> $o1.foo.bar = 23
PS C:\> $o2.foo.bar = 24
PS C:\> $o1.foo.bar
24
PS C:\> $o2.foo.bar
24
Only if you change a property of the topmost object you'll get a difference between $o1 and $o2:
PS C:\> $o1.baz = 5
PS C:\> $o.baz
5
PS C:\> $o1.baz
5
PS C:\> $o2.baz
23
While you could do a deep copy it's not as simple and straightforward as one would like to think. Usually it takes less effort (and simpler code) to just create the object multiple times as #PetSerAl suggested in the comments to your question.
I'd also recommend to avoid appending to an array (or arraylist) in a loop. You can simply echo your objects inside the loop and collect the entire output as a list/array by assigning the loop to a variable:
$json = Get-Content '.\Sample Json.json' -Raw
$array = foreach ($g in $guests) {
$a = $json | ConvertFrom-Json # create new object
$a.Username = $g.'EmailAddress'
...
$a # echo object, so it can be collected in $array
}
Use Get-Content -Raw on PowerShell v3 and newer (or Get-Content | Out-String on earlier versions) to avoid issues with multiline JSON data in the JSON file.
So I am curious how to call a function from an array?
Function example:
function test($a, $b)
{
write-host $a
write-host $b
}
An array
$testArray = #{"test";"testA";"testB"}
And the whole bit I want to work is
$testArray[0] $testArray[1] $testArray[2]
Essentially mimic this
test "testA" "testB"
Reason for this is I have a few arrays like this and a loop that would go through each one using the custom function call on the data in each array.
Basically trying a different programing style.
Ok, it sounds like you have an array of arrays to be honest, so we'll go with that. Then let's reference this SO question which very closely resembles your question, and from that take away the whole [scriptblock]::create() thing, and splatting arrays. From that we can come up with this script:
function test($a, $b)
{
Write-Host "Function 'test'"
write-host $a
write-host $b
}
function test2($a, $b)
{
Write-Host "Function 'test2'"
write-host $b
write-host $a
}
$TestArray = #() #the correct way to create an array, instead of a broken HashTable
$testArray = #("test","testA","testB"),#("test2","testC","testD")
ForEach($Test in $TestArray){
$script = [scriptblock]::Create($test[0]+" #Args")
$script.Invoke($test[1..$test.count])
}
If all you have is one array, and not an array of arrays then I guess this is pretty simple. You could do:
$testArray = #("test","testA","testB")
$script = [scriptblock]::Create($testArray[0]+" #Args")
$script.Invoke($testArray[1..$testArray.count])
Edit (Capturing): Ok, to capture the results of a function you should be able to prefix it with $Variable = and be good to go, such as:
$MyResults = $script.Invoke($testArray[1..$testArray.count])
That will capture any output given by the function. Now, since the functions we have been working with only perform Write-Host they don't actually output anything at all, they just print text to the screen. For this I would modify the function a bit to get real output that's usable. In this example the function takes 2 parameters as input, and creates a new object with those 2 parameters assigned to it as properties. That object is the output.
function test($a, $b)
{
New-Object PSObject -Property #{Value1=$a;Value2=$b}
}
$testArray = #("test","testA","testB")
$script = [scriptblock]::Create($testArray[0]+" #Args")
$MyResults = $script.Invoke($testArray[1..$testArray.count])
Now if you ran that you would end up with the variable $MyResults being a PSCustomObject that has 2 properties named Value1 and Value2. Value1 would contain the string "testA" and Value2 would contain the string "testB". Any pair of strings passed to that function would output 1 object with 2 properties, Value1 and Value2. So after that you could call $MyResults.Value1 and it would return testA.
After reading this helpful article on the Windows PowerShell Blog, I realized I could "shift" off the first part of an array, but didn't know how to "unshift" or push elements onto the front of an array in PowerShell.
I am creating an array of hash objects, with the last read item pushed onto the array first. I'm wondering if there is a better way to accomplish this.
## Create a list of files for this collection, pushing item on top of all other items
if ($csvfiles[$collname]) {
$csvfiles[$collname] = #{ repdate = $date; fileobj = $csv }, $csvfiles[$collname] | %{$_}
}
else {
$csvfiles[$collname] = #{ repdate = $date; fileobj = $csv }
}
A couple of things to note:
I need to unroll the previous array element with a foreach loop %{$_}, because merely referencing the array would create a nested array structure. I have to have all the elements at the same level.
I need to differentiate between an empty array and one that contains elements. If I attempt to unroll an empty array, it places a $null element at the end of the array.
Thoughts?
PS: The reason the empty hash element produces a NULL value is that $null is treated as a scalar in PowerShell. For details, see https://connect.microsoft.com/PowerShell/feedback/details/281908/foreach-should-not-execute-the-loop-body-for-a-scalar-value-of-null.
ANSWER:
Looks like the best solution is to pre-create the empty array when necessary, rather than code around the $null issue. Here's the rewrite using a .NET ArrayList and a native PoSh array.
if (!$csvfiles.ContainsKey($collname)) {
$csvfiles[$collname] = [System.Collections.ArrayList]#()
}
$csvfiles[$collname].insert(0, #{ repdate = $repdate; fileobj = $csv })
## NATIVE POSH SOLUTION...
if (!$csvfiles.ContainsKey($collname)) {
$csvfiles[$collname] = #()
}
$csvfiles[$collname] = #{ repdate = $repdate; fileobj = $csv }, `
$csvfiles[$collname] | %{$_}
You might want to use ArrayList objects instead, as they support insertions at arbitrary locations. For example:
# C:\> $a = [System.Collections.ArrayList]#(1,2,3,4)
# C:\> $a.insert(0,6)
# C:\> $a
6
1
2
3
4
You can simply use a plus operator:
$array = #('bar', 'baz')
$array = #('foo') + $array
Note: this re-creates actually creates a new array instead of changing the existing one (but the $head, $tail = $array way of shifting you refer to works extactly in the same way).