Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 33 additions & 46 deletions src/System.Management.Automation/engine/CommandDiscovery.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1199,81 +1199,68 @@ internal void UnregisterLookupCommandInfoAction(string currentAction, string com
/// </remarks>
internal LookupPathCollection GetLookupDirectoryPaths()
{
LookupPathCollection result = new LookupPathCollection();

string path = Environment.GetEnvironmentVariable("PATH");
discoveryTracer.WriteLine("PATH: {0}", path);

discoveryTracer.WriteLine(
"PATH: {0}",
path);

bool isPathCacheValid =
path != null &&
string.Equals(_pathCacheKey, path, StringComparison.OrdinalIgnoreCase) &&
_cachedPath != null;
bool isPathCacheValid = _cachedLookupPaths is not null
&& string.Equals(_pathCacheKey, path, StringComparison.OrdinalIgnoreCase);

if (!isPathCacheValid)
{
// Reset the cached lookup paths
_cachedLookupPaths = null;

// Tokenize the path and cache it

_pathCacheKey = path;
_cachedLookupPaths = null;

if (_pathCacheKey != null)
if (string.IsNullOrEmpty(path))
{
// Cache an empty collection when PATH is null (unset) or an empty string.
_cachedLookupPaths = new List<string>();
}
else
{
// Tokenize the path and cache it
string[] tokenizedPath = _pathCacheKey.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
_cachedPath = new Collection<string>();
_cachedLookupPaths = new List<string>(capacity: tokenizedPath.Length);

foreach (string directory in tokenizedPath)
{
string tempDir = directory.TrimStart();
if (tempDir.EqualsOrdinalIgnoreCase("~"))
{
tempDir = Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify);
}
else if (tempDir.StartsWith("~" + Path.DirectorySeparatorChar))
string tempDir = directory.Trim();
if (tempDir.StartsWith('~'))
{
tempDir = Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify)
+ Path.DirectorySeparatorChar
+ tempDir.Substring(2);
if (tempDir.Length is 1)
{
tempDir = Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify);
}
else if (tempDir[1] == Path.DirectorySeparatorChar)
{
string homeDir = Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify);
tempDir = $"{homeDir}{Path.DirectorySeparatorChar}{tempDir.AsSpan(2)}";
}
}

_cachedPath.Add(tempDir);
result.Add(tempDir);
_cachedLookupPaths.Add(tempDir);
}
}
}
else
{
result.AddRange(_cachedPath);
}

// Cache the new lookup paths
return _cachedLookupPaths ??= result;
// 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);
}

/// <summary>
/// The cached list of lookup paths. It can be invalidated by
/// the PATH changing.
/// The cached list of lookup paths. It can be invalidated by the PATH changing.
/// </summary>
private LookupPathCollection _cachedLookupPaths;
private List<string> _cachedLookupPaths;

/// <summary>
/// The key that determines if the cached PATH can be used.
/// </summary>
private string _pathCacheKey;

/// <summary>
/// The cache of the tokenized PATH directories.
/// </summary>
private Collection<string> _cachedPath;

#endregion internal members

#region environment variable helpers
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -275,3 +275,54 @@ Describe "Get-Command Tests" -Tags "CI" {
$result | Should -Be $null
}
}

Describe "Test relative path in PATH env var" -Tags "CI" {

BeforeAll {
$originalPath = $env:PATH
$originalLocation = Get-Location

$subDir1 = Join-Path $TestDrive -ChildPath "subdir1"
$subDir2 = Join-Path $TestDrive -ChildPath "subdir2"
$toolsUnderSubDir1 = Join-Path $subDir1 -ChildPath "tools"
$toolsUnderSubDir2 = Join-Path $subDir2 -ChildPath "tools"

$null = New-Item -Path $subDir1 -ItemType Directory -Force
$null = New-Item -Path $subDir2 -ItemType Directory -Force
$null = New-Item -Path $toolsUnderSubDir1 -ItemType Directory -Force
$null = New-Item -Path $toolsUnderSubDir2 -ItemType Directory -Force

$helloScript = Join-Path $toolsUnderSubDir1 -ChildPath "hello.ps1"
$byeScript = Join-Path $toolsUnderSubDir2 -ChildPath "bye.ps1"
$null = New-Item -Path $helloScript -ItemType File -Force
$null = New-Item -Path $byeScript -ItemType File -Force
}

AfterAll {
Set-Location $originalLocation
$env:PATH = $originalPath
}

It "Get-Command should resolve relative path in PATH env var based on user's current working directory" {
$dirSep = [System.IO.Path]::DirectorySeparatorChar
$pathSep = [System.IO.Path]::PathSeparator

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

$result = Get-Command "hello.ps1" -ErrorAction silentlycontinue
$result | Should -Not -BeNullOrEmpty -Because "CWD is $subDir1, so '.${dirSep}tools' in PATH should be resolved to '$toolsUnderSubDir1', which contains 'hello.ps1'"
$result.Path | Should -BeExactly $helloScript

$result = Get-Command "bye.ps1" -ErrorAction silentlycontinue
$result | Should -BeNullOrEmpty -Because "'bye.ps1' is not in '$toolsUnderSubDir1'"

Set-Location $subDir2
$result = Get-Command "hello.ps1" -ErrorAction silentlycontinue
$result | Should -BeNullOrEmpty -Because "CWD is $subDir2, so '.${dirSep}tools' in PATH should be resolved to '$toolsUnderSubDir2', which contains 'bye.ps1'"

$result = Get-Command "bye.ps1" -ErrorAction silentlycontinue
$result | Should -Not -BeNullOrEmpty
$result.Path | Should -BeExactly $byeScript
}
}
Loading