Honor -Recurse when Get-ChildItem uses -Name (#28078)

This commit is contained in:
Dongbo Wang authored and GitHub committed 2026-10-09 10:21:53 -07:00
1 parent 373f6a7b79
commit 5d871eb5e0
2 files changed
+160 -46

No files matched your search

@@ -1323,52 +1323,7 @@ namespace System.Management.Automation
try
{
// If we're recursing, do some path fixups to match user
// expectations, but only if the last part is a file and not a directory:
if (recurse && !path.EndsWith(Path.DirectorySeparatorChar) && !path.EndsWith(Path.AltDirectorySeparatorChar))
{
string childName = GetChildName(path, context);
// If -File or -Directory is specified and path is ended with '*', we should include the parent path as search path
bool isFileOrDirectoryPresent = false;
if (context.DynamicParameters is Microsoft.PowerShell.Commands.GetChildDynamicParameters dynParam)
{
isFileOrDirectoryPresent = dynParam.File.IsPresent || dynParam.Directory.IsPresent;
}
if (string.Equals(childName, "*", StringComparison.OrdinalIgnoreCase) && isFileOrDirectoryPresent)
{
string parentName = path.Substring(0, path.Length - childName.Length);
path = parentName;
}
// dir c:\tem* -include *.ps1 -rec => No change
if ((context.Include == null) || (context.Include.Count == 0))
{
// dir c:\tem* -rec => dir c:\ -include tem* -rec
// dir tem* -rec => dir -include tem* -rec
// dir temp -rec
// Should glob paths and files that match tem*, but then
// recurse into all subdirectories and do the same for
// those directories.
if (!string.IsNullOrEmpty(path) && !IsItemContainer(path))
{
if (!string.Equals(childName, "*", StringComparison.OrdinalIgnoreCase))
{
if (context.Include != null)
{
context.Include.Add(childName);
modifiedInclude = true;
}
}
string parentName = path.Substring(0, path.Length - childName.Length);
path = parentName;
}
}
}
modifiedInclude = NormalizePathForChildItemRecursion(ref path, recurse, context);
// Save the include and exclude filters so that we can ignore
// them when doing recursion
@@ -1529,6 +1484,63 @@ namespace System.Management.Automation
}
}
private bool NormalizePathForChildItemRecursion(
ref string path,
bool recurse,
CmdletProviderContext context)
{
bool modifiedInclude = false;
// If we're recursing, do some path fixups to match user
// expectations, but only if the last part is a file and not a directory:
if (recurse && !path.EndsWith(Path.DirectorySeparatorChar) && !path.EndsWith(Path.AltDirectorySeparatorChar))
{
string childName = GetChildName(path, context);
// If -File or -Directory is specified and path is ended with '*', we should include the parent path as search path
bool isFileOrDirectoryPresent = false;
if (context.DynamicParameters is Microsoft.PowerShell.Commands.GetChildDynamicParameters dynParam)
{
isFileOrDirectoryPresent = dynParam.File.IsPresent || dynParam.Directory.IsPresent;
}
if (string.Equals(childName, "*", StringComparison.OrdinalIgnoreCase) && isFileOrDirectoryPresent)
{
string parentName = path.Substring(0, path.Length - childName.Length);
path = parentName;
}
// dir c:\tem* -include *.ps1 -rec => No change
if ((context.Include == null) || (context.Include.Count == 0))
{
// dir c:\tem* -rec => dir c:\ -include tem* -rec
// dir tem* -rec => dir -include tem* -rec
// dir temp -rec
// Should glob paths and files that match tem*, but then
// recurse into all subdirectories and do the same for
// those directories.
if (!string.IsNullOrEmpty(path) && !IsItemContainer(path))
{
if (!string.Equals(childName, "*", StringComparison.OrdinalIgnoreCase))
{
if (context.Include != null)
{
context.Include.Add(childName);
modifiedInclude = true;
}
}
string parentName = path.Substring(0, path.Length - childName.Length);
path = parentName;
}
}
}
return modifiedInclude;
}
/// <summary>
/// Gets the child items of the item at the specified path.
/// </summary>
@@ -2229,6 +2241,10 @@ namespace System.Management.Automation
}
CmdletProviderContext context = new CmdletProviderContext(this.ExecutionContext);
context.SetFilters(
include: new Collection<string>(),
exclude: new Collection<string>(),
filter: null);
context.Force = force;
context.SuppressWildcardExpansion = literalPath;
@@ -2319,6 +2335,33 @@ namespace System.Management.Automation
throw PSTraceSource.NewArgumentNullException(nameof(path));
}
bool modifiedInclude = false;
try
{
if ((recurse && !context.SuppressWildcardExpansion) || LocationGlobber.ShouldPerformGlobbing(path, context))
{
modifiedInclude = NormalizePathForChildItemRecursion(ref path, recurse, context);
}
GetChildNamesCore(path, returnContainers, recurse, depth, context);
}
finally
{
if (modifiedInclude)
{
context.Include.Clear();
}
}
}
private void GetChildNamesCore(
string path,
ReturnContainers returnContainers,
bool recurse,
uint depth,
CmdletProviderContext context)
{
// Construct the include filter
Collection<WildcardPattern> includeMatcher =
@@ -142,6 +142,77 @@ Describe "Get-ChildItem" -Tags "CI" {
(Get-ChildItem -Path $TestDrive -Depth 1 -Exclude $item_a).Count | Should -Be 7
}
It "Should return names recursively when the path has a wildcard leaf" {
$root = New-Item -Path $TestDrive -Name "NameRecurse" -ItemType Directory
try {
$child = New-Item -Path $root -Name "Child" -ItemType Directory
$null = New-Item -Path $root -Name "root.md" -ItemType File
$null = New-Item -Path $child -Name "child.md" -ItemType File
$null = New-Item -Path $child -Name "ignored.txt" -ItemType File
$path = Join-Path $root "*.md"
$result = Get-ChildItem -Path $path -Recurse -File -Name
$result | Should -HaveCount 2
$result | Should -Contain "root.md"
$result | Should -Contain "Child$([IO.Path]::DirectorySeparatorChar)child.md"
Get-ChildItem -Path $path -Depth 0 -File -Name | Should -BeExactly "root.md"
}
finally {
Remove-Item -LiteralPath $root -Recurse -Force
}
}
It "Should preserve recursive wildcard filters through the public GetNames API" {
$root = New-Item -Path $TestDrive -Name "PublicGetNamesRecurse" -ItemType Directory
try {
$child = New-Item -Path $root -Name "Child" -ItemType Directory
$null = New-Item -Path $root -Name "root.md" -ItemType File
$null = New-Item -Path $child -Name "child.md" -ItemType File
$null = New-Item -Path $child -Name "ignored.txt" -ItemType File
$path = Join-Path $root "*.md"
$result = $ExecutionContext.SessionState.InvokeProvider.ChildItem.GetNames(
$path,
[System.Management.Automation.ReturnContainers]::ReturnMatchingContainers,
$true)
$result | Should -HaveCount 2
$result | Should -Contain "root.md"
$result | Should -Contain "Child$([IO.Path]::DirectorySeparatorChar)child.md"
}
finally {
Remove-Item -LiteralPath $root -Recurse -Force
}
}
It "Should resolve recursive leaf paths from the current directory" {
$root = New-Item -Path $TestDrive -Name "CurrentDirectoryRecurse" -ItemType Directory
try {
$child = New-Item -Path $root -Name "Child" -ItemType Directory
$rootFile = New-Item -Path $root -Name "root.md" -ItemType File
$childFile = New-Item -Path $child -Name "child.md" -ItemType File
Push-Location $root
try {
$result = Get-ChildItem *.md -Recurse -File -Name
$result | Should -HaveCount 2
$result | Should -Contain $rootFile.Name
$result | Should -Contain "Child$([IO.Path]::DirectorySeparatorChar)$($childFile.Name)"
$result = Get-ChildItem child.md -Recurse -File -Name
$result | Should -HaveCount 1
$result | Should -BeExactly "Child$([IO.Path]::DirectorySeparatorChar)$($childFile.Name)"
}
finally {
Pop-Location
}
}
finally {
Remove-Item -LiteralPath $root -Recurse -Force
}
}
It "Should return items recursively when using 'Include' or 'Exclude' parameters with -LiteralPath" {
(Get-ChildItem -LiteralPath $TestDrive -Recurse -Exclude *).Count | Should -Be 0
(Get-ChildItem -LiteralPath $TestDrive -Recurse -Include *.dll).Count | Should -Be (Get-ChildItem $TestDrive -Recurse -Include *.dll).Count