24

I often have the following situation in my PowerShell code: I have a function or property that returns a collection of objects, or $null. If you push the results into the pipeline, you also handle an element in the pipeline if $null is the only element.

Example:

$Project.Features | Foreach-Object { Write-Host "Feature name: $($_.Name)" } 

If there are no features ($Project.Features returns $null), you will see a single line with "Feature name:".

I see three ways to solve this:

if ($Project.Features -ne $null) { $Project.Features | Foreach-Object { Write-Host "Feature name: $($_.Name)" } } 

or

$Project.Features | Where-Object {$_ -ne $null) | Foreach-Object { Write-Host "Feature name: $($_.Name)" } 

or

$Project.Features | Foreach-Object { if ($_ -ne $null) { Write-Host "Feature name: $($_.Name)" } } } 

But actually I don't like any of these approaches, but what do you see as the best approach?

4 Answers 4

32

I don't think anyone likes the fact that both "foreach ($a in $null) {}" and "$null | foreach-object{}" iterate once. Unfortunately there is no other way to do it than the ways you have demonstrated. You could be pithier:

$null | ?{$_} | % { ... } 

the ?{$_} is shorthand for where-object {$_ -ne $null} as $null evaluated as a boolean expression will be treated as $false

I have a filter defined in my profile like this:

filter Skip-Null { $_|?{ $_ } } 

Usage:

$null | skip-null | foreach { ... } 

A filter is the same as a function except the default block is process {} not end {}.

UPDATE: As of PowerShell 3.0, $null is no longer iterable as a collection. Yay!

-Oisin

Sign up to request clarification or add additional context in comments.

3 Comments

The problem with the concise shorthand is that it will reject anything that coerces to false, which includes things like 0, "", @(), @(0), ... I'd probably expect Skip-Null to actually skip only $null. I know that in the context of this question the result is the same, but for a filter that might be used elsewhere too ...
sadly @() |?{$false} still returns $null instead of returning an empty list
$null should be on left side of equality comparison (Where-Object {$Null -ne $_}), because $_ could theoretically hold a (sub) array with multiple items where just one item is $Null which will incorrectly filter out the object.
12

If you can modify your function, have it return an empty collection/array instead of $null:

PS> function Empty { $null } PS> Empty | %{'hi'} hi PS> function Empty { @() } PS> Empty | %{'hi'} 

Otherwise, go with what Oisin suggests although I would suggest a slight tweak:

filter Skip-Null { $_|?{ $_ -ne $null } } 

Otherwise this will also filter 0 and $false.

Update 4-30-2012: This issue is fixed in PowerShell v3. V3 will not iterate over a scalar $null value.

5 Comments

Why can't I only accept one answer! They are both great, thank you guys! I gave Oisin the "answer", Keith already has the most point:-)
What are the chances that I happened to be looking at this very answer when you edited it nearly 17 months after it was posted? Is there a cmdlet to calculate that?
@BACON It's definitely not coincidental. I noticed via my SO inbox that you had posted a comment on one of my answers. :-) Just thought it would be good to point out that this isn't an issue in V3.
Glad this has been fixed. sigh
This is not fixed in v3 or v4 for that matter. They only fixed it in foreach, not for piping. See blogs.msdn.com/b/powershell/archive/2012/06/14/…
2

A quick note to Keith's answer to complete it

Personally, I would return nothing. It makes sense:

PS> function Empty { if ('a' -eq 'b') { 'equal' } } PS> Empty | % { write-host result is $_ } 

But now you are in problems if you assign result from Empty to a variable:

PS> $v = Empty PS> $v | % { write-host result is $_ } 

There is a little trick to make it work. Just wrap the result from Empty as a array like this:

PS> $v = @(Empty) PS> $v | % { write-host result is $_ } PS> $v.GetType() IsPublic IsSerial Name BaseType -------- -------- ---- -------- True True Object[] System.Array PS> $v.Length 0 

2 Comments

That's the approach I use today if I don't control the definition of the command being invoked. Otherwise I return an empty array if the function normall returns multiple items such that I would foreach over it - been bit too many times forgetting to wrap in @(). :-)
@Keith, I think you wrote great article about this tricky behaviour. You can link it here, others should read it, definitely ;)
2

Another possibility:

$objects | Foreach-Object -Begin{If($_ -eq $null){continue}} -Process {do your stuff here} 

More info in about_Continue

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.