Get-ChildItem -Path C:\logs -Include *.log looks correct, runs without an error, and returns nothing. Swap -Include for -Filter and the files show up. Same cmdlet, same pattern, different result.

The two parameters run at different stages. -Filter goes to the provider, which skips non-matching files while it enumerates. -Include and -Exclude run inside PowerShell after the provider has already returned items, and only under certain path conditions. Once you know which stage you're working with, the "returns nothing" bug and the slow scripts both make sense.

Every command below was run in PowerShell 7.4.6 on Linux. I point out where Windows or Windows PowerShell 5.1 behaves differently, based on Microsoft's docs, because I couldn't test those here.

Build a small lab

Run this once. It creates a throwaway folder in your temp directory with a few tricky names: a folder whose name contains .log, an uppercase .LOG file, a .log.bak file, and the classic .htm / .html pair.

$root = Join-Path ([IO.Path]::GetTempPath()) 'gci-lab' Remove-Item $root -Recurse -Force -ErrorAction SilentlyContinue New-Item -ItemType Directory -Path "$root/logs/archive", "$root/logs/old.log.d" -Force | Out-Null 'a' | Set-Content "$root/logs/app.log" 'b' | Set-Content "$root/logs/web.log" 'c' | Set-Content "$root/logs/notes.txt" 'd' | Set-Content "$root/logs/error.LOG" 'e' | Set-Content "$root/logs/archive/2026-09.log" 'f' | Set-Content "$root/logs/archive/2026-09.log.bak" 'g' | Set-Content "$root/logs/report.html" 'h' | Set-Content "$root/logs/report.htm" Set-Location $root 

I pipe results to ForEach-Object Name to keep the output short. I don't use -Name because it changes how -Include behaves (more on that below).

The "returns nothing" gotcha

Get-ChildItem -Path ./logs -Filter *.log | ForEach-Object Name 
app.log error.LOG web.log 
@(Get-ChildItem -Path ./logs -Include *.log).Count 
0 

This isn't a bug, and it isn't only a Windows PowerShell 5.1 problem. It happened in pwsh 7.4. The Get-ChildItem docs say that -Include "is effective only when the command includes the contents of an item, such as C:\Windows\*". A plain -Path ./logs refers to the folder itself, not its contents, so nothing passes the include check.

You can fix it in any of three ways:

# 1. Wildcard path: "the contents of logs" Get-ChildItem -Path ./logs/* -Include *.log | ForEach-Object Name # 2. Add -Recurse (trailing * becomes optional) Get-ChildItem -Path ./logs -Recurse -Include *.log | ForEach-Object Name # 3. -LiteralPath (worked in pwsh 7.4 without * or -Recurse) Get-ChildItem -LiteralPath ./logs -Include *.log | ForEach-Object Name 
app.log error.LOG web.log 2026-09.log app.log error.LOG web.log app.log error.LOG web.log 

The -Recurse version also goes into archive/. Keep that in mind if you add -Recurse only to make -Include work: you also get every subfolder.

The third fix is something I observed in pwsh 7.4, not documented behavior. On Windows PowerShell 5.1, use -Path ...\* or -Recurse. Those two are the forms the docs describe.

Bonus trap: -Name takes a different code path

"without -Name: " + @(Get-ChildItem -Path ./logs -Include *.log).Count "with -Name: " + @(Get-ChildItem -Path ./logs -Include *.log -Name).Count "LiteralPath -Include *.txt -Name: " + @(Get-ChildItem -LiteralPath ./logs -Include *.txt -Name).Count 
without -Name: 0 with -Name: 3 LiteralPath -Include *.txt -Name: 8 

With -Name added, the "returns nothing" command returned 3 files. The -LiteralPath command that correctly returned only notes.txt returned all 8 items once -Name was added, so the include pattern was ignored. If you test an -Include command with -Name and then remove -Name in your script, you may get different results. Filter on objects first and select Name at the end.

What -Filter can and can't do

The docs describe it this way: the filter string is passed to the .NET file-enumeration API, it accepts one string, and only * and ? work as wildcards.

You can't pass more than one pattern:

Get-ChildItem -Path ./logs -Filter *.log, *.txt 
Cannot convert 'System.Object[]' to the type 'System.String' required by parameter 'Filter'. Specified method is not supported. 

Bracket ranges also don't work. You don't get an error, just no results:

