I try do work with nested hashtables to get a specific value (number).
The hashtable looks like the following.
$Hashtable = #{
'Feature1' = #{
'Audit' = 1
'Block' = 2
'Change' = 3
}
'Feature2' = #{
'Audit' = 4
'Block' = 5
'Change' = 3
}
}
I want to access the hashtable with a lookup by key.
The first example works just fine.
$Hashtable['Feature1']['Audit', 'Block']
1
2
Specifying multiple keys works fine, too.
$Hashtable['Feature1', 'Feature2']
Name Value
---- ---- -
Block 2
Change 3
Audit 1
Block 5
Change 3
Audit 4
However, if I specify multiple keys in the first hashtable, it seems that I cannot access the nested hashtable.
$Hashtable['Feature1', 'Feature2']['Block', 'Audit']
Expected output would be 2, 1, 5 and 4, but instead I get $null.
I do have a workaround. If I loop through the first result, I get the expected result.
$Hashtable['Feature1', 'Feature2'] | % {$_['Block', 'Audit']}
2
1
5
4
I just wonder if there is 1) a nicer way to do that and 2) why I have to loop in that example.
I wished I could access the hashtable like $Hashtable[$Features][$Actions] where both variables are an array of keys.
$Hashtable['Feature1', 'Feature2'].Block
$Hashtable['Feature1', 'Feature2'] is an array, and the array.Property notation is a shorthand for selecting a single property for all elements of an array (requires PowerShell v3 or newer).
Edit: The above approach only works when you need a single key. If you need multiple, I can't think of any way to skip iterating over the inner hashtables. Selecting multiple keys at once (e.g. $Hashtable['Feature1', 'Feature2']) can only be done on hashtables, not arrays, and returns an array, so it can't be chained.
Related
This question already has answers here:
Powershell Join-Path showing 2 dirs in result instead of 1 - accidental script/function output
(1 answer)
Why does Range.BorderAround emit "True" to the console?
(1 answer)
Create a Single-Element Json Array Object Using PowerShell
(2 answers)
Closed 1 year ago.
I am new to PowerShell and there is a weird behavior I cannot explain. I call a function that returns a [System.Collections.ArrayList] but when I print my variable that receives the content of the array, if I have one value(for example: logXXX_20210222_075234355.txt), then I get 0 logXXX_20210222_075234355.txt. The value 0 gets added for some reason as if it has the index of the value.
If I have 4 values, it will look like this:
0 1 2 3 logXXX_20210222_075234315.txt logXXX_20210225_090407364.txt
logXXX_20210204_120318221.txt logXXX_20210129_122737751.txt
Can anyone help?
Here is a simple code that does that:
function returnAnArray{
$arrayToReturn =[System.Collections.ArrayList]::new()
$arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
$fileNames = returnAnArray
Write-Host $fileNames
0 logICM_20210222_075234315.txt
It's characteristic of the ArrayList class to output the index on .Add(...). However, PowerShell returns all output, which will cause it to intermingle the index numbers with the true or other intended output.
My favorite solution is to simply cast the the output from the .Add(...) method to [Void]:
function returnAnArray{
$arrayToReturn = [System.Collections.ArrayList]::new()
[Void]$arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
You can also use Out-Null for this purpose but in many cases it doesn't perform as well.
Another method is to assign it to $null like:
function returnAnArray{
$arrayToReturn = [System.Collections.ArrayList]::new()
$null = $arrayToReturn.Add('logICM_20210222_075234315.txt')
return $arrayToReturn
}
In some cases this can be marginally faster. However, I prefer the [Void] syntax and haven't observed whatever minor performance differential there may be.
Note: $null = ... works in all cases, while there are some cases where [Void] will not; See this answer (thanks again mklement0) for more information.
An aside, you can use casting to establish the list:
$arrayToReturn = [System.Collections.ArrayList]#()
Update Incorporating Important Comments from #mklement0:
return $arrayToReturn may not behave as intended. PowerShell's output behavior is to enumerate (stream) arrays down the pipeline. In such cases a 1 element array will end up returning a scalar. A multi-element array will return a typical object array [Object[]], not [Collection.ArrayList] as seems to be the intention.
The comma operator can be used to guarantee the return type by making the ArrayList the first element of another array. See this answer for more information.
Example without ,:
Function Return-ArrayList { [Collections.ArrayList]#(1,2,3,4,5,6) }
$ArrReturn = Return-ArrayList
$ArrReturn.gettype().FullName
Returns: System.Object[]
Example with ,:
Function Return-ArrayList { , [Collections.ArrayList]#(1,2,3,4,5,6) }
$ArrReturn = Return-ArrayList
$ArrReturn.gettype().FullName
Returns: System.Collections.ArrayList
Of course, this can also be handled by the calling code. Most commonly by wrapping the call in an array subexpression #(...). a call like: $filenames = #(returnAnArray) will force $filenames to be a typical object array ([Object[]]). Casting like $filenames = [Collections.ArrayList]#(returnArray) will make it an ArrayList.
For the latter approach, I always question if it's really needed. The typical use case for an ArrayList is to work around poor performance associated with using += to increment arrays. Often this can be accomplished by allowing PowerShell to return the array for you (see below). But, even if you're forced to use it inside the function, it doesn't mean you need it elsewhere in the code.
For Example:
$array = 1..10 | ForEach-Object{ $_ }
Is preferred over:
$array = [Collections.ArrayList]#()
1..10 | ForEach-Object{ [Void]$array.Add( $_ ) }
Persisting the ArrayList type beyond the function and through to the caller should be based on a persistent need. For example, if there's a need easily add/remove elements further along in the program.
Still More Information:
Notice the Return statement isn't needed either. This very much ties back to why you were getting extra output. Anything a function outputs is returned to the caller. Return isn't explicitly needed for this case. More commonly, Return can be used to exit a function at desired points...
A function like:
Function Demo-Return {
1
return
2
}
This will return 1 but not 2 because Return exited the function beforehand. However, if the function were:
Function Demo-Return
{
1
return 2
}
This returns 1, 2.
However, that's equivalent to Return 1,2 OR just 1,2 without Return
Update based on comments from #zett42:
You could avoid the ArrayList behavior altogether by using a different collection type. Most commonly a generic list, [Collections.Generic.List[object]]. Technically [ArrayList] is deprecated already making generic lists a better option. Furthermore, the .Add() method doesn't output anything, thus you do not need [Void] or any other nullification method. Generic lists are slightly faster than ArrayLists, and saving the nullification operation a further, albeit still small performance advantage.
ArrayList appears to store alternating indexes and values:
PS /home/alistair> $filenames[0]
0
PS /home/alistair> $filenames[1]
logICM_20210222_075234315.txt
I have an issue with .count. Its to do with the way it counts objects and also arrays containing objects.
For dull reasons, I have a situation where I don't know if what is being passed into my function is a single array of objects or 2 or more variables holding array lists of objects.
I though it would be easy to detect this. But because of the way count tries to help me out I am having trouble. When 2 or more arrays of objects are returned count reflects the number of arrays. But when only one is return count reflects the amount of objects in that array.
Simplified demo using simple arrays:
$Arr1 = "a","b","c"
$Arr2 ="d","f","e"
$arrayjoined = $Arr1,$Arr2
#count showing the number of arrays = 2
$arrayjoined.count
# Count on just one array = 3
$Arr1.count
My original function works apart from occasions when only one array is returned. Is there any thing I can do to check for this, or force the count to return 1 to reflect just one array returned?
PS. I know I can use += to get the results into the same array in the example above but I can't do this with the arrays coming into my function.
So what can we do if we can to count arrays in a array
$Array = 1,2,3
$CountArray = $Array
$CountArray.Length
$CountArray.Count
Will return
3
3
We can instead prepend the array with a ,
$Array = 1,2,3
$CountArray = ,$Array
$CountArray.Length
$CountArray.Count
Which will return
1
1
You can also add to an array like this
$Array = #()
$Array += ,#(1,2,3)
$Array.Length
$Array.Count
Returns
1
1
The only way I know to create an array in powershell is
$arr = #(1, 2, 3)
However, this creating method is not convenient if I want to create an array with large initial size such as 10000.
Because I don't want to write code like this
$arr = #(0, 0, 0, 0, 0, 0, ... ,0) # 10000 0s in this line of code
Writing code like the following is not efficient.
$arr = #()
for ($i = 1; $i -le 10000; $i++) {
$arr += 0
}
Because whenever += operator is executed, all of the elements in the old array would be copied into a newly created array.
What's the best way to create an array with large initial size in powershell?
Use New-Object in this case:
PS> $arr = New-Object int[] 10000; $arr.length
10000
Or, in PSv5+, using the static new() method on the type:
PS> $arr = [int[]]::new(10000); $arr.length
10000
These commands create a strongly typed array, using base type [int] in this example.
If the use case allows it, this is preferable for reasons of performance and type safety.
If you need to create an "untyped" array the same way that PowerShell does ([System.Object[]]), substitute object for int; e.g., [object[]]::new(10000); the elements of such an array will default to $null.
TessellatingHeckler's helpful answer, however, shows a much more concise alternative that even allows you initialize all elements to a specific value.
Arrays have a fixed size; if you need an array-like data structure that you can preallocate and grow dynamically, see Bluecakes' helpful [System.Collections.ArrayList]-based answer.
[System.Collections.ArrayList] is the resizable analog to [System.Object[]], and its generic equivalent - which like the [int[]] example above - allows you to use a specific type for performance and robustness (if feasible), is [System.Collections.Generic.List[<type>]], e.g.:
PS> $lst = [System.Collections.Generic.List[int]]::New(10000); $lst.Capacity
10000
Note that - as with [System.Collections.ArrayList] - specifying an initial capacity (10000, here) does not allocate the internally used array with that size right away - the capacity value is simply stored (and exposed as property .Capacity), and an internal array with that capacity (internal size that leaves room for growth) is allocated on demand, when the first element is added to the list.
[System.Collections.Generic.List[<type>]]'s .Add() method commendably does not produce output, whereas [System.Collections.ArrayList]'s does (it returns the index of the element that was just added).
# The non-generic ArrayList's .Add() produces (usually undesired) output.
PS> $al = [System.Collections.ArrayList]::new(); $al.Add('first elem')
0 # .Add() outputs the index of the newly added item
# Simplest way to suppress this output:
PS> $null = $al.Add('first elem')
# NO output.
# The generic List[T]'s .Add() does NOT produce output.
PS> $gl = [System.Collections.Generic.List[string]]::new(); $gl.Add('first elem')
# NO output from .Add()
Array literal multiplication:
$arr = #($null) * 10000
In PowerShell you are correct in that += will destroy the old array and create a new array with the new items.
For working with a large collection of items i would highly recommend using the ArrayList type from .NET as this is not a fixed size array so PowerShell will not destroy it every time you add an item to it and i've found this to work better in my projects.
Using ArrayList also means that you don't need to start with 10000 items. Because PowerShell won't need to recreate your array every time, you can start with 0 and then add each item as it's needed instead of starting with 10000.
So in your script i would create an empty ArrayList like so
[System.Collections.ArrayList]$arr = #()
and then when you need to add something to it just call .Add() (You don't need to prepopulate the array with 10000 items, it will expand as you add items).
$arr.Add([int]0)
Your example using an ArrayList:
[System.Collections.ArrayList]$arr = #()
for ($i = 1; $i -le 10000; $i++) {
$arr.Add([int]0)
}
You could always do this:
$a = #(0..9999)
$a.length
This is nearly like what you are already doing, except that you don't have to
write out all the values.
I wrote a generic function to call a stored procedure. I tried to use a multidimensional array to pass the parameters. Now it is possible, that the procedure only takes one parameter, so my multidimensional array has also only one parameter. But the lenght of such an array is 2!
$MyParameters = ("param1_name", "param1_value")
$MyParameters.Length returns 2!! Strange, why? It should return 1
$MyParameters returns correctly:
param1_name
param1_value
If I write:
$MyParameters = ("param1_name", "param1_value"), ("param2_name", "param2_value")
$MyParameters.Length returns also 2 which is correct. $MyParameters returns correctly all four elements:
param1_name
param1_value
param2_name
param2_value
Any reasons for that? Am I missing something?
What you are trying to do is creating an array of multi value object.
Here is an example to solve that issue:
$x = ,("param1","param2","param3")
x.Lenght
Will return 1 which is correct for your issue.
$x = ,("param1","param2","param3"),("param1","param2","param3")
x.Lenght
Will return 2
$x = ,("param1","param2","param3"),("param1","param2","param3")
x[0].Lenght
will return 1, thats because $x[0] is array with one element.
In addition, If you would like to create an Array of Arrays this is the way to do it:
$x = #("param1","param2","param3"),#("param1","param2","param3")
$x.Lenght
#2
$x[0].Lenght
#3
I've got some problems with two-dimensional arrays in PowerShell.
Here's what I want to do:
I create a function that is supposed to return a two-dimensional array. When invoking the function I want the return value to be a new two-dimensional array.
For a better understanding I've added an example function, below:
function fillArray() {
$array = New-Object 'object[,]' 2,3
$array[0,0] = 1
$array[0,1] = 2
$array[0,2] = 3
$array[1,0] = 4
$array[1,1] = 5
$array[1,2] = 6
return $array
}
$erg_array = New-Object 'object[,]' 2,3
$erg_array = fillArray
$erg_array[0,1] # result is 1 2
$erg_array[0,2] # result is 1 3
$erg_array[1,0] # result is 2 1
The results are not what I expect. I want to return the information in the same way as declared in the function. So I would expect $erg_array[0,1] to give me 2 instead of the 1,2 I receive with the code above. How can I achieve this?
In order to return the array exactly as it is without "unrolling" use the comma operator (see help about_operators)
function fillArray() {
$array = New-Object 'object[,]' 2, 3
$array[0,0] = 1
$array[0,1] = 2
$array[0,2] = 3
$array[1,0] = 4
$array[1,1] = 5
$array[1,2] = 6
, $array # 'return' is not a mistake but it is not needed
}
# get the array (we do not have to use New-Object now)
$erg_array = fillArray
$erg_array[0,1] # result is 2, correct
$erg_array[0,2] # result is 3, correct
$erg_array[1,0] # result is 4, correct
The , creates an array with a single item (which is our array). This 1-item array gets unrolled on return, but only one level, so that the result is exactly one object, our array. Without , our array itself is unrolled, its items are returned, not the array. This technique with using comma on return should be used with some other collections as well (if we want to return a collection instance, not its items).
What is really missing in this port is what everyone is looking for. How to get more than one thing out of a function. Well I am going to share what everyone wants to know who has searched and found this hoping it will answer the question.
function My-Function([string]$IfYouWant)
{
[hashtable]$Return = #{}
$Return.Success = $False
$Return.date = get-date
$Return.Computer = Get-Host
Return $Return
}
#End Function
$GetItOut = My-Function
Write-host “The Process was $($GetItOut.Success) on the date $($GetItOut.date) on the host $($GetItOut.Computer)”
#You could then do
$var1 = $GetItOut.Success
$Var2 =$GetItOut.date
$Var3 = $GetItOut.Computer
If ($var1 –like “True”){write-host “Its True, Its True”}