Get-ChildItem.Length is Wrong

3.5k views Asked by At

I am writing a recursive function that goes through a directory and copies every file and folder in it. The first check I have in the function is to see if the path passed in has children. To find this out, I use the following method:

[array]$arrExclude = @("Extras")
Function USBCopy
{
Param ([string]$strPath, [string]$strDestinationPath)
    try
    {
        $pathChildren = Get-ChildItem -Path $strPath
        if($pathChildren.Length -gt 0)
        {
            foreach($child in $pathChildren)
            {
                if($arrExclude -notcontains $child)
                {
                    $strPathChild = "$strPath\$child"
                    $strDestinationPathChild = "$strDestinationPath\$child" 
                    Copy-Item $strPathChild -Destination $strDestinationPathChild
                    USBCopy $strPathChild $strDestinationPathChild  
                }   
            }
        }              
    }
    catch
    {
        Write-Error ("Error running USBCopy: " + $Error[0].Exception.Message)
    }    
}

For the most part my function works, but my code will say a directory is empty when it actually has 1 file in it. When I debug my function, the variable will say that the folder has children but the variable's length is 0. Anyone know how to get around this?

2

There are 2 answers

2
WillPanic On BEST ANSWER

Try $pathChildren.Count instead of $pathChildren.Length - that will return the number of items in the array.

0
mklement0 On

PetSerAl, as many times before, has provided the crucial pointer in a terse comment on the question (and he's also assisted in refining this answer):

$pathChildren = @(Get-ChildItem -Path $strPath)

The use of @(...), the array subexpression operator, ensures that whatever the enclosed command outputs is treated as an array, even if only 1 object is output, so that .Length is guaranteed to be the array's .Length property.

However, in PSv3+, accessing .Count instead of .Length, as in WillPanic's helpful answer, works too - see below.

Without @(...), the result may be a single object, because PowerShell automatically unwraps an output collection that contains only 1 object, which yields that one object only, which implies the following:

  • up to PSv2:

    • If that one object happens to have a .Length property, its values is returned.
      In the case at hand, this is true if the only object returned represents a file (a [System.IO.FileInfo] instance) (which in turn is true if the directory contains exactly 1 file and no subdirectories, hidden items aside).
      A [System.IO.FileInfo]'s instance's .Length property returns the file's size in bytes. A value of 0 implies an empty file.
      (If the only object returned had been a directory (a [System.IO.DirectoryInfo] instance, .Length would have returned $null, because such instances don't have a .Length property.)
  • in PSv3+, the workaround is no longer strictly needed, if you use .Count, because you can treat even a scalar (single object) as if it were an array, with implicit .Length / .Count[1] properties and the ability to index into (e.g.,
    <scalar>[0])
    - these implicit members are known as intrinsic members - but there are caveats:

    • If Set-StrictMode -Version 2 or higher is in effect, access to .Length and .Count properties that don't actually exist on a scalar at hand cause an error.
      This behavior is quite unfortunate, however, as these properties should be considered to exist implicitly - if you agree, make your voice heard in GitHub issue #2798.

    • If the scalar itself has a property such as .Length or .Count or supports indexing, that takes precedence - this is why .Count must be used in this case (as stated, [System.IO.FileInfo] instances have a .Length property reporting the file size in bytes); see below for examples.

    • Using @(...) avoids such collisions, because the result is always an array.

    • Member-access enumeration is the complementary aspect of unification, which allows you to apply a member (property or method) of the items contained in the collection at the collection level, in which case the member is implicitly accessed on every item in the collection, and the resulting values are returned as an array; see below for an example.
      To resolve name collisions with member-access enumeration, a different approach is needed - see this answer.


Examples of PSv3+ unified collection handling

PS> (666).Length
1  # Scalar 666 was implicitly treated as a collection of length 1

PS> (666).Count
1  # Ditto - ** .Count is preferable, because it less often means something else **

# Caveat: A *string* scalar has a native .Length property
PS> ('666').Length; ('666').Count
3  # .Length: The string types's native property: the number of *characters*
1  # .Count: PowerShell's implicit collection handling: 1 *element*

PS> (666)[0]; (666)[-1]
666  # Index [0] always yields the scalar itself.
666  # Ditto for [-1], the *last* element.

# Member-access enumeration example: get the .Day property value from each
# [datetime] instance stored in an array.
PS> ((Get-Date), (Get-Date).AddDays(-1)).Day
20
19

[1] As PetSerAl points out, up to PSv5.1, an array's .Count property was an alias property of .Length, added by PowerShell's ETS (extended type system - see Get-Help about_Types.ps1xml). However, this alias property hasn't really been needed since PSv3, when explicitly implemented .NET interface type members were exposed by PowerShell too, providing access to the array type's ICollection.Count property. v6 will therefore no longer have the alias property, at which point .Count will directly access ICollection.Count - see GitHub issue #3222.
Note that PowerShell magic (i.e., an intrinsic member) is still involved when it comes to invoking .Count on a scalar (non-collection), however.