Friday, August 05, 2011

What do you get when you iterate null?

In most cases, an exception; but in PowerShell you get $null.

Suppose I have folders with 0, 1 and more than 1 file in (called empty, full and two, say) and I want to enumerate the names of the files contained in each, with some general purpose code.

That should be no problem --

function Get-FileNames($dir) {
  $files = Get-ChildItem $dir
  foreach ($file in $files) {
    Split-Path -Leaf $file
  }
}

and let's write a little reporting function

function Get-Report($arg) {
  $arg
  $arg.GetType().FullName
  $arg.Length
  $arg | % { Write-Host "++$_++" }
  Write-Host "-----"
}

When we report on the folders in descending order of contained files we get

test (2).txt
test.txt
System.Object[]
2
++test (2).txt++
++test.txt++
-----
test.txt
System.String
8
++test.txt++
-----
Split-Path : Cannot bind argument to parameter 'Path' because it is null.
At C:\Users\Steve\Documents\scratch\Untitled1.ps1:6 char:15
+     Split-Path <<<<  -Leaf $file
    + CategoryInfo          : InvalidData: (:) [Split-Path], ParameterBindingValidationException
    + FullyQualifiedErrorId : ParameterArgumentValidationErrorNullNotAllowed,Microsoft.PowerShell.Commands.Split 
   PathCommand
 
You cannot call a method on a null-valued expression.
At C:\Users\Steve\Documents\scratch\Untitled1.ps1:12 char:15
+   $arg.GetType <<<< ().FullName
    + CategoryInfo          : InvalidOperation: (GetType:String) [], RuntimeException
    + FullyQualifiedErrorId : InvokeMethodOnNull
 
++++

Not happy.

The first error is what you get when you work on the foreach over $null, yielding $null; and calling code has to special-case the single item return -- getting the .Length property of the return is not the number of files! For that replace $arg.Length by $arg | Measure-Object | % {write-host $_.Count} .

So try this

function Get-Array($list)
{
  if (-not $list) {
    ,(@())
  } else {
    if ($list -is [array]) {
      $list
    } else {
      ,(,$list)
    }
  }
}

function Get-FileNamesX($dir)
{
  $files = Get-Array(Get-ChildItem $dir)
  foreach ($file in $files)
  {
    Split-Path -Leaf $file
  }
}

function Get-FileNames2($dir)
{
  Get-Array(Get-FileNamesX($dir))
}

where we fight PowerShell's fragmentation of sequences all the way. Now we get what we expected

test (2).txt
test.txt
System.Object[]
2
++test (2).txt++
++test.txt++
-----
test.txt
System.Object[]
1
++test.txt++
-----
System.Object[]
0
-----

Oh, yeah -- the more PowerShell way of doing all this:

function Get-FileNames($dir)
{
  Get-ChildItem $dir | % {
    Split-Path -Leaf $_
  }
}

function Get-Report {
  PARAM ($InputObject) 
  END {
  $arg = @($InputObject + $Input)
  $arg
  $arg.GetType().FullName
  $arg.Length
  $arg | Measure-Object | % {write-host "Measure = $($_.Count)"}
  $arg | % { Write-Host "++$_++" }
  Write-Host "-----"
 }
}

Get-FileNames(".\two") | Get-Report
Get-FileNames(".\full") | Get-Report
Get-FileNames(".\empty") | Get-Report

Post a Comment