Get-ChildItem.Length is Wrong
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?
Solution 1:
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 of0
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.)
- If that one object happens to have a
-
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.-
@(...)
, the array-subexpression operator is also necessary to work around a bug in Windows PowerShell (as of the latest and final version, 5.1), which has since been corrected in PowerShell (Core):[pscustomobject]
instances unexpectedly do not have.Count
and.Length
properties (see GitHub issue #3671 for the original bug report) - see this answer.
-
-
Member 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 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 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.
Solution 2:
Try $pathChildren.Count
instead of $pathChildren.Length
- that will return the number of items in the array.