3

I'm a bit confused by the -Depth flag for Get-ChildItem. The following works great (finds all files and folders only one deep under "C:\Program Files"):

dir 'C:\Program Files\' -Depth 1

But if I then want to extend it to find only *.txt type files, I cannot find how to do that (following just give weirdly unexpected output where -Depth 1 is ignored and it instead does the equivalent of a -Recurse to all subfolders no matter how deep):

dir 'C:\Program Files\*.txt' -Depth 1
dir 'C:\Program Files\' -Include *.txt -Depth 1
dir 'C:\Program Files\*' -Include *.txt -Depth 1

How do we use -Depth to a specific depth for Get-ChildItem and a required file-pattern?

YorSubs
  • 3,194
  • 7
  • 37
  • 60

4 Answers4

3

The behavior you're seeing is a bug in Windows PowerShell, that has since been fixed in PowerShell [Core] 6+ - see this GitHub issue.

Given that Windows PowerShell is no longer actively developed, it is unlikely that the bug will be fixed.

To spell it out, Windows PowerShell ignores -Depth's depth constraint in the following cases:

  • with -Include or -Exclude
  • if the (implied) -Path argument contains wildcard characters.

While recursion is still performed, no depth limit is imposed; in effect, -Depth behaves like -Recurse (alone) in these cases.

Workarounds:

  • For -Include and wildcard-based -Path arguments where the wildcards are limited to the last path component:

    • Use -Filter instead, as shown in Wasif Hasan's answer.
    • Caveat: -Filter is usually preferable anyway for its superior performance, but its wildcard language is less powerful than PowerShell's and has legacy quirks - notably, character sets and ranges ([...]) are not supported and in Windows PowerShell a filter such as *.xls also matches *.xlsx files, for instance - see this answer.
  • For -Exclude:

    • Use only -Depth and perform filtering after the fact with a Where-Object call; e.g.,
      Get-ChildItem -File 'C:\Program Files\' -Depth 1 | Where-Object Name -NotLike *.txt
  • [Probably rarely needed] For wildcard-based -Path arguments with wildcard characters (also) in a component other than the last one (e.g., C:\foo*\bar)

    • Use -Recurse and perform filtering after the fact with Where-Object; in this case, you'll also have to weed out too-deep paths by counting the number of their components.
