Skip to content
Merged
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
32 changes: 23 additions & 9 deletions src/System.Management.Automation/namespaces/EnvironmentProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}