From 5d871eb5e02cbcc8ecb03fce4b24d7f48bf97c26 Mon Sep 17 00:00:00 2001 From: Dongbo Wang Date: Fri, 9 Oct 2026 10:21:53 -0700 Subject: [PATCH] Honor `-Recurse` when `Get-ChildItem` uses `-Name` (#28078) --- .../engine/SessionStateContainer.cs | 135 ++++++++++++------ .../Get-ChildItem.Tests.ps1 | 71 +++++++++ 2 files changed, 160 insertions(+), 46 deletions(-) diff --git a/src/System.Management.Automation/engine/SessionStateContainer.cs b/src/System.Management.Automation/engine/SessionStateContainer.cs index fa8884b92..8749105a1 100644 --- a/src/System.Management.Automation/engine/SessionStateContainer.cs +++ b/src/System.Management.Automation/engine/SessionStateContainer.cs @@ -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; + } + /// /// Gets the child items of the item at the specified path. /// @@ -2229,6 +2241,10 @@ namespace System.Management.Automation } CmdletProviderContext context = new CmdletProviderContext(this.ExecutionContext); + context.SetFilters( + include: new Collection(), + exclude: new Collection(), + 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 includeMatcher = diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 index 4c7fdfa52..e43ef3b62 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-ChildItem.Tests.ps1 @@ -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