@(Get-ChildItem -Path ./logs -Filter '[aw]*.log').Count Get-ChildItem -Path ./logs/* -Include '[aw]*.log' | ForEach-Object Name 
0 app.log web.log 

-Include uses PowerShell wildcards (about_Wildcards), so [aw] matches names starting with a or w. -Filter didn't treat [aw] as a character range and found nothing.

Also note that -Filter *.log returned error.LOG on Linux, where the file system is case-sensitive. I didn't expect that.

Windows-only quirks

On Windows, -Filter uses the operating system's file name matching rules, and the Directory.GetFiles docs list two old quirks:

  • Three-character extensions match longer extensions. On .NET Framework (the runtime under Windows PowerShell 5.1), *.xls also returns book.xlsx, so *.htm also returns .html. The docs label this ".NET Framework only". On .NET 5+ it returns only the exact extension. In my pwsh 7.4 lab, -Filter *.htm returned only report.htm.
  • 8.3 short names are matched too. A pattern like *1*.txt can return longfilename.txt because its short name is LONGFI~1.TXT.

If a 5.1 script that uses -Filter *.htm deletes or moves files, run it on a test folder first. It may also catch .html files.

-Exclude: two behaviors that catch people

Unlike -Include, -Exclude works on a plain folder path:

Get-ChildItem -Path ./logs -Exclude *.log | ForEach-Object Name 
archive old.log.d notes.txt report.htm report.html 

But it only compares item names. It doesn't skip a subfolder during recursion:

Get-ChildItem -Path ./logs -Recurse -File -Exclude archive | ForEach-Object Name 
2026-09.log 2026-09.log.bak app.log error.LOG notes.txt report.htm report.html web.log 

The archive folder itself was excluded, but both files inside it were still returned. To skip a subfolder, filter on the path: Where-Object FullName -notlike '*/archive/*' (use \archive\ on Windows).

The second one: the docs say exclusions are applied after inclusions, and neither one cares whether an item is a file or a folder:

Get-ChildItem -Path ./logs -Recurse -Include *.log* -Exclude *.bak | Select-Object Mode, Name 
Mode Name ---- ---- ----- 2026-09.log d---- old.log.d ----- app.log ----- error.LOG ----- web.log 

The folder old.log.d matches *.log*, so it's included. Adding -File removes it:

Get-ChildItem -Path ./logs -Recurse -File -Include *.log* -Exclude *.bak | ForEach-Object Name 
2026-09.log app.log error.LOG web.log 

-File and -Directory are FileSystem-provider switches. Use one whenever your pattern could match both files and folders. -Filter has the same problem: -Filter *.log* -Recurse also returns old.log.d.

How much faster is -Filter?

The Measure-Command docs show -Filter being much faster on C:\Windows. Here is the same comparison on a generated tree with 20,000 files, so you can rerun it yourself:

$root = Join-Path ([IO.Path]::GetTempPath()) 'gci-perf' Remove-Item $root -Recurse -Force -ErrorAction SilentlyContinue $ext = '.log', '.txt', '.json', '.csv', '.tmp' foreach ($d in 1..200) { $dir = New-Item -ItemType Directory -Path (Join-Path $root "app$d") -Force foreach ($f in 1..100) { [IO.File]::WriteAllText((Join-Path $dir "file$f$($ext[$f % 5])"), 'x') } } "Files created: " + (Get-ChildItem $root -Recurse -File).Count # warm-up so the first run doesn't pay the cold cache cost $null = Get-ChildItem $root -Recurse -File $tests = [ordered]@{ 'Filter *.log' = { Get-ChildItem -Path $root -Recurse -File -Filter *.log } 'Include *.log' = { Get-ChildItem -Path $root -Recurse -File -Include *.log } 'Where-Object Extension' = { Get-ChildItem -Path $root -Recurse -File | Where-Object Extension -eq '.log' } 'Include *.log, *.csv' = { Get-ChildItem -Path $root -Recurse -File -Include *.log, *.csv } 'Filter twice (log + csv)' = { '*.log', '*.csv' | ForEach-Object { Get-ChildItem -Path $root -Recurse -File -Filter $_ } } } foreach ($name in $tests.Keys) { $runs = 1..5 | ForEach-Object { (Measure-Command $tests[$name]).TotalMilliseconds } [pscustomobject]@{ Test = $name Matches = @(& $tests[$name]).Count MedianMs = [int]($runs | Sort-Object)[2] } } 
Files created: 20000 Test Matches MedianMs ---- ------- -------- Filter *.log 4000 50 Include *.log 4000 428 Where-Object Extension 4000 219 Include *.log, *.csv 8000 487 Filter twice (log + csv) 8000 97 

All runs were on the same 8-core Linux box with a warm cache, median of 5. Some results surprised me:

  • -Filter was about 8× faster than -Include for the same 4,000 files.
  • -Include was slower than Get-ChildItem | Where-Object. You might expect a built-in parameter to beat a pipeline filter. Here it took twice as long.
  • Running -Filter twice beat one -Include with two patterns (97 ms vs 487 ms), even though it walks the tree twice.

Expect different absolute numbers on Windows, network shares, and cold caches, but the ranking should be the same, because -Filter skips non-matching files before PowerShell creates objects for them.

The pattern I use now

One name pattern → -Filter, plus -File or -Directory:

Get-ChildItem -Path ./logs -Recurse -File -Filter *.log 

Several patterns or [a-z] ranges → -Include with -Recurse or a \* path. When speed matters, narrow first with -Filter and refine with -Include. The two can be combined:

Get-ChildItem -Path ./logs -Recurse -File -Filter *.log -Include 'a*', 'w*' | ForEach-Object Name 
app.log web.log 

Anything based on size, age, or path → Where-Object after a -Filter:

(Get-Item ./logs/app.log).LastWriteTime = (Get-Date).AddDays(-40) Set-Content ./logs/web.log -Value ('x' * 2048) $cutoff = (Get-Date).AddDays(-30) Get-ChildItem -Path ./logs -Recurse -File -Filter *.log | Where-Object { $_.Length -gt 1KB -or $_.LastWriteTime -lt $cutoff } | Select-Object Name, Length, @{ n = 'AgeDays'; e = { ((Get-Date) - $_.LastWriteTime).Days } } 
Name Length AgeDays ---- ------ ------- app.log 2 40 web.log 2049 0 

If you want to try these commands without setting up a lab folder, you can practice PowerShell file commands in a free browser terminal on CMD Master. It also covers CMD and Bash.

Quick reference

You want Use Watch out for
One pattern, fast -Filter *.log Single string, * and ? only; on 5.1, *.htm also matches .html
Several patterns -Include *.log, *.csv Needs -Recurse or path\*, otherwise 0 results
Remove matches -Exclude *.bak Compares names only, still descends into excluded folders
Files only / folders only -File / -Directory *.log* also matches a folder named old.log.d
Size, date, path logic Where-Object { ... } after the pipe Narrow with -Filter first
Names only ForEach-Object Name after the pipe -Name changes how -Include behaves

Have you run into any of these, especially the -Name difference, on Windows PowerShell 5.1? I'd like to compare results.