If I have an array of objects like
$Results = #(
[PSCustomObject]#{
Email = $_.email
Type = $Type
}
# ...
)
and I want to update the 100th $results.type value from X to y. I thought this code would be
$results.type[100] = 'Hello'
But this is not working and I am not sure why?
$Results.Type[100] = 'Hello' doesn't work because $Results.Type isn't real!
$Results is an array. It contains a number of objects, each of which have a Type property. The array itself also has a number of properties, like its Length.
When PowerShell sees the . member invocation operator in the expression $Results.Type, it first attempts to resolve any properties of the array itself, but since the [array] type doesn't have a Type property, it instead tries to enumerate the items contained in the array, and invoke the Type member on each of them. This feature is known as member enumeration, and produces a new one-off array containing the values enumerated.
Change the expression to:
$Results[100].Type = 'Hello'
Here, instead, we reference the 101st item contained in the $Results array (which is real, it already exists), and overwrite the value of the Type property of that object.
#MathiasR.Jessen answered this. The way I was indexing the array was wrong.
the correct way is $results[100].type = 'Hello'
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 below .csv input file. These are the Distribution Groups.
Level-4 is member of Level3,
Level-3 is member of Level2,
Level-2 is member of Level1,
Level-1 is member of Level0.
So far, I have tried the below code. Starting to add Level-4 into Level-3. I have marked them for better understanding. However, I am not able to select the object and iterate in correct way using PowerShell.
e.g. First instance of Level-3 DL is 'DL_L3_US1' and it will contain members from Level-4 i.e. DL_L4_US1 and DL_L4_US2. How do I make this work?
$DLlist = Import-Csv C:\tempfile\Book2.csv
$Test = $DLlist | select Level3,Level4 | ? {$_.level3 -notlike $null }
foreach ($x in $DLlist)
{Add-DistributionGroupMember -Identity "$($x.Level3)" -Member "$($x.Level4)"}
So my first answer wasn't correct. I misunderstood the question. Here's a new example:
$Props = 'Level-0','Level-1','Level-2','Level-3','Level-4'
$GroupDefs = Import-Csv C:\temp\Book2.csv
For( $i = 0; $i -lt $GroupDefs.Count; ++$i )
{ # Loop through the objects that came from the CSV file...
For( $p = 1; $p -lt $Props.Count; ++$p )
{ # Loop through the known properties...
$Prop = $Props[$p] # Convenience var
$GroupProp = $Props[$p-1] # Convenience var
If( $GroupDefs[$i].$Prop ) {
# If the property is populated then loop backwards from $i-1,
# the group def record just prior to the current one.
$Member = $GroupDefs[$i].$Prop
:Inner For($r = ($i-1); $r -ge 0; --$r)
{
If( $GroupDefs[$r].$GroupProp ) {
# This means you hit the first record behind your current
# position that has the previous property populated. Now
# we know the group...
$Group = $GroupDefs[$r].$GroupProp
Add-DistributionGroupMember -Identity $Group -Member $Member -WhatIf
Break Inner
}
}
}
}
}
By using traditional For Loops we can find values at other positions. So, the way I worked this out is to nest a loop of the known properties in a loop of the group definitions. When I find a property that has a value, I then loop backward from the current position in $GroupDefs until I find the previous property populated. By that point, I've managed to find both the group and the member, so I can run the command.
Update for Comments:
There is no Dot sourcing in this program. Dot referencing is used. As previously mentioned . is an operator in the sense that the right-hand side will be evaluated before the property is referenced, hence we can use a variable or expression.
Imagine that you are going through the spreadsheet line by line. That is the outer loop of $GroupDefs. You can look at the value of $i as if it's a row#.
Now, for each of those rows, I want to look at each of a known set of property names. So we're going to loop through $Props. If one of those properties has a value, I then want to look at previous rows to find the nearest previous row where the previous property ($Prop[$p-1]) is populated. For example, if Level-2 is populated in Row# 3 I know I have to look back through rows 2, 1, 0 to find the first previous value for property Level-1. That part is the innermost loop, which moves backward through the $GroupDefs array from the current position -1 ($p = ($i-1)) to 0. When the first populated previous value for Level-1 is found I know that's the group name.
$Member is a convenience variable. It's set to $GroupDefs[$i].$Prop because the value of the given property is the member we wish to add to the yet to be determined group.
Note: $GroupDefs.$i returning nothing is expected. At any given moment $i is a number determined by the loop, but it is not the name of a property on the $GroupDefs array or any objects within it. So, it will neither return any property value from the array itself nor will it unroll (enumerate) any properties from the objects contained within.
Note: The value of $Prop will change as you loop through the properties and until a value is found on a property by the given name.
I realize this is confusing, but if you step through the code you will better understand what's happening. First, try to understand what's literally being done. Then the code should make more sense...
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 been trying to follow this answer in order to obtain unique strings from a given cell array. However, I'm running into trouble when iterating over these values. I have tried for loops as follows:
[unique_words, ~, occurrences] = unique(words);
unique_counts = hist(occurrences, 1:max(occurrences));
for a=1:numel(unique_words)
word = unique_words{a}
count = unique_counts{a}
result = result + a_struct.(unique_words{a}) + unique_counts{a}
end
When trying to reference the items like this, I receive the error:
Cell contents reference from a non-cell array object.
Changing the curly brackets to round brackets for unique_couts yields the error:
Reference to non-existent field 'N1'.
Changing both unique_words and unique_counts to round brackets yields:
Argument to dynamic structure reference must evaluate to a valid field name.
How am I to iterate over the results of unique?
unique_words is a cell array. unique_counts is a vector. So unique_words should be accessed using curly brackets and unique_counts using round ones. The error that you are getting in this case is related to the a_struct (which is not defined in the question) not having the corresponding field, not the access method.
I have an object I defined with a method, childNodes(), which returns an array. When I do something like:
my #arr = obj->childNodes() I can clearly see that it can properly return an array.
My problem is that when I try to use this method to set the attribute of another class object, Perl decides I just want the length of childNodes() rather than the full array. This is not at all what I want and ruins everything. The code I'm using for this is:
$self->{'_arr'} = obj->childNodes()
How can I make this set $self->{'_arr'} to an array instead of just a scalar number?
Thanks in advance!
When you evaluate an array in scalar context, it returns the length of the array.
You want a reference to the array:
$self->{'_arr'} = [ obj->childNodes() ];
See perldoc perlref.