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),
*.xlsalso returnsbook.xlsx, so*.htmalso 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 *.htmreturned onlyreport.htm. -
8.3 short names are matched too. A pattern like
*1*.txtcan returnlongfilename.txtbecause its short name isLONGFI~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:
-
-Filterwas about 8× faster than-Includefor the same 4,000 files. -
-Includewas slower thanGet-ChildItem | Where-Object. You might expect a built-in parameter to beat a pipeline filter. Here it took twice as long. -
Running
-Filtertwice beat one-Includewith 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.
Top comments (0)