diff --git a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
index 4116060625..9a0511a93c 100644
--- a/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
+++ b/src/System.Management.Automation/engine/Modules/ModuleIntrinsics.cs
@@ -12,6 +12,7 @@ using System.Text;
using System.Threading;
using Microsoft.PowerShell.Commands;
+using Microsoft.Win32;
using Dbg = System.Management.Automation.Diagnostics;
@@ -1162,92 +1163,6 @@ namespace System.Management.Automation
return result.ToString();
}
- ///
- /// Check if the current powershell is likely running in following scenarios:
- /// - PSCore started on windows [machine-wide env:PSModulePath will influence]
- /// - PSCore started from full ps
- /// - PSCore started from inbox nano/iot ps
- /// If it's likely one of them, then we need to clear the current process module path.
- ///
- private static bool NeedToClearProcessModulePath(string currentProcessModulePath, string personalModulePath, string sharedModulePath)
- {
-#if UNIX
- return false;
-#else
- Dbg.Assert(!string.IsNullOrEmpty(personalModulePath), "caller makes sure personalModulePath not null or empty");
- Dbg.Assert(sharedModulePath != null, "caller makes sure sharedModulePath is not null");
-
- const string winSxSModuleDirectory = @"PowerShell\Modules";
- const string winLegacyModuleDirectory = @"WindowsPowerShell\Modules";
-
- // The machine-wide and user-wide environment variables are only meaningful for full ps,
- // so if the current process module path contains any of them, it's likely that the sxs
- // ps was started directly on windows, or from full ps. The same goes for the legacy personal
- // and shared module paths.
- string hklmModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.Machine);
- string hkcuModulePath = GetExpandedEnvironmentVariable(Constants.PSModulePathEnvVar, EnvironmentVariableTarget.User);
- string legacyPersonalModulePath = personalModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory);
- string legacyProgramFilesModulePath = sharedModulePath.Replace(winSxSModuleDirectory, winLegacyModuleDirectory);
-
- return (!string.IsNullOrEmpty(hklmModulePath) && currentProcessModulePath.IndexOf(hklmModulePath, StringComparison.OrdinalIgnoreCase) != -1) ||
- (!string.IsNullOrEmpty(hkcuModulePath) && currentProcessModulePath.IndexOf(hkcuModulePath, StringComparison.OrdinalIgnoreCase) != -1) ||
- currentProcessModulePath.IndexOf(legacyPersonalModulePath, StringComparison.OrdinalIgnoreCase) != -1 ||
- currentProcessModulePath.IndexOf(legacyProgramFilesModulePath, StringComparison.OrdinalIgnoreCase) != -1;
-#endif
- }
-
- ///
- /// When sxs ps instance B got started from sxs ps instance A, A's pshome module path might
- /// show up in current process module path. It doesn't make sense for B to load modules from
- /// A's pshome module path, so remove it in such case.
- ///
- private static string RemoveSxSPsHomeModulePath(string currentProcessModulePath, string personalModulePath, string sharedModulePath, string psHomeModulePath)
- {
-#if UNIX
- const string powershellExeName = "pwsh";
-#else
- const string powershellExeName = "pwsh.exe";
-#endif
- const string powershellDepsName = "pwsh.deps.json";
-
- StringBuilder modulePathString = new StringBuilder(currentProcessModulePath.Length);
- char[] invalidPathChars = Path.GetInvalidPathChars();
-
- foreach (var path in currentProcessModulePath.Split(Utils.Separators.PathSeparator, StringSplitOptions.RemoveEmptyEntries))
- {
- string trimedPath = path.Trim().TrimEnd(Path.DirectorySeparatorChar);
- if (trimedPath.IndexOfAny(invalidPathChars) != -1 || !Path.IsPathRooted(trimedPath))
- {
- // Path contains invalid characters or it's not an absolute path. Ignore it.
- continue;
- }
-
- if (!trimedPath.Equals(personalModulePath, StringComparison.OrdinalIgnoreCase) &&
- !trimedPath.Equals(sharedModulePath, StringComparison.OrdinalIgnoreCase) &&
- !trimedPath.Equals(psHomeModulePath, StringComparison.OrdinalIgnoreCase) &&
- trimedPath.EndsWith("Modules", StringComparison.OrdinalIgnoreCase))
- {
- string parentDir = Path.GetDirectoryName(trimedPath);
- string psExePath = Path.Combine(parentDir, powershellExeName);
- string psDepsPath = Path.Combine(parentDir, powershellDepsName);
- if ((File.Exists(psExePath) && File.Exists(psDepsPath)))
- {
- // Path is a PSHome module path from a different PowerShell instance. Ignore it.
- continue;
- }
- }
-
- if (modulePathString.Length > 0)
- {
- modulePathString.Append(Path.PathSeparator);
- }
-
- modulePathString.Append(trimedPath);
- }
-
- return modulePathString.ToString();
- }
-
///
/// Checks the various PSModulePath environment string and returns PSModulePath string as appropriate. Note - because these
/// strings go through the provider, we need to escape any wildcards before passing them
@@ -1259,16 +1174,6 @@ namespace System.Management.Automation
string sharedModulePath = GetSharedModulePath(); // aka location
string psHomeModulePath = GetPSHomeModulePath(); // $PSHome\Modules location
- if (!string.IsNullOrEmpty(currentProcessModulePath) &&
- NeedToClearProcessModulePath(currentProcessModulePath, personalModulePath, sharedModulePath))
- {
- // Clear the current process module path in the following cases
- // - start PSCore on windows [machine-wide env:PSModulePath will influence]
- // - start PSCore from full ps
- // - start PSCore from inbox nano/iot ps
- currentProcessModulePath = null;
- }
-
// If the variable isn't set, then set it to the default value
if (currentProcessModulePath == null) // EVT.Process does Not exist - really corner case
{
@@ -1291,39 +1196,30 @@ namespace System.Management.Automation
{
currentProcessModulePath += hklmMachineModulePath; // += EVT.Machine
}
+
+#if !UNIX
+ // Add Windows Modules path
+ currentProcessModulePath = $"{currentProcessModulePath}{Path.PathSeparator}{s_windowsPowerShellPSHomeModulePath}";
+#endif
}
// EVT.Process exists
// Now handle the case where the environment variable is already set.
else
{
- // When SxS PS instance A starts SxS PS instance B, A's PSHome module path might be inherited by B. We need to remove that path from B
- currentProcessModulePath = RemoveSxSPsHomeModulePath(currentProcessModulePath, personalModulePath, sharedModulePath, psHomeModulePath);
-
string personalModulePathToUse = string.IsNullOrEmpty(hkcuUserModulePath) ? personalModulePath : hkcuUserModulePath;
string systemModulePathToUse = string.IsNullOrEmpty(hklmMachineModulePath) ? psHomeModulePath : hklmMachineModulePath;
+ // Maintain order of the paths, but ahead of any existing paths:
+ // personalModulePath
+ // sharedModulePath
+ // systemModulePath
currentProcessModulePath = AddToPath(currentProcessModulePath, personalModulePathToUse, 0);
-
- int insertIndex = -1;
-#if !UNIX
- string windowsPowerShellModulePath = GetWindowsPowerShellPSHomeModulePath();
- // If the Windows PowerShell Module path is already present, insert the system module path
- // ($PSHOME/Modules) before it.
- insertIndex = PathContainsSubstring(currentProcessModulePath, windowsPowerShellModulePath);
-#endif
+ int insertIndex = PathContainsSubstring(currentProcessModulePath, personalModulePathToUse) + personalModulePathToUse.Length + 1;
+ currentProcessModulePath = AddToPath(currentProcessModulePath, sharedModulePath, insertIndex);
+ insertIndex = PathContainsSubstring(currentProcessModulePath, sharedModulePath) + sharedModulePath.Length + 1;
currentProcessModulePath = AddToPath(currentProcessModulePath, systemModulePathToUse, insertIndex);
}
- // if we reached this point - always add location to EVT.Process
- // everything below is the same behaviour as WMF 4 code
-
- // index of $PSHome\Modules in currentProcessModulePath
- int indexOfPSHomeModulePath = PathContainsSubstring(currentProcessModulePath, psHomeModulePath);
-
- // if $PSHome\Modules not found (psHomePosition == -1) - append location to the end;
- // if $PSHome\Modules IS found (psHomePosition >= 0) - insert location before $PSHome\Modules
- currentProcessModulePath = AddToPath(currentProcessModulePath, sharedModulePath, indexOfPSHomeModulePath);
-
return currentProcessModulePath;
}
@@ -1338,6 +1234,45 @@ namespace System.Management.Automation
return currentModulePath;
}
+#if !UNIX
+ ///
+ /// Returns a PSModulePath suiteable for Windows PowerShell by removing this PowerShell's specific
+ /// paths from current PSModulePath.
+ ///
+ ///
+ /// Returns appropriate PSModulePath for Windows PowerShell.
+ ///
+ internal static string GetWindowsPowerShellModulePath()
+ {
+ string currentModulePath = GetModulePath();
+
+ if (currentModulePath == null)
+ {
+ return null;
+ }
+
+ // PowerShell specific paths including if set in powershell.config.json file we want to exclude
+ var excludeModulePaths = new HashSet {
+ GetPersonalModulePath(),
+ GetSharedModulePath(),
+ GetPSHomeModulePath(),
+ PowerShellConfig.Instance.GetModulePath(ConfigScope.AllUsers),
+ PowerShellConfig.Instance.GetModulePath(ConfigScope.CurrentUser)
+ };
+
+ var modulePathList = new List();
+ foreach (var path in currentModulePath.Split(';'))
+ {
+ if (!excludeModulePaths.Contains(path))
+ {
+ modulePathList.Add(path);
+ }
+ }
+
+ return string.Join(Path.PathSeparator, modulePathList);
+ }
+#endif
+
///
/// Checks if $env:PSModulePath is not set and sets it as appropriate. Note - because these
/// strings go through the provider, we need to escape any wildcards before passing them
@@ -1353,17 +1288,6 @@ namespace System.Management.Automation
if (!string.IsNullOrEmpty(newModulePathString))
{
-#if !UNIX
- // If on Windows, we want to add the System32 Windows PowerShell module directory
- // so that Windows modules are discoverable
- string windowsPowerShellModulePath = GetWindowsPowerShellPSHomeModulePath();
- if (!newModulePathString.Contains(windowsPowerShellModulePath, StringComparison.OrdinalIgnoreCase))
- {
- newModulePathString += Path.PathSeparator + windowsPowerShellModulePath;
- }
-#endif
-
- // Set the environment variable...
Environment.SetEnvironmentVariable(Constants.PSModulePathEnvVar, newModulePathString);
}
diff --git a/src/System.Management.Automation/engine/NativeCommandProcessor.cs b/src/System.Management.Automation/engine/NativeCommandProcessor.cs
index 7a7ab4a3b8..c41cd9a055 100644
--- a/src/System.Management.Automation/engine/NativeCommandProcessor.cs
+++ b/src/System.Management.Automation/engine/NativeCommandProcessor.cs
@@ -409,6 +409,19 @@ namespace System.Management.Automation
// Get the start info for the process.
ProcessStartInfo startInfo = GetProcessStartInfo(redirectOutput, redirectError, redirectInput, soloCommand);
+#if !UNIX
+ string commandPath = this.Path.ToLowerInvariant();
+ if (commandPath.EndsWith("powershell.exe") || commandPath.EndsWith("powershell_ise.exe"))
+ {
+ // if starting Windows PowerShell, need to remove PowerShell specific segments of PSModulePath
+ string psmodulepath = ModuleIntrinsics.GetWindowsPowerShellModulePath();
+ startInfo.Environment["PSModulePath"] = psmodulepath;
+
+ // must set UseShellExecute to false if we modify the environment block
+ startInfo.UseShellExecute = false;
+ }
+#endif
+
if (this.Command.Context.CurrentPipelineStopping)
{
throw new PipelineStoppedException();
diff --git a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1 b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
index e1ed4e22c5..7285798830 100644
--- a/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
+++ b/test/powershell/Host/TabCompletion/TabCompletion.Tests.ps1
@@ -17,9 +17,10 @@ Describe "TabCompletion" -Tags CI {
}
It 'Should complete abbreviated function' {
- $res = (TabExpansion2 -inputScript 'pschrl' -cursorColumn 'pschr'.Length).CompletionMatches.CompletionText
+ function Test-AbbreviatedFunctionExpansion {}
+ $res = (TabExpansion2 -inputScript 't-afe' -cursorColumn 't-afe'.Length).CompletionMatches.CompletionText
$res.Count | Should -BeGreaterOrEqual 1
- $res | Should -BeExactly 'PSConsoleHostReadLine'
+ $res | Should -BeExactly 'Test-AbbreviatedFunctionExpansion'
}
It 'Should complete native exe' -Skip:(!$IsWindows) {
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1
index 7f29101044..518c476adf 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Core/Import-Module.Tests.ps1
@@ -32,7 +32,7 @@ Describe "Import-Module" -Tags "CI" {
}
It "should be able to load a module with a trailing directory separator: " -TestCases @(
- @{ modulePath = (Get-Module -ListAvailable $moduleName).ModuleBase + [System.IO.Path]::DirectorySeparatorChar; expectedName = $moduleName },
+ @{ modulePath = (Get-Module -ListAvailable $moduleName)[0].ModuleBase + [System.IO.Path]::DirectorySeparatorChar; expectedName = $moduleName },
@{ modulePath = Join-Path -Path $TestDrive -ChildPath "\Modules\TestModule\"; expectedName = "TestModule" }
) {
param( $modulePath, $expectedName )
@@ -41,7 +41,7 @@ Describe "Import-Module" -Tags "CI" {
}
It "should be able to add a module with using ModuleInfo switch" {
- $a = Get-Module -ListAvailable $moduleName
+ $a = (Get-Module -ListAvailable $moduleName)[0]
{ Import-Module -ModuleInfo $a } | Should -Not -Throw
(Get-Module -Name $moduleName).Name | Should -BeExactly $moduleName
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Security/certificateCommon.psm1 b/test/powershell/Modules/Microsoft.PowerShell.Security/certificateCommon.psm1
index e9a9d95a77..03dd6bb99c 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Security/certificateCommon.psm1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Security/certificateCommon.psm1
@@ -146,22 +146,15 @@ function Install-TestCertificates
# PKI module is not available for PowerShell, so we need to use Windows PowerShell to import the cert
$fullPowerShell = Join-Path "$env:SystemRoot" "System32\WindowsPowerShell\v1.0\powershell.exe"
- try {
- $modulePathCopy = $env:PSModulePath
- $env:PSModulePath = $null
-
- $command = @"
+ $command = @"
Import-PfxCertificate $script:certLocation -CertStoreLocation cert:\CurrentUser\My | ForEach-Object PSPath
Import-Certificate $script:badCertLocation -CertStoreLocation Cert:\CurrentUser\My | ForEach-Object PSPath
"@
- $certPaths = & $fullPowerShell -NoProfile -NonInteractive -Command $command
- $certPaths.Count | Should -Be 2 | Out-Null
+ $certPaths = & $fullPowerShell -NoProfile -NonInteractive -Command $command
+ $certPaths.Count | Should -Be 2 | Out-Null
- $script:importedCert = Get-ChildItem $certPaths[0]
- $script:testBadCert = Get-ChildItem $certPaths[1]
- } finally {
- $env:PSModulePath = $modulePathCopy
- }
+ $script:importedCert = Get-ChildItem $certPaths[0]
+ $script:testBadCert = Get-ChildItem $certPaths[1]
}
elseif($IsWindows)
{
diff --git a/test/powershell/engine/Module/ModulePath.Tests.ps1 b/test/powershell/engine/Module/ModulePath.Tests.ps1
index d038fe7f2c..c124a8ed17 100644
--- a/test/powershell/engine/Module/ModulePath.Tests.ps1
+++ b/test/powershell/engine/Module/ModulePath.Tests.ps1
@@ -3,7 +3,6 @@
Describe "SxS Module Path Basic Tests" -tags "CI" {
BeforeAll {
-
if ($IsWindows)
{
$powershell = "$PSHOME\pwsh.exe"
@@ -14,13 +13,22 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
}
$expectedUserPath = Join-Path -Path $HOME -ChildPath "Documents\$ProductName\Modules"
$expectedSharedPath = Join-Path -Path $env:ProgramFiles -ChildPath "$ProductName\Modules"
+ $userConfigPath = "~/Documents/powershell/powershell.config.json"
}
else
{
$powershell = "$PSHOME/pwsh"
$expectedUserPath = [System.Management.Automation.Platform]::SelectProductNameForDirectory("USER_MODULES")
$expectedSharedPath = [System.Management.Automation.Platform]::SelectProductNameForDirectory("SHARED_MODULES")
+ $userConfigPath = "~/.config/powershell/powershell.config.json"
}
+
+ $userConfigExists = $false
+ if (Test-Path $userConfigPath) {
+ $userConfigExists = $true
+ Copy-Item $userConfigPath "$userConfigPath.backup" -Force -ErrorAction Ignore
+ }
+
$expectedSystemPath = Join-Path -Path $PSHOME -ChildPath 'Modules'
# Skip these tests in cases when there is no 'pwsh' executable (e.g. when framework dependent PS package is used)
@@ -41,6 +49,15 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
New-Item -Path $fakePSHomeModuleDir -ItemType Directory > $null
}
+ AfterAll {
+ if ($userConfigExists) {
+ Move-Item "$userConfigPath.backup" $userConfigPath -Force -ErrorAction Ignore
+ }
+ else {
+ Remove-Item "$userConfigPath" -Force -ErrorAction Ignore
+ }
+ }
+
BeforeEach {
$originalModulePath = $env:PSModulePath
}
@@ -74,7 +91,7 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
}
}
- It "ignore pshome module path derived from a different PowerShell instance" -Skip:(!$IsCoreCLR -or $skipNoPwsh) {
+ It "Works with pshome module path derived from a different PowerShell instance" -Skip:(!$IsCoreCLR -or $skipNoPwsh) {
## Create 'powershell' and 'pwsh.deps.json' in the fake PSHome folder,
## so that the module path calculation logic would believe it's real.
@@ -88,21 +105,15 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
$newModulePath = & $powershell -nopro -c '$env:PSModulePath'
$paths = $newModulePath -split [System.IO.Path]::PathSeparator
- if ($IsWindows)
- {
- $paths.Count | Should -Be 4
- }
- else
- {
- $paths.Count | Should -Be 3
- }
+ $paths.Count | Should -Be 4
$paths[0] | Should -Be $expectedUserPath
$paths[1] | Should -Be $expectedSharedPath
$paths[2] | Should -Be $expectedSystemPath
+ $paths[3] | Should -Be $fakePSHomeModuleDir
if ($IsWindows)
{
- $paths[3].TrimEnd([System.IO.Path]::DirectorySeparatorChar) | Should -Be $expectedWindowsPowerShellPSHomePath
+ $expectedWindowsPowerShellPSHomePath | Should -Not -BeIn $paths
}
} finally {
@@ -120,15 +131,7 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
$env:PSModulePath = $fakePSHomeModuleDir, $customeModules -join ([System.IO.Path]::PathSeparator)
$newModulePath = & $powershell -nopro -c '$env:PSModulePath'
$paths = $newModulePath -split [System.IO.Path]::PathSeparator
-
- if ($IsWindows)
- {
- $paths.Count | Should -Be 6
- }
- else
- {
- $paths.Count | Should -Be 5
- }
+ $paths.Count | Should -Be 5
$paths -contains $fakePSHomeModuleDir | Should -BeTrue
$paths -contains $customeModules | Should -BeTrue
}
@@ -162,4 +165,24 @@ Describe "SxS Module Path Basic Tests" -tags "CI" {
$wpshIndex | Should -Not -Be -1
$pwshIndex | Should -BeLessThan $wpshIndex
}
+
+ It 'Windows PowerShell does not inherit PowerShell paths' -Skip:(!$IsWindows) {
+ $out = powershell.exe -noprofile -command '$env:PSModulePath'
+ $out | Should -Not -Contain $expectedUserPath
+ $out | Should -Not -Contain $expectedSharedPath
+ $out | Should -Not -Contain $expectedSystemPath
+ }
+
+ It 'Windows PowerShell inherits user added paths' -Skip:(!$IsWindows) {
+ $env:PSModulePath += ";myPath"
+ $out = powershell.exe -noprofile -command '$env:PSModulePath'
+ $out | Should -BeLike '*;myPath'
+ }
+
+ It 'Windows PowerShell does not inherit path defined in powershell.config.json' -Skip:(!$IsWindows) {
+ $userConfig = '{ "PSModulePath": "myUserPath" }'
+ Set-Content -Path $userConfigPath -Value $userConfig -Force
+ $out = pwsh -noprofile -command 'powershell.exe -noprofile -command $env:PSModulePath'
+ $out | Should -Not -BeLike 'myUserPath;*'
+ }
}