Why Is It Possible to Loop Through a Null Array - powershell

Given the following PowerShell code:
$FolderItems = Get-ChildItem -Path "C:\Test"
Write-Host "FolderItems Is Null: $($FolderItems -eq $null)"
foreach ($FolderItem in $FolderItems)
{
Write-Host "Inside the loop: $($FolderItem.Name)"
}
Write-Host "Done."
When I test it with one file in the C:\Test folder, it outputs this:
FolderItems Is Null: False
Inside the loop: MyFile.txt
Done.
However, when I test it with ZERO files in the folder, it outputs this:
FolderItems Is Null: True
Inside the loop:
Done."
If $FolderItems is null, then why does it enter the foreach loop?

This was an intentional design choice made in V1 and revisited in V3.
In most languages, the foreach statement can only loop over collections of things. PowerShell has always been a little different, and in V1, you could loop over a single value in addition to collections of values.
For example:
foreach ($i in 42) { $i } # prints 42
In V1, if a value was a collection, foreach would iterate over each element in the collection, otherwise it would enter the loop for just that value.
Note in the above sentence, $null isn't special. It's just another value. From a language design point of view, this is fairly clean and concisely explained.
Unfortunately many people did not expect this behavior and it caused many bugs. I think some confusion arises because people expect the foreach statement to behave almost like the foreach-object cmdlet. In other words, I think people expect the following to work the same:
$null | foreach { $_ }
foreach ($i in $null) { $i }
In V3, we decided that it was important enough to change behavior because we could help scripters avoid introducing bugs in their scripts.
Note that changing the behavior could in theory break existing scripts in unexpected ways. We ultimately decided that most scripts that potentially see $null in the foreach statement already guard the foreach statement with an if, e.g.:
if ($null -ne $c)
{
foreach ($i in $c) { ... }
}
So in reality, most real world scripts would not see a change in behavior.

This was something of an idiosyncracy/bug in ForEach in V1 and V2. It was corrected in the V3 release.

Seems to me like you need to wrap your foreach within a conditional that checks if $FolderItem != null. This way, it'll never get in the if statement whenever $FolderItems is NULL
If (-NOT $FolderItems -eq $null) {
foreach ($FolderItem in $FolderItems)
{
Write-Host "Inside the loop: $($FolderItem.Name)"
}
}
This may be of help as well http://bit.ly/1brKRRk

Related

Foreach with Where-Object not yielding correct results

I have the following code:
$ErrCodes = Get-AlarmIDs_XML -fileNamePath $Paper_Dialog_FullBasePath
$excelDevice = Get_ErrorCodes -errorCodeListFilePath $outFilePath -ws "DEVICE COMPONENT MAP"
foreach ($errCode in $ErrCodes | Where-Object{$excelDevice.Common_Alarm_Number -eq $errCode.Value })
{
#$dataList = [System.Collections.Generic.List[psobject]]::new()
#$resultHelp = [System.Collections.Generic.List[psobject]]::new()
Write-Host "another:"
$err = $errCode.Value #get each error code to lookup in mdb file
$key = $errCode.Key
Write-Host $err
...
}
But it's definitely getting in the foreach loop when it shouldn't.
My intention is to use the foreach, and if it has a value in the $ErrCodes, then it should continue with the code that follows.
Let me know if you need to see the Functions that do the file reads, but the data structures look like this:
$excelDevice:
[Object[57]]
[0]:#{Common_Alarm_Number=12-2000}
[1]:#{Common_Alarm_Number=12-5707}
[2]:#{Common_Alarm_Number=12-9}
[3]:#{Common_Alarm_Number=12-5703}
...
$ErrCodes:
[Object[7]]
[0]:#{Key=A;Value=12-5702}
[1]:#{Key=B;Value=12-5704}
[2]:#{Key=C;Value=12-5706}
[3]:#{Key=D;Value=12-5707}
...
So we only care about the ones in $ErrCodes that are also in $excelDevice.
When I step through the code, it's getting into the foreach code for 12-5702 for some reason, when it shouldn't be there (prints 12-5702 to screen). I know I wouldn't want 12-5702 to be used because it isn't in $excelDevice list.
How would I get that Where-Object to filter out $ErrCodes that aren't in $excelDevice list? I don't want to process error codes that don't have data for this device.
Right now you're testing whether any of the values in $excelDevice.Common_Alarm_Number (which presumably evaluates to an array) is exactly the same value as all the values in $errCodes.Value - which doesn't make much sense.
It looks like you'll want to test each error code for whether it is contained in the $excelDevice.Common_Alarm_Number list instead. Use $_ to refer to the individual input items received via the pipeline:
foreach ($errCode in $ErrCodes | Where-Object{ $excelDevice.Common_Alarm_Number -contains $_.Value }) { ... }

Powershell Key/Value Pair Matching Problem

I am looking to check a key value pair in powershell. I have tried various methods but none seem to be working as I would expect.
I keep getting a True response on a lookup of $checked_group, even though I can see the KeyValue pair in the write-out.
if (!($checked_groups[$varDomain] -eq "$varName"))
Even if the keyValue pair is in the $checked_groups dictionary the above statement is True. Have you ever come across this before?
I cant recreate the problem using the snippet below even though its pretty much the same logic as my live code. The sources of the values are different, as I am dynamically collecting these in the live code. And iterating over them in a loop, my $checked_groups = #{ } is outside this loop to maintain the dict key/pairs across all iterations and I can confirm they are preserved across ever iteration.
I can't work out why this statement would always resolve to True :(
$checked_groups = #{ }
$varDomain = "example.com"
$varName = "Administrator"
if (!($checked_groups[$varDomain] -eq "$varName"))
{
write-host 'Not In There'
}
foreach ($thing in $checked_groups)
{
Write-Host ($thing | Out-String)
}
$checked_groups += #{$varDomain = $varName}
if (($checked_groups[$varDomain] -eq "$varName"))
{
write-host 'In There'
foreach ($thing in $checked_groups)
{
Write-Host ($thing | Out-String)
}
}

Writing $null to Powershell Output Stream

There are powershell cmdlets in our project for finding data in a database. If no data is found, the cmdlets write out a $null to the output stream as follows:
Write-Output $null
Or, more accurately since the cmdlets are implemented in C#:
WriteOutput(null)
I have found that this causes some behavior that is very counter to the conventions employed elsewhere, including in the built-in cmdlets.
Are there any guidelines/rules, especially from Microsoft, that talk about this? I need help better explaining why this is a bad idea, or to be convinced that writing $null to the output stream is an okay practice. Here is some detail about the resulting behaviors that I see:
If the results are piped into another cmdlet, that cmdlet executes despite no results being found and the pipeline variable ($_) is $null. This means that I have to add checks for $null.
Find-DbRecord -Id 3 | For-Each { if ($_ -ne $null) { <do something with $_> }}
Similarly, If I want to get the array of records found, ensuring that it is an array, I might do the following:
$recsFound = #(Find-DbRecord -Category XYZ)
foreach ($record in $recsFound)
{
$record.Name = "Something New"
$record.Update()
}
The convention I have seen, this should work without issue. If no records are found, the foreach loop wouldn't execute. Since the Find cmdlet is writing null to the output, the $recsFound variable is set to an array with one item that is $null. Now I would need to check each item in the array for $null which clutters my code.
$null is not void. If you don't want null values in your pipeline, either don't write null values to the pipeline in the first place, or remove them from the pipeline with a filter like this:
... | Where-Object { $_ -ne $null } | ...
Depending on what you want to allow through the filter you could simplify it to this:
... | Where-Object { $_ } | ...
or (using the ? alias for Where-Object) to this:
... | ? { $_ } | ...
which would remove all values that PowerShell interprets as $false ($null, 0, empty string, empty array, etc.).

PowerShell IF statement Variables

im curious how to use the IF statement logic further in my code. Let me elaborate with an example.
$a=1
$b=10000
if(($a=1) -or ($b=1))
{write-host ""} #Here, I want to write what ever variable was $true
#in the if statement above.... so I want it to
#write "1" which was $a and was $true in the if
#statement...
I could write more logic to accomplish this, but im wondering if the values that the if statement used can be used again in the code. Im thinking there is a "hidden" variable maybe?
($a=1) is usually an assignment statement. This is not the case in Powershell, but it sure looks like a bug. The Powershell way to do comparison is to use -eq. So,
if(($a -eq 1) -or ($b -eq1))
Now, the simple solution is a bit different. What happens, if both $a and $b happen to be 1?
$comp = 1 # Value to compare against
$a=1
$b=100
if($a -eq $comp) { write-host "`$a: $a" }
if($b -eq $comp) { write-host "`$b: $b" }
This approach is easy to understand, which is in most of the cases more important than other factors like speed.

Powershell being too clever

Apologies for what is probably a newbish question.
I am writing some Powershell scripts that run various queries against AD. They will usually return a bunch of results, which are easy to deal with, ie:
$results = myQuery
write-host "Returned " + $results.Count + " results."
foreach ($result in $results) { doSomething }
No worries. However, if there is only 1 result, Powershell automagically translates that result into a single object, rather than an array that contains 1 object. So the above code would break, both at the Count and the foreach. I'm sure the same problem would occur with 0 results.
Could someone suggest an elegant way to handle this? Perhaps some way to cast the results so they are always an array?
Change the first line of code to
$results = #(myQuery)
This will always return an array. See this blog entry for additional details.
Actually, the foreach works just fine. All uses of foreach (the foreach keyword, the Foreach-Object cmdlet, and Foreach-Object's aliases "foreach" and "%") all have the same behavior of "wrapping" the object in question in an array if needed. Thus, all uses of foreach will work with both scalar and array values.
Annoyingly, this means they work with null values too. Say I do:
$x = $null
foreach ($y in $x) {Write-Host "Hello world 1"}
$x | Foreach-Object {Write-Host "Hello world 2"}
I'll get
"Hello world 1"
"Hello world 2"
out of that.
This has bitten me as well. No clever ideas on how to fix $results.Count, but the foreach can be fixed by switching to a pipeline.
$scalar = 1
$list = (1,2)
$list | % { $_ }
prints
1
2
$scalar | % { $_ }
prints
1