I noticed odd behaviour using arrays in scriptblocks. The following code shows the problem:
$array = #("x", "y")
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$bad = {
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
$good = {
$array = $array.Clone()
$array += "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $good
& $bad
Executing the script will produce the following output:
Object[]
array
Object[]
array
x
y
z
String
System.Object
z
The scriptblock $bad does not work as I would expect. It converts the array to string, but it should simply add the element z to the array. If there is no element added, the array can be used as expected.
I noticed this behaviour in powershell 5.0 and 5.1 but not in the ISE. Is it a bug or can anyone explain this?
It's a scope issue. The variable on the left side of the assignment operation in the scriptblocks is defined in the local scope.
This statement
$array = $array.Clone()
clones the value of the global variable $array and assigns it to the local variable $array (same name, but different variable due to different scope). The local variable $array then contains a copy of the original array, so the next statement
$array += "z"
appends a new element to that array.
In your other scriptblock you immediately append a string to the (local) variable $array. In that context the local variable is empty, so $array += "z" has the same effect as $array = "z", leaving you with a variable containing just the string "z".
Specify the correct scope and you'll get the behavior you expect:
$array = #("x", "y")
$not_bad = {
$script:array += "z"
Write-Host "$($script:array.GetType().Name)"
Write-Host "$($script:array.GetType().BaseType)"
$script:array
}
& $not_bad
Beware, however, that this will actually modify the original array in the global/script scope (your $good example leaves the original array unchanged).
I'm not sure if I would consider this behavior a bug, but it's definitely a gotcha.
I would like to post my preferred solution which bases on Ansgars explanation:
$array = #("x", "y")
$not_bad = {
$array = $array + "z"
Write-Host "$($array.GetType().Name)"
Write-Host "$($array.GetType().BaseType)"
$array
}
& $not_bad
Important is the assignment to the local variable (or better to create a local variable) before adding further elements. A simple
$array = $array
would do, but this line may be confusing.
Related
This question already has an answer here:
Powershell: Piping output of pracl command to array
(1 answer)
Closed 1 year ago.
I am trying to add elements to array for filtering. after it goes through the loop the first time
I receive "Method invocation failed because [System.Management.Automation.PSObject] does not contain a method named 'op_Addition'."
I have tried several methods to try and figure this out.
$JsonDB = Get-Content 'Q:\Technology\1AA\HardwareCollection.json' | Out-String | ConvertFrom-Json
foreach($client in $JsonDB)
{
if($client.HRSeparation -eq "No")
{
$ClientNotHRSeparated += $client
}
else
{
$ClientHRSeparated += $client
}
}
$JsonDB
Any help would be greatly appreciated, Thanks!!
ConvertFrom-Json parses a JSON string into PSObject(s).
Since you did not define $ClientNotHRSeparated and $ClientHRSeparated anywhere, but immediately start adding ($client) objects to it, in the first iteration your variable $ClientNotHRSeparated will become that client object.
The next time you do +=, you're trying to add an object to another object which does not work.
Define the variables on top of the script, preferably as List object that has a .Add() method.
$ClientNotHRSeparated = [System.Collections.Generic.List[object]]::new()
$ClientHRSeparated = [System.Collections.Generic.List[object]]::new()
Then in your loop use that as
$ClientNotHRSeparated.Add($client)
# same for $ClientHRSeparated
P.S. Using a List is much faster/better that adding to a simple array (#()), because when you add items to an array (which has a fixed length) with +=, the entire array needs to be rebuilt in memory, consuming memory and processing time
Although this works, you don't need a loop at all. Just do:
$ClientNotHRSeparated = $JsonDB | Where-Object { $_.HRSeparation -eq "No" }
$ClientHRSeparated = $JsonDB | Where-Object { $_.HRSeparation -ne "No" }
The first line can be rewritten as $JsonDB = Get-Content -Path 'Q:\Technology\1AA\HardwareCollection.json' -Raw | ConvertFrom-Json.
Switch -Raw makes the cmdlet read the content of the file as one single multilined string
The behavior of += is entirely dependent on the left-hand side operand. On the first assignment, the value of $ClientNotHRSeparated is $null, so the resulting operation is:
$ClientNotHRSeparated = $null + $someCustomPSObject
Which PowerShell evaluates as just:
$ClientNotHRSeparated = $someObject
On the second assigment, $ClientNotHRSeparated is no longer $null, and PowerShell instead of tries to identify an overload for + that works on two operands of type [PSObject], which is where it fails.
If you want += to perform array addition, define the two array variables ahead of time with an assignment of a resizable array (use the #() array subexpression operator):
$ClientNotHRSeparated = #()
$ClientHRSeparated = #()
$JsonDB = Get-Content 'Q:\Technology\1AA\HardwareCollection.json' | Out-String | ConvertFrom-Json
foreach ($client in $JsonDB) {
if ($client.HRSeparation -eq "No") {
$ClientNotHRSeparated += $client
}
else {
$ClientHRSeparated += $client
}
}
$JsonDB
Now += is unambiguous both the first time and subsequently - the left-hand side operand is an array in either case.
As an alternative to looping through the whole collection manually, consider using the .Where() extension method in Split mode:
$JsonDB = Get-Content 'Q:\Technology\1AA\HardwareCollection.json' | Out-String | ConvertFrom-Json
$ClientNotHRSeparated, $ClientHRSeparated = #($JsonDB).Where({$_.HRSeparation -eq 'No'}, 'Split')
Much faster and more concise :-)
So lets say my variable $a is an array containing "1" and "2" as string.
$a = "1", "2"
Now I want to use foreach through a pipeline to subtract 1 from each value, so I'd do something like
$a | foreach{$_ = [int]$_ - 1}
but this seems do nothing, yet produces no error. So $a still contains "1" and "2". I struggle to understand where I went wrong... It's possible if i don't have an array, so this works:
$b = "3"; $b - 2
And it will return 1. So I also tried without "[int]" but it still fails, so I'm guessing it either has to do with the pipeline or my foreach but I wouldn't know why it does that.
Any suggestions?
Your foreach isn't mutating the items in your original array like you think it is - you're assigning the calculated value to the context variable $_, not updating the array index.
You can either create a new array with the calculated values as follows:
$a = $a | foreach { [int]$_ - 1 }
or mutate the items in the original array in-place:
for( $i = 0; $i -lt $a.Length; $i++ )
{
$a[$i] = [int]$a[$i] - 1
}
Note that your second example doesn't quite do what you think either:
PS> $b = "3"; $b - 2
1
PS> $b
3
The $b - 2 part is an expression which is evaluated and echoed out to console - it doesn't change the value of $b because you haven't assigned the result of the expression back to anything.
Just add the instance variable to the last line of your loop like so:
$a = $a | foreach{$_ = [int]$_ - 1; $_}
I am trying to compare 2 values in variables to see if they're the same, in the PowerShell output I can see that some combinations should be true!
First, without the making of $vergelijking1 and $vergelijking2 it showed as if $nummersPOs[$counter] and $object.'col1' were the same but the if statement was never true.
The only thing I could think of as to why it would fail is that 1 of the variables comes from an array. When I changed both types to String I could indeed see that there was some hidden text but I don't understand why my if statment is never true now. It writes "test2" but should write "inside the loop".
[System.Collections.ArrayList]$data = Import-Csv "C:\Users\UserName\Documents\test.csv"
[System.Collections.ArrayList]$NummersPOs = Import-Csv "C:\Users\UserName\Documents\test.csv" | select "col1" -Unique
$counter = 0
foreach ($object in $NummersPOs) {
$newCSV = New-Object System.Collections.ArrayList
foreach ($object in $data) {
if ($object."col2") {
$index = $newCSV.Add($object)
[string]$vergelijking1 = $NummersPOs[$counter]
#$vergelijking1 = $vergelijking1 -replace '\D+(\d+)','$1'
$vergelijking1
[string]$vergelijking2 = $object.'col1'
$vergelijking2
if ($vergelijking1 -contains $vergelijking2) {
Write-Host "inside the loop"
} else {
Write-Host "test2"
}
}
}
$counter++
}
$newCSV | Export-Csv "C:\Users\UserName\Documents\test2.csv"
Sample output:
#{col1=632424}
632424
test2
#{col1=632424}
632446
test2
As you can see, the first one should have been true already. -contain or -like both give false BTW.
Take an actual look at your operands:
"#{col1=632424}" ← $vergelijking1
"632424" ← $vergelijking2
These two strings are obviously not equal, so why would you expect them to be?
You're assigning an object to $verglijking1 and cast it to a string, so you actually get a string representation of that object (#{col1=632424}). To $verglijking2 you assign the value of an object property ($object.'col1'). Even though you still cast the value to a string you only get the string representation of the value, not of the object.
Also, you can't use the -contains operator, as #Mathias R. Jesson pointed out, since that operator is for checking the presence of elements in an array. The -like operator would've worked, but you'd have to put wildcards before and after the value ("*$verglijking2*"). Without wildcards the operator behaves exactly like the -eq operator.
With that said, all the casting you do in your script is completely unnecessary. Handle both variables the same way and use the correct comparison operator, and the problem will disappear.
if ($object.col2) {
$vergelijking1 = $NummersPOs[$counter].col1
$vergelijking2 = $object.col1
if ($vergelijking1 -eq $vergelijking2) {
Write-Host "inside the loop"
} else {
Write-Host "test2"
}
}
I wondered if anyone could shed some light on the issue I am facing when returning values from a multidimensional array through a function:
$ArrayList = #()
function MultiDimensionalArrayTest
{
param(
[array]$ArrayList
)
for($i = 0; $i -lt 1; $i++)
{
$ArrayModify += ,#("Test", "Test")
}
return $ArrayModify
}
$ArrayModify = MultiDimensionalArrayTest
foreach ($item in $ArrayModify)
{
Write-Host $item[0]
}
When the loop is executed once the values returned are:
T
T
However if the for statement is lopped twice, the values returned are:
Test
Test
My aim is to retrieve x amount of values "Test" from the Write-Host $item[0] regardless of how many times the statement is executed
It appears that if two or more rows are captured and returned in the $ArrayModify array, the value is a system.object[], however if looped once, the value "Test, Test" is captured and when Write-Host $item[0] is executed, it will print T.
Any advice would be greatly appreciated.
Not the cleanest way of dealing with it but you need to prevent PowerShell from unrolling the single element array into an array with two elements.
function MultiDimensionalArrayTest{
$ArrayModify = #()
for($i = 0; $i -lt 1; $i++){
$ArrayModify += ,#("Test$i", "Test$i$i")
}
,#($ArrayModify)
}
Using the above function will get the desired output I believe. ,#($ArrayModify) ensures that the array is returned and not unrolled into its elements as you saw above.
$ArrayList = #()
$ArrayList = MultiDimensionalArrayTest
foreach ($item in $ArrayList){$item[0]}
Giving the output for $i -lt 1 in the loop
Test0
Giving the output for $i -lt 2 in the loop
Test0
Test1
Your Output
Concerning your output from your example with $i -lt 1 PowerShell is unrolling the array into a single dimension array with 2 elements "Test" and "Test". You are seeing the letter "T" since all strings support array indexing and will return the character from the requested position.
Other issues with code
Not to beat a dead horse but really look at the other answers and comments as they provide some tips as to some coding errors and anomalies of the code you presented in the question.
First of all I notice several mistakes in your code.
This line $ArrayList = #() is useless since you don't use the $ArrayList variable afterwards.
Regarding the MultiDimensionalArrayTest function, you declared an $ArrayList argument but you never use it in the function.
Finally when this line makes no sense $ArrayModify = MultiDimensionalArrayTest = $item, you probably meant $ArrayModify = MultiDimensionalArrayTest.
This is not a complete answer to your question, since I am not sure what you are trying to achieve, but take a look at this line:
$ArrayModify = MultiDimensionalArrayTest = $item
This makes no sense, since you are calling the function MultiDimensionalArrayTest and passing two arguments to it, which are "=" (powershell assumes this is a string) and $item (null object). Then you assign whatever is returned to $ArrayModify.
The reason "T" is outputted, is because you are outputting the first element of what is at the moment a string. "Test" is outputted when $item is an array of strings.
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.