Skip to content

Fix PATH caching in CommandDiscovery code - #27809

Open
Dongbo Wang (daxian-dbw) wants to merge 3 commits into
PowerShell:masterfrom
daxian-dbw:caching-fix
Open

Fix PATH caching in CommandDiscovery code#27809
Dongbo Wang (daxian-dbw) wants to merge 3 commits into
PowerShell:masterfrom
daxian-dbw:caching-fix

Conversation

@daxian-dbw

Copy link
Copy Markdown
Member

PR Summary

The method GetLookupDirectoryPaths always returns the cached instance of path collection. However, the returned collection gets mutated in CommandPathSearch.ResolveCurrentDirectoryInLookupPaths to resolve the relative paths such as .\tools based on the user's current working directory $PWD, so for example, .\tools gets replaced with cwd-1\tools in the cached instance. Then, when the user changes to a different working directory cwd-2, command discovery won't find the executable or ps1 script under cwd-2/tools as expected, but those executables under cwd-1\tools will always be discoverable no matter what the $PWD is. That behavior is incorrect.

This pull request improves the caching logic of GetLookupDirectoryPaths and makes it return a copy of the path collection, so the mutation happens to the returned copy, and the cached instance is kept intact. So, for every command search, the CommandPathSearch will resolve relative paths against the $PWD that the user is located at that time.

Core engine improvements:

  • Refactored GetLookupDirectoryPaths in CommandDiscovery.cs to ensure that relative paths in PATH are resolved based on the current working directory by returning a copy of the cached lookup paths, preventing mutation of the cache during command resolution.
  • Simplified and corrected the handling of the PATH cache, removing redundant fields and ensuring that the cache is invalidated and rebuilt properly when the environment variable changes.

Testing enhancements:

  • Added a new Pester test in Get-Command.Tests.ps1 that verifies Get-Command correctly resolves relative paths in PATH depending on the current working directory, covering scenarios where scripts exist in different directories.

PR Checklist

Copilot AI lite review requested due to automatic review settings August 11, 2026 03:06
@daxian-dbw
Dongbo Wang (daxian-dbw) requested a review from a team as a code owner August 11, 2026 03:06
@azure-pipelines

Copy link
Copy Markdown
Azure Pipelines:
There may be pipelines that require an authorized user to comment /azp run to run.

@daxian-dbw Dongbo Wang (daxian-dbw) added the CL-Engine Indicates that a PR should be marked as an engine change in the Change Log label Aug 11, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR fixes an engine-level PATH caching bug in CommandDiscovery where cached lookup paths could be mutated during relative-path resolution, causing subsequent command discovery to resolve against an old working directory. It also adds a regression test to validate that Get-Command resolves relative PATH entries based on the current $PWD.

Changes:

  • Updated CommandDiscovery.GetLookupDirectoryPaths() to return a fresh LookupPathCollection copy each time, preventing cache mutation during relative path resolution.
  • Simplified PATH cache storage by caching tokenized entries as a List<string> and rebuilding when PATH changes/unsets.
  • Added a Pester test covering relative PATH resolution across directory changes.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 2 comments.

File Description
src/System.Management.Automation/engine/CommandDiscovery.cs Fixes PATH lookup caching by returning a copy per lookup to avoid cache mutation when resolving relative entries.
test/powershell/Modules/Microsoft.PowerShell.Core/Get-Command.Tests.ps1 Adds regression coverage for resolving relative PATH entries based on the current working directory.

💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.

Comment thread src/System.Management.Automation/engine/CommandDiscovery.cs Outdated

@iSazonov Ilya (iSazonov) left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM with minor comments.

_pathCacheKey = path;

if (_pathCacheKey != null)
if (path is null)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For empty path we get the same empty collection _cachedLookupPaths = new List<string>()

Suggested change
if (path is null)
if (string.IsNullorEmpty(path))

And line 1210: _pathCacheKey = path;

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. Updated as suggested, please take another look.

Comment thread test/powershell/Modules/Microsoft.PowerShell.Core/Get-Command.Tests.ps1 Outdated
Comment thread test/powershell/Modules/Microsoft.PowerShell.Core/Get-Command.Tests.ps1 Outdated
Comment thread test/powershell/Modules/Microsoft.PowerShell.Core/Get-Command.Tests.ps1 Outdated
Comment thread test/powershell/Modules/Microsoft.PowerShell.Core/Get-Command.Tests.ps1 Outdated

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 2 out of 2 changed files in this pull request and generated no new comments.

Suppressed comments (3)

test/powershell/Modules/Microsoft.PowerShell.Core/Get-Command.Tests.ps1:311

  • This test prepends the relative entry to the existing PATH (...${pathSep}$env:PATH). That means bye.ps1 could be found later in the original PATH on some dev machines/CI images, making the Should -BeNullOrEmpty assertion flaky. Consider isolating PATH to just the relative entry for the duration of this test.
        $dirSep = [System.IO.Path]::DirectorySeparatorChar
        $pathSep = [System.IO.Path]::PathSeparator

        Set-Location $subDir1
        $env:PATH = ".${dirSep}tools${pathSep}$env:PATH"

src/System.Management.Automation/engine/CommandDiscovery.cs:1215

  • The comment says this branch is for when PATH is null, but the condition is string.IsNullOrEmpty(path) (null or empty). Please update the comment so it matches behavior.
                    // Cache an empty collection when PATH is null (unset).

src/System.Management.Automation/engine/CommandDiscovery.cs:1251

  • GetLookupDirectoryPaths() now allocates a new LookupPathCollection on every call, and LookupPathCollection(IEnumerable<string>) adds items via Add(), which does an O(n) Contains() check each time. That makes copying O(n^2) per command lookup (even when PATH is unchanged), which may regress command discovery performance for large PATH values. Consider adding a fast-copy path (e.g., an internal ctor/Clone that copies the backing list without duplicate checks, since the cached list can already be de-duplicated when built).
            // The returned instance will be mutated in 'CommandPathSearch.ResolveCurrentDirectoryInLookupPaths' when resolving relative paths,
            // which depends on user's current working directory. So, we need to return a copy of the lookup paths to keep the cache intact.
            return new LookupPathCollection(_cachedLookupPaths);

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CL-Engine Indicates that a PR should be marked as an engine change in the Change Log

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants