How do I create an empty array of arrays in Powershell? - arrays

I want to create an empty array of arrays in Powershell to hold "tuples" of values (arrays are immutable).
Therefore I try something like:
The type of $arr is Object[]. I've read that += #(1,2) appends the given element (i.e. #(1,2)) to $arr (actually creates a new array). However, in this case it seems that the arrays are concatenated, why?
$arr = #()
$arr += #(1,2)
$arr.Length // 2 (not 1)
If I do as follows, it seems that $arr contains the two arrays #(1,2),#(3,4), which is what I want:
$arr = #()
$arr += #(1,2),#(3,4)
$arr.Length // 2
How do I initialize an empty array of arrays, such that I can add one subarray at a time, like $arr += #(1,2)?

The answer from Bruce Payette will work. The syntax seems a bit awkward to me, but it does work. At least it is not Perl.
Another way to do this would be with an ArrayList. To me, this syntax is more clear and more likely to be understood by another developer (or myself) in six months.
[System.Collections.ArrayList]$al = #()
$al.Add(#(1,2))
$al.Add(#(3,4))
foreach ($e in $al) {
$e
$e.GetType()
}

The + operator concatenates arrays. To add an array as a single element, prefix the element to add with a comma. Something like #() + , (1,2) + , (3, 4).

As far as I can tell, you can't do it natively in PowerShell, nor can you do it with the [System.Array] type. In both cases, you seem to need to define both the length and the type of the array. Because I wanted a completely empty array of arrays, with the ability to store any type of value in the array, I did it this way.
$x=[System.Collections.ArrayList]::new()
$x.Count
$x.GetType()
$x.Add([System.Collections.ArrayList]::new()) > $null
$x.Count
$x[0].Count
$x[0].GetType()
$x[0].Add("first element") > $null
$x.Count
$x[0].Count
$x[0][0]

Related

how can i make foreach output two different files?

I have this variable
$sync_output = Join-Path $syncPATH $CUBE_input
I have this foreach loop:
$i=0
$Destination_Server = #()
foreach($row in $Table | where { $_.cube_name -match $CUBE })
{
$i++
$Destination_Server += $row.Item("query_server")
write-host " > Query Server $($i):" $Destination_Server[$Destination_Server.length -1]
$sync_output += "_$Destination_Server.txt"
Invoke-ASCmd –InputFile $XML_file -Server $Destination_Server[$Destination_Server.length -1] >$sync_output
}
it does what it needs to do, except for the sync_output, I am getting this created:
as you can see, the first file,
CUBE_Destination_Server1
is perfectly created.
however, the second file, should be named
CUBE_Destination_Server2
but instead its for some reason appending the Destination_Server2 after appending Destination_Server1 twice...
Why is that?
The += operator appends. So this line:
$sync_output += "_$Destination_Server.txt"
takes the existing value of $sync_output and then adds "_$Destination_Server.txt" onto the end.
One way to get your desired result is to assign (not append) $syncResult inside your loop, with a statement like this:
$sync_output = (Join-Path $syncPATH $CUBE_input) + "_$Destination_Server.txt"
Update: it looks like you have a similar problem with the $Destination_Server variable--you should use plain assignment (=) with it too, rather than appending (also called concatenation) (+=).
The string is appending, because that is what you told it to do.
Straight up, the += says to add the new content at the end, so in the case of
$sync_output = Join-Path $syncPATH $CUBE_input
...
$sync_output += "_$Destination_Server.txt"
It sets a string, and then appends things to the end of that string, and keeps appending to it each time that command is executed when the ForEach-Object loop cycles. This is compounded by your other use of +=, which is appending objects into an array, since you setup $Destination_Server = #() which sets that variable as an empty array. So when you append to your string you are appending an entire array to that string.
So to replicate your example:
$sync_output = 'CUBE'
$destination_server = #()
Then inside the loop on the first pass you add a string to the array:
$destination_server += 'Destination_Server1'
So that array has 1 item in it. You then add that array to $sync_output which expanding the variables basically reads like:
"CUBE" += "_Destination_Server1.txt"
So now $sync_output has a value of CUBE_Destination_Server1.txt. Next iteration of the loop! You add another string to the $Destination_Server array:
$destination_server += 'Destination_Server2'
Now that array has 2 strings in it, so when you append it to the $sync_output string variable you essentially are doing this:
"CUBE_Destination_Server1.txt" += "_Destination_Server1Destination_Server2.txt"
It does that because the array of strings simply concocts all of the strings in its array into one string. In order to really fix this you need to be consistent and reference the last string in the array. I recommend not re-using the same variable for this, and will use $sync_output_file.
$sync_output = Join-Path $syncPATH $CUBE_input
$i=0
$Destination_Server = #()
foreach($row in $Table | where { $_.cube_name -match $CUBE })
{
$i++
$Destination_Server += $row.Item("query_server")
write-host " > Query Server $($i):" $Destination_Server[$Destination_Server.length -1]
$sync_output_file = $sync_output + '_' + $Destination_Server[-1] + '.txt'
Invoke-ASCmd –InputFile $XML_file -Server $Destination_Server[$Destination_Server.length -1] >$sync_output_file
}
Edit: Here's why $Destination_Server[$Destination_Server.length - 1] works like $Destination_Server[-1]:
In PowerShell an each item in an array has an index. That index is zero based. You can reference an item in the array by its index number as such $Array[X] where X is the index number of the item you are interested in. So given this array:
$MyArray = 'cat','dog','fish','goat','banana'
If you reference $MyArray[0] it will return cat, since that is the first item in the array. With arrays in PowerShell the .length and .count properties are synonymous, so when you reference $MyArray.length you are simply getting the count of items in the array. When you count items you start at 1, but array indexes start at 0, which is why you have to do .length - 1 to get the index number of the last item in the array. In my example if we do $MyArray.Length it would return 5, because my array has 5 items in it. So $MyArray[$MyArray.Length - 1] is essentially $MyArray[5 - 1], or $MyArray[4], which is the last item in $MyArray.
In addition to referencing items by index you can also use negative numbers, which will start at the end of the array, and count backwards. This method is not zero based, so $MyArray[-1] references the last item in the array, just like $MyArray[-2] is the second to the last item in the array.
So the difference between $MyArray[$MyArray.length - 1] and $MyArray[-1] is that in the first you calculate the index number for the last item in the array, where the second references the last item in the array regardless of what its index number is.
You can also specify ranges this way, so $MyArray[0..2] would get you the first 3 items in the array, and $MyArray[-2..-1] would get you the last 2 items in the array. That doesn't really apply to your situation right now, but it's handy to know in general and might be helpful in the future for you.

Split an array of strings in to chunks of a maximum size

I'm attempting to ensure an array of unknown length is split in one-or-more arrays with a maximum of 20 elements.
I've searched around and found several answers for similar questions based on arrays containing number sequences (or maybe I'm just missing something), but nothing for an array of strings.
I've come up with a solution, but it seems a little convoluted and I feel it could probably be better.
For example, take this array -
$metricDefinitionsHash= [ordered]#{"this one" = "111"; "that one" = "222"; "another one" = "333"}
And for the sake of this example, that's say I want only a maximum of two elements -
$metricsQueryParts = #()
$counter = 0
$metricDefinitionsHash.Keys | ForEach-Object {
$index = [System.Math]::Floor($counter / 2)
if ($metricsQueryParts.Length -eq $index)
{
$metricsQueryParts += ""
$metricsQueryParts[$index] = #()
}
$metricsQueryParts[$index] += $_
$counter++
}
So now I have an array of arrays -
#(#("this one", "that one"), #("another one"))
But... Can I achieve my goal in a more efficient way?
Use a for loop, the range operator (..), and the unary array construction operator (,):
[array]$keys = $metricDefinitionsHash.Keys
$size = 3
$metricsQueryParts = #()
for ($i=0; $i -lt $keys.Count; $i+=$size) {
$metricsQueryParts += ,$keys[$i..($i+$size-1)]
}
[array]$keys casts the key/value collection .Keys to a generic array (so that the index operator can be used on it).
The expression $i..($i+$size-1) gives you the indexes from $i up to the index before $i+$size, i.e. the next $size elements from the array starting at position $i. If there are less than $size elements the expression will give just the remaining number of elements.
The unary array construction operator ensures that $keys[...] is appended to $metricsQueryParts as a nested array instead of just concatenating the two arrays.
I really like Ansgar's solution but the keys collection isn't an array and as such you can't use the array range syntax. Keys is an OrderedDictionaryKeyValueCollection. I have used out-string -stream to force the OrderedDictionaryKeyValueCollection to an array.
$keys = $metricDefinitionsHash.Keys | out-string -stream
$size = 2
$metricsQueryParts = #()
for ($i=0; $i -lt $keys.Count; $i+=$size) {
$metricsQueryParts += ,$keys[$i..($i+$size-1)]
}

Appending a string to each item of an array

I have an array and when I try to append a string to it the array converts to a single string.
I have the following data in an array:
$Str
451 CAR,-3 ,7 ,10 ,0 ,3 , 20 ,Over: 41
452 DEN «,40.5,0,7,0,14, 21 ,  Cover: 4
And I want to append the week of the game in this instance like this:
$Str = "Week"+$Week+$Str
I get a single string:
Week16101,NYG,42.5 ,3 ,10 ,3 ,3 , 19 ,Over 43 102,PHI,- 1,14,7,0,3, 24 ,  Cover 4 103,
Of course I'd like the append to occur on each row.
Instead of a for loop you could also use the Foreach-Object cmdlet (if you prefer using the pipeline):
$str = "apple","lemon","toast"
$str = $str | ForEach-Object {"Week$_"}
Output:
Weekapple
Weeklemon
Weektoast
Another option for PowerShell v4+
$str = $str.ForEach({ "Week" + $Week + $_ })
Something like this will work for prepending/appending text to each line in an array.
Set array $str:
$str = "apple","lemon","toast"
$str
apple
lemon
toast
Prepend text now:
for ($i=0; $i -lt $Str.Count; $i++) {
$str[$i] = "yogurt" + $str[$i]
}
$str
yogurtapple
yogurtlemon
yogurttoast
This works for prepending/appending static text to each line. If you need to insert a changing variable this may require some modification. I would need to see more code in order to recommend something.
Another solution, which is fast and concise, albeit a bit obscure.
It uses the regex-based -replace operator with regex '^' which matches the position at the start of each input string and therefore effectively prepends the replacement string to each array element (analogously, you could use '$' to append):
# Sample array.
$array = 'one', 'two', 'three'
# Prepend 'Week ' to each element and create a new array.
$newArray = $array -replace '^', 'Week '
$newArray then contains 'Week one', 'Week two', 'Week three'
To show an equivalent foreach solution, which is syntactically simpler than a for solution (but, like the -replace solution above, invariably creates a new array):
[array] $newArray = foreach ($element in $array) { 'Week ' + $element }
Note: The [array] cast is needed to ensure that the result is always an array; without it, if the input array happens to contain just one element, PowerShell would assign the modified copy of that element as-is to $newArray; that is, no array would be created.
As for what you tried:
"Week"+$Week+$Str
Because the LHS of the + operation is a single string, simple string concatenation takes place, which means that the array in $str is stringified, which by default concatenates the (stringified) elements with a space character.
A simplified example:
PS> 'foo: ' + ('bar', 'baz')
foo: bar baz
Solution options:
For per-element operations on an array, you need one of the following:
A loop statement, such as foreach or for.
Michael Timmerman's answer shows a for solution, which - while syntactically more cumbersome than a foreach solution - has the advantage of updating the array in place.
A pipeline that performs per-element processing via the ForEach-Object cmdlet, as shown in Martin Brandl's answer.
An expression that uses the .ForEach() array method, as shown in Patrick Meinecke's answer.
An expression that uses an operator that accepts arrays as its LHS operand and then operates on each element, such as the -replace solution shown above.
Tradeoffs:
Speed:
An operator-based solution is fastest, followed by for / foreach, .ForEach(), and, the slowest option, ForEach-Object.
Memory use:
Only the for option with indexed access to the array elements allows in-place updating of the input array; all other methods create a new array.[1]
[1] Strictly speaking, what .ForEach() returns isn't a .NET array, but a collection of type [System.Collections.ObjectModel.Collection[psobject]], but the difference usually doesn't matter in PowerShell.

Powershell array is not cleared

My source code:
# $arr = #(); results in same behaviour
$arr = New-Object System.Collections.ArrayList;
$arr.Count;
$arr += "z";
$arr.Count;
$arr.Clear();
$arr.Count;
Output:
0
1
1
Powershell does some array-casting trickery when you do +=, so the easy solution is to do $arr.Add("z"). Then $arr.Clear() will act like you expect.
To clarify:
#() is a Powershell array. It uses +=, but you can't Clear it. (You can, however, do $arr = #() again to reset it to an empty array.)
ArrayList is the .NET collection. It uses .Add, and you can Clear it, but for some reason if you += it, Powershell does some weird array coercion. (If any experts care to comment on this, awesome.)

create a new variable for each loop in a foreach-loop

How can I put $org into an array together with $count?
Like this example array:
$myArray = #{
1="SampleOrg";
2="AnotherSampleOrg"
}
Another example:
$myArray = #{
$count="$org";
$count="$org"
}
Example foreach:
$count=0;get-organization | foreach {$count++; $org = $_.Name.ToString();write-host $count -nonewline;write-host " $org"}
$answer = read-host "Select 1-$count"
The above will display:
1 SampleOrg
2 AnotherSampleOrg
Select 1-2:
What I would like to do afterwards is to put the array to use in a switch.
Example:
switch ($answer)
{
1 {$org=myArray[1]} #<-- or whatever that corresponds to "SampleOrg"
2 {$org=myArray[2]} #<-- or whatever that corresponds to "AnotherSampleOrg"
}
You have to initialize your hashtable somewhere before the loop:
$myArray = #{}
and add a
$myArray.Add($count, $org)
to your foreach-loop.
EDIT: For the discussion about hastable/array see the whole thread ;) I just kept the name of the variable from the original posting
Looks like you're confusing arrays and Hashtables. Arrays are ordered, and indexed by an numeric value. Hashtables are associative, and indexed by any value that has equality defined.
This is array syntax
$arr = #(1,2,3)
and this is Hashtable syntax
$ht = #{red=1;blue=2;}
For your question, the following will work
$orgs = #(get-organization | % { $_.Name })
this will create a 0 based array, mapping int -> OrgName, so
$orgs[$answer]
will get the correct name. Or if you're using 1 based indexing
$orgs[$answer-1]
Note, I removed the switch, as there's no reason for it.

Resources