diff --git a/src/System.Management.Automation/namespaces/EnvironmentProvider.cs b/src/System.Management.Automation/namespaces/EnvironmentProvider.cs index fc5952b6459..8abb7fe9247 100644 --- a/src/System.Management.Automation/namespaces/EnvironmentProvider.cs +++ b/src/System.Management.Automation/namespaces/EnvironmentProvider.cs @@ -198,15 +198,29 @@ internal override IDictionary GetSessionStateTable() IDictionary environmentTable = Environment.GetEnvironmentVariables(); foreach (DictionaryEntry entry in environmentTable) { - // Windows only: duplicate key (variable name that differs only in case) - // NOTE: Even though this shouldn't happen, it can, e.g. when npm - // creates duplicate environment variables that differ only in case - - // see https://github.com/PowerShell/PowerShell/issues/6305. - // However, because retrieval *by name* later is invariably - // case-Insensitive, in effect only a *single* variable exists. - // We simply ask Environment.GetEnvironmentVariable() which value is - // the effective one, and use that. - providerTable.TryAdd((string)entry.Key, entry); + if (!providerTable.TryAdd((string)entry.Key, entry)) + { // Windows only: duplicate key (variable name that differs only in case) + // NOTE: Even though this shouldn't happen, it can, e.g. when npm + // creates duplicate environment variables that differ only in case - + // see https://github.com/PowerShell/PowerShell/issues/6305. + // However, because retrieval *by name* later is invariably + // case-INsensitive, in effect only a *single* variable exists. + // We simply ask Environment.GetEnvironmentVariable() for the effective value + // and use that as the only entry, because for a given key 'foo' (and all its case variations), + // that is guaranteed to match what $env:FOO and [environment]::GetEnvironmentVariable('foo') return. + // (If, by contrast, we just used `entry` as-is every time a duplicate is encountered, + // it could - intermittently - represent a value *other* than the effective one.) + string effectiveValue = Environment.GetEnvironmentVariable((string)entry.Key); + if (((string)entry.Value).Equals(effectiveValue, StringComparison.Ordinal)) { // We've found the effective definition. + // Note: We *recreate* the entry so that the specific name casing of the + // effective definition is also reflected. However, if the case variants + // define the same value, it is unspecified which name variant is reflected + // in Get-Item env: output; given the always case-insensitive nature of the retrieval, + // that shouldn't matter. + providerTable.Remove((string)entry.Key); + providerTable.Add((string)entry.Key, entry); + } + } } return providerTable; diff --git a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 index 65f8256c263..c66ba603afb 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Management/Get-Item.Tests.ps1 @@ -143,3 +143,29 @@ Describe "Get-Item" -Tags "CI" { } } } + +Describe "Get-Item environment provider on Windows with accidental case-variant duplicates" -Tags "Scenario" { + BeforeAll { + $env:testVar = 'a' # Note: Even though PSScriptAnalyzer can't detect it, this variable *is* used below, namely via Node.js. + } + AfterAll { + $env:testVar = $null + } + It "Reports the effective value among accidental case-variant duplicates on Windows" -skip:$skipNotWindows { + if (-not (Get-Command -ErrorAction Ignore node.exe)) { + Write-Warning "Test skipped, because prerequisite Node.js is not installed." + } else { + $valDirect, $valGetItem, $unused = node.exe -pe @" + env = {} + env.testVar = process.env.testVar // include the original case variant with its original value. + env.TESTVAR = 'b' // redefine with a case variant name and different value + // Note: Which value will win is not deterministic(!); what matters, however, is that both + // $env:testvar and Get-Item env:testvar report the same value. + // The nondeterministic behavior makes it hard to prove that the values are *always* the + // same, however. + require('child_process').execSync(\"\\\"$($PSHOME -replace '\\', '/')/pwsh.exe\\\" -noprofile -command `$env:testvar, (Get-Item env:testvar).Value\", { env: env }).toString() +"@ + $valGetItem | Should -BeExactly $valDirect + } + } +}