diff --git a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
index daf69d50251..58938422814 100644
--- a/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
+++ b/src/Microsoft.PowerShell.ConsoleHost/host/msh/ConsoleHost.cs
@@ -118,33 +118,6 @@ internal static int Start(
throw new ConsoleHostStartupException(ConsoleHostStrings.ShellCannotBeStartedWithConfigConflict);
}
- // Put PSHOME in front of PATH so that calling `pwsh` within `pwsh` always starts the same running version.
- string path = Environment.GetEnvironmentVariable("PATH");
- string pshome = Utils.DefaultPowerShellAppBase;
- string dotnetToolsPathSegment = $"{Path.DirectorySeparatorChar}.store{Path.DirectorySeparatorChar}powershell{Path.DirectorySeparatorChar}";
-
- int index = pshome.IndexOf(dotnetToolsPathSegment, StringComparison.Ordinal);
- if (index > 0)
- {
- // We're running PowerShell global tool. In this case the real entry executable should be the 'pwsh'
- // or 'pwsh.exe' within the tool folder which should be the path right before the '\.store', not what
- // PSHome is pointing to.
- pshome = pshome[0..index];
- }
-
- pshome += Path.PathSeparator;
-
- // To not impact startup perf, we don't remove duplicates, but we avoid adding a duplicate to the front
- // we also don't handle the edge case where PATH only contains $PSHOME
- if (string.IsNullOrEmpty(path))
- {
- Environment.SetEnvironmentVariable("PATH", pshome);
- }
- else if (!path.StartsWith(pshome, StringComparison.Ordinal))
- {
- Environment.SetEnvironmentVariable("PATH", pshome + path);
- }
-
try
{
string profileDir = Platform.CacheDirectory;
diff --git a/src/System.Management.Automation/engine/CommandDiscovery.cs b/src/System.Management.Automation/engine/CommandDiscovery.cs
index 9ca87f4c834..20c2fa6de4f 100644
--- a/src/System.Management.Automation/engine/CommandDiscovery.cs
+++ b/src/System.Management.Automation/engine/CommandDiscovery.cs
@@ -126,6 +126,53 @@ internal class CommandDiscovery
#region ctor
+ ///
+ /// The directory path where the currently running pwsh executable is located.
+ ///
+ private static readonly string _psExeHome;
+
+ static CommandDiscovery()
+ {
+#if UNIX
+ string pwshName = "pwsh";
+#else
+ string pwshName = "pwsh.exe";
+#endif
+
+ string processPath = Environment.ProcessPath;
+ if (pwshName.Equals(Path.GetFileName(processPath), StringComparison.Ordinal))
+ {
+ // Use 'Environment.ProcessPath' if it points to 'pwsh.exe' or 'pwsh'.
+ _psExeHome = Path.GetDirectoryName(processPath);
+ return;
+ }
+
+ // We need to handle 3 cases here:
+ // - We are running from PowerShell dotnet global tool ('ProcessPath' points to 'dotnet' in this case).
+ // - We are running from an application that hosts PowerShell via NuGet packages.
+ // - Rare case where we are running from pwsh, but 'ProcessPath' somehow doesn't point to it.
+ _psExeHome = Utils.DefaultPowerShellAppBase;
+
+ // Check if the pwsh executable exists in pshome.
+ string exePath = Path.Combine(_psExeHome, pwshName);
+ if (!File.Exists(exePath))
+ {
+ // This means PowerShell is being hosted via NuGet packages.
+ _psExeHome = null;
+ return;
+ }
+
+ string dotnetToolPathSegment = string.Format("{0}.store{0}powershell{0}", Path.DirectorySeparatorChar);
+ int index = _psExeHome.IndexOf(dotnetToolPathSegment, StringComparison.Ordinal);
+ if (index > 0)
+ {
+ // We're running PowerShell global tool. In this case the real entry executable should be the 'pwsh'
+ // or 'pwsh.exe' within the tool folder which should be the path right before the '\.store', because
+ // the pwsh executable under $PSHOME is an x86-64 binary that won't work on non-x86/64 platforms.
+ _psExeHome = _psExeHome[0..index];
+ }
+ }
+
///
/// Default constructor...
///
@@ -1199,63 +1246,86 @@ internal void UnregisterLookupCommandInfoAction(string currentAction, string com
///
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
+ _pathCacheKey = null;
_cachedLookupPaths = null;
- // Tokenize the path and cache it
-
- _pathCacheKey = path;
+ bool isPwshExe = _psExeHome is not null;
+ discoveryTracer.WriteLine(isPwshExe
+ ? "PWSH scenario: Prepend PWSH-EXE home directory to searching paths."
+ : "NuGet Package scenario: Use PATH as is.");
- if (_pathCacheKey != null)
+ if (path is null)
{
+ // Cache an empty collection when PATH is null (unset).
+ _cachedLookupPaths = new LookupPathCollection();
+ if (isPwshExe)
+ {
+ // Add '_psExeHome' when we are executing from a pwsh executable.
+ _cachedLookupPaths.Add(_psExeHome);
+ }
+ }
+ else
+ {
+ // Tokenize the path and cache it
+ _pathCacheKey = path;
string[] tokenizedPath = _pathCacheKey.Split(Path.PathSeparator, StringSplitOptions.RemoveEmptyEntries);
- _cachedPath = new Collection();
+
+ List pathList = new(capacity: tokenizedPath.Length + 1);
+ ReadOnlySpan psHomeSpan = default;
+
+ if (isPwshExe)
+ {
+ // Add '_psExeHome' to the front of the path list so that calling `pwsh` within `pwsh` always starts the same running version.
+ pathList.Add(_psExeHome);
+ psHomeSpan = _psExeHome.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
+ }
foreach (string directory in tokenizedPath)
{
- string tempDir = directory.TrimStart();
- if (tempDir.EqualsOrdinalIgnoreCase("~"))
+ string tempDir = directory.Trim();
+ if (tempDir.StartsWith('~'))
{
- tempDir = Environment.GetFolderPath(
+ string homeDir = Environment.GetFolderPath(
Environment.SpecialFolder.UserProfile,
Environment.SpecialFolderOption.DoNotVerify);
+
+ if (tempDir.Length is 1)
+ {
+ tempDir = homeDir;
+ }
+ else if (tempDir[1] == Path.DirectorySeparatorChar)
+ {
+ tempDir = $"{homeDir}{Path.DirectorySeparatorChar}{tempDir.Substring(2)}";
+ }
}
- else if (tempDir.StartsWith("~" + Path.DirectorySeparatorChar))
+
+ // Skip the duplicate path if it is the same as '_psExeHome' and we are running from a pwsh executable.
+ if (isPwshExe && tempDir.Length >= psHomeSpan.Length)
{
- tempDir = Environment.GetFolderPath(
- Environment.SpecialFolder.UserProfile,
- Environment.SpecialFolderOption.DoNotVerify)
- + Path.DirectorySeparatorChar
- + tempDir.Substring(2);
+ ReadOnlySpan tempDirSpan = tempDir.AsSpan().TrimEnd(Path.DirectorySeparatorChar);
+ if (psHomeSpan.Equals(tempDirSpan, StringComparison.OrdinalIgnoreCase))
+ {
+ continue;
+ }
}
- _cachedPath.Add(tempDir);
- result.Add(tempDir);
+ pathList.Add(tempDir);
}
+
+ // Cache the new lookup paths.
+ _cachedLookupPaths = new LookupPathCollection(pathList);
}
}
- else
- {
- result.AddRange(_cachedPath);
- }
- // Cache the new lookup paths
- return _cachedLookupPaths ??= result;
+ return _cachedLookupPaths;
}
///
@@ -1269,11 +1339,6 @@ internal LookupPathCollection GetLookupDirectoryPaths()
///
private string _pathCacheKey;
- ///
- /// The cache of the tokenized PATH directories.
- ///
- private Collection _cachedPath;
-
#endregion internal members
#region environment variable helpers
diff --git a/test/powershell/Host/ConsoleHost.Tests.ps1 b/test/powershell/Host/ConsoleHost.Tests.ps1
index 735c0a39682..0e56d58efbf 100644
--- a/test/powershell/Host/ConsoleHost.Tests.ps1
+++ b/test/powershell/Host/ConsoleHost.Tests.ps1
@@ -745,8 +745,8 @@ $powershell -c '[System.Management.Automation.Platform]::SelectProductNameForDir
}
Context "PATH environment variable" {
- It "`$PSHOME should be in front so that pwsh.exe starts current running PowerShell" {
- & $powershell -v | Should -Match $PSVersionTable.GitCommitId
+ It "Running 'pwsh' should start the currently running PowerShell" {
+ pwsh -v | Should -Match $PSVersionTable.GitCommitId
}
It "powershell starts if PATH is not set" -Skip:($IsWindows) {
@@ -1191,10 +1191,15 @@ Describe 'Pwsh startup and PATH' -Tag CI {
}
It 'pwsh starts even if PATH is not defined' {
- $pwsh = Join-Path -Path $PSHOME -ChildPath "pwsh"
Remove-Item Env:\PATH
- $path = & $pwsh -noprofile -command '$env:PATH'
- $path | Should -BeExactly ($PSHOME + [System.IO.Path]::PathSeparator)
+
+ $version = pwsh -v
+ $version | Should -BeExactly "PowerShell $($PSVersionTable.GitCommitId)"
+ }
+
+ It 'pwsh should not alter the PATH environment variable during startup' {
+ Remove-Item Env:\PATH
+ pwsh -noprofile -command '$null -eq $env:PATH' | Should -BeExactly 'True'
}
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1
index 65dd74e1b94..fcd313f0541 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Start-Process.Tests.ps1
@@ -221,9 +221,9 @@ Describe "Environment Tests" -Tags "Feature" {
($out | Where-Object { $_.Name -eq 'TERM' }).Value | Should -BeExactly 'dumb'
$pathSeparator = [System.IO.Path]::PathSeparator
if ($IsWindows) {
- ($out | Where-Object { $_.Name -eq 'PATH' }).Value | Should -BeLike "*${pathSeparator}mine${pathSeparator}*"
+ ($out | Where-Object { $_.Name -eq 'PATH' }).Value | Should -BeLike "mine${pathSeparator}*"
} else {
- ($out | Where-Object { $_.Name -eq 'PATH' }).Value | Should -BeLike "*${pathSeparator}mine"
+ ($out | Where-Object { $_.Name -eq 'PATH' }).Value | Should -BeLike "mine"
}
}