mklement0
  • 382,024
  • 64
  • 607
  • 775
  • 1
    Good to know, thanks Michael. Yes, I could not get my head around how unintuitive this behaviour appeared. Would be nice if Microsoft did a bug-fix rollout for PS 5.1. We are going to be using PS 5.1 for a long time since it has so much Windows specific functionality that PS 6 and 7 just do not have. Hopefully at some point MS can recognise that. Don't need any 6/7 features backported to 5.1, but at least fixing bugs would be useful. (PS v5.1 SP1!) – YorSubs Mar 03 '20 at 15:43
  • 1
    Understood, @YorSubs; the upcoming v7.0 is an attempt to be a viable substitute for v5.1, but it'll never be able to offer _everything_ that Windows PowerShell does. It would indeed be nice if more fixes were back-ported, but chances are that only security-critical issues will be addressed. [UserVoice](https://windowsserver.uservoice.com/forums/301869-powershell) is the official place for Windows PowerShell bug reports, but, unfortunately, most issues reported there seem to never get addressed. – mklement0 Mar 03 '20 at 15:54
  • On PS v7, `(Get-WMIObject win32_operatingsystem).name` results in `Get-WMIObject: The term 'Get-WMIObject' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the spelling of the name, or if a path was included, verify that the path is correct and try again.`. If WMI is missing on PS v7, that's really a deal-breaker. Are there modules that re-add WMI for when running PS v7 on Windows, as otherwise I just cannot touch it for my work or personal projects? :-( – YorSubs Mar 09 '20 at 22:37
  • 1
    @YorSubs, here's the standard advice: The CIM cmdlets (e.g., `Get-CimInstance`) superseded the WMI cmdlets (e.g., `Get-WmiObject`) in PowerShell v3 (released in September 2012). Therefore, the WMI cmdlets should be avoided, not least because PowerShell _Core_, where all future effort will go, doesn't even _have_ them anymore. For more information, see [this answer](https://stackoverflow.com/a/54508009/45375). – mklement0 Mar 09 '20 at 22:39
  • 1
    oh, I see, that's good then. I think I do vaguely remember hearing that a while back but I guess I never really kept on top of these things (if it works, it works!). Great thanks, I'lll try and convert everything that is WMI over to CIM then, hopefully not too difficult. – YorSubs Mar 09 '20 at 22:42
2

The issue gets solved when you use Filter instead of Include. Filter parameter will return file in correct pattern with depth. (TESTED)

dir 'C:\Program Files\' -Filter *.txt  -Depth 1
Wasif
  • 14,755
  • 3
  • 14
  • 34
  • An effective workaround; two things worth noting: in PowerShell [Core] 6+, the workaround is no longer needed, because the [underlying bug](https://github.com/PowerShell/PowerShell/issues/3726) has been fixed. `-Filter` is often preferable for performance anyway, but its behavior differs from PowerShell's own wildcard matching - see [this answer](https://stackoverflow.com/a/60171419/45375). – mklement0 Mar 03 '20 at 14:46
1

In older versions of PowerShell there was no depth, in that case the above can also be

Get-ChildItem -Path "C:\DIRECTORY\*","C:\DIRECTORY\*\*"

If it is pure for filenames then

(Get-ChildItem -Path "c:\program files" -file -Depth 3 -Force -erroraction SilentlyContinue).FullName

Is identical to the ancient kind of tricks, i.e.

(cmd.exe /c dir "c:\program files" /b /a-d /s)|foreach {if ($_.split("\").length -le 5){$_}}

It's amazing that PowerShell is even faster than the above line! I remember that a few years ago that was not the case, but I just tested it and it was 3-4 times faster

E-D-H
  • 11
  • 1
0

To further clarify the answer by Wasif Hasan As I was going through the official documentation for the Get-ChildItem, it is stated there

When using the -Include parameter, if you do not include an asterisk in the path the command returns no output.

Which means that the Depth will be ignored automatically as the behavior required for the Include is recursive. Further some details of the -Include reveals these points.

If the Recurse parameter is added to the command, the trailing asterisk (*) in the Path parameter is optional. The Recurse parameter gets items from the Path directory and its subdirectories. For example, -Path C:\Test\ -Recurse -Include *.txt

So the behavior you are looking for is in the Filter flag for the Get-ChildItem which do not requires any wild cards

For me the Depth flag with any other flag that accepts wild cards in the path do not make sense as the purpose of the Depth flag is to restrict the depth of search in the Items where as specifying a wild card excludes that particular purpose. You can try this by simply using this command and you will see that the Depth parameter is not effective if you specify a wild card in the path for example

Get-ChildItem -Path C:\DIRECTORY\* -Depth 1

and

Get-ChildItem -Path C:\DIRECTORY\* -Depth 2

are going to return the same results.

Hope this helps clarify some issues

DevX
  • 308
  • 5
  • 16
  • 1
    Yes thanks, I was just going to say also that this doesn't make sense. Give me an error, or a meaningful warning that indicates how my syntax might be wrong, don't give me strangely unexpected results. I see zero reasons why `Depth` could not be written such that it operates *as expected* in those other situations. All seems a bit counterproductive. I wonder what the author of this Cmdlet was thinking with this strange output result. – YorSubs Mar 03 '20 at 08:04
  • 1
    The behavior is a _bug_ in Windows PowerShell that has since been fixed in PowerShell [Core] 6+ - see [this GitHub issue](https://github.com/PowerShell/PowerShell/issues/3726). Yes, `-Filter` doesn't require the `-Path` argument to end in `*` to take effect, but neither does `-Include` _if recursion is also performed_, and the use of `-Depth` _implies_ `-Recurse`. It is the _bug_ that makes your two `C:\DIRECTORY\*` commands with differing depths output the same result - the `-Depth` limit is again ignored. – mklement0 Mar 03 '20 at 14:43
  • @mklement0 agreed about the reported issue but not sure why `-Include`'s official documentation states that it needs a wildcard in the path or will return empty – DevX Mar 04 '20 at 05:12
  • 1
    @DevX, that is due to an _unrelated issue_, namely the unfortunate fact that _without recursion_, the `-Include` pattern is (first) matched against the _input paths themselves_, which means that often _nothing_ matches. Appending `\*` to input paths works around that problem - see the bottom section of [this answer](https://stackoverflow.com/a/38308796/45375). As written, your answer amounts to a distraction, because it adds to the confusion, so please consider amending it. – mklement0 Mar 04 '20 at 10:29