Add Update-Environment cmdlet#27360
Conversation
Adds a new Update-Environment cmdlet to Microsoft.PowerShell.Management to sync process environment from the machine and user registry targets. Previously, users needed to restart their shell to inherit updated/created environment variables. This cmdlet safely pulls the newly created or updated environment variables without the need to restart the shell.
|
@microsoft-github-policy-service agree |
There was a problem hiding this comment.
Pull request overview
Adds a new Update-Environment cmdlet to Microsoft.PowerShell.Management intended to refresh the current process environment from the persisted Machine/User environment values.
Changes:
- Added
UpdateEnvironmentCommandcmdlet implementation in the Management commands assembly. - Exported
Update-Environmentfrom the Windows and UnixMicrosoft.PowerShell.Managementmodule manifests. - Added Pester tests validating basic variable behavior and user-scope refresh.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 8 comments.
| File | Description |
|---|---|
src/Microsoft.PowerShell.Commands.Management/commands/management/UpdateEnvironmentCommand.cs |
Implements the new Update-Environment cmdlet and merging logic. |
src/Modules/Windows/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1 |
Exports Update-Environment on Windows. |
src/Modules/Unix/Microsoft.PowerShell.Management/Microsoft.PowerShell.Management.psd1 |
Exports Update-Environment on Unix (currently problematic). |
test/powershell/Modules/Microsoft.PowerShell.Management/Update-Environment.Tests.ps1 |
Adds tests for ignored variables and user-target refresh behavior. |
|
|
||
| if (updateAll || Machine.IsPresent) | ||
| { | ||
| WriteVerbose("Updating Machine environment variables..."); | ||
| UpdateFromTarget(EnvironmentVariableTarget.Machine); | ||
| } | ||
|
|
||
| if (updateAll || User.IsPresent) | ||
| { | ||
| WriteVerbose("Updating User environment variables..."); | ||
| UpdateFromTarget(EnvironmentVariableTarget.User); | ||
| } | ||
|
|
||
| if (updateAll || (Machine.IsPresent && User.IsPresent)) | ||
| { | ||
| FixListVariable("Path"); | ||
| FixListVariable("PSModulePath"); | ||
| } | ||
| } | ||
|
|
||
| private void FixListVariable(string variableName) | ||
| { | ||
| string machineVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.Machine); | ||
| string userVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.User); | ||
|
|
||
| if (!string.IsNullOrEmpty(machineVal) && !string.IsNullOrEmpty(userVal)) | ||
| { | ||
| string combinedValue = machineVal + Path.PathSeparator + userVal; | ||
| string processVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.Process); | ||
|
|
||
| if (!string.Equals(combinedValue, processVal, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| Environment.SetEnvironmentVariable(variableName, combinedValue, EnvironmentVariableTarget.Process); | ||
| WriteVerbose($"Merged User and Machine values for {variableName}"); | ||
| } | ||
| } |
| string combinedValue = machineVal + Path.PathSeparator + userVal; | ||
| string processVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.Process); | ||
|
|
||
| if (!string.Equals(combinedValue, processVal, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| Environment.SetEnvironmentVariable(variableName, combinedValue, EnvironmentVariableTarget.Process); | ||
| WriteVerbose($"Merged User and Machine values for {variableName}"); | ||
| } | ||
| } | ||
| } | ||
|
|
| WriteVerbose($"Added {target} variable: {key} = '{value}'"); | ||
| } | ||
| else | ||
| { | ||
| WriteVerbose($"Updated {target} variable: {key} from '{currentValue}' to '{value}'"); |
| { | ||
| // A list of variables that should never be overwritten | ||
| // by static Machine or User registry reads. | ||
| private static readonly HashSet<string> _ignoredVariables = new HashSet<string>(StringComparer.OrdinalIgnoreCase) |
| # Copyright (c) Microsoft Corporation. | ||
| # Licensed under the MIT License. | ||
|
|
||
| Describe "Update-Environment" -Tag "CI" { |
| Update-Environment | ||
|
|
||
| # Assert | ||
| $env:USERNAME | Should -BeExactly $originalUsername | ||
| } |
| if (updateAll || (Machine.IsPresent && User.IsPresent)) | ||
| { | ||
| FixListVariable("Path"); | ||
| FixListVariable("PSModulePath"); | ||
| } |
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…pdate test cases for Windows compatibility
…dling methods and removing redundant code
…and cleaning up code structure
| # Snapshot the CI runner's environment to restore after all tests execute | ||
| BeforeAll { | ||
| $script:originalPath = $env:PATH | ||
| $script:originalUser = $env:USERNAME | ||
| } | ||
|
|
||
| AfterAll { | ||
| $env:PATH = $script:originalPath | ||
| $env:USERNAME = $script:originalUser | ||
| } |
| [Cmdlet(VerbsData.Update, "Environment")] | ||
| public class UpdateEnvironmentCommand : PSCmdlet |
…ribute and improving test structure for environment variable management
| Context "Variable merging and blocklist" { | ||
| It "Should not overwrite ignored dynamic variables like USERNAME" { | ||
| # Act | ||
| Update-Environment | ||
|
|
||
| # Assert | ||
| $env:USERNAME | Should -BeExactly $script:originalUser | ||
| } | ||
|
|
||
| It "Should successfully pull new variables from the User target" { | ||
| # Arrange | ||
| $testKey = "TEST_UPDATE_ENV_VAR_$(Get-Random)" | ||
| $testValue = "HelloWorld" | ||
|
|
||
| # Set a new environment variable in the User registry target | ||
| [Environment]::SetEnvironmentVariable($testKey, $testValue, "User") | ||
|
|
||
| try { | ||
| # Ensure the current process does not have it yet | ||
| [Environment]::GetEnvironmentVariable($testKey, "Process") | Should -BeNullOrEmpty | ||
|
|
||
| # Act - Run cmdlet | ||
| Update-Environment -User | ||
|
|
||
| # Assert - The process should now have the variable | ||
| (Get-Item -Path "Env:\$testKey").Value | Should -BeExactly $testValue | ||
| } |
| Remove-Item -Path "Env:\$testKey" -ErrorAction SilentlyContinue | ||
| } | ||
| } | ||
|
|
| string processVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.Process); | ||
| List<string> mergedSegments = new List<string>(); | ||
| HashSet<string> seenSegments = new HashSet<string>(StringComparer.OrdinalIgnoreCase); | ||
|
|
||
| if (includeMachine) | ||
| { | ||
| string machineVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.Machine); | ||
| AppendUniqueListSegments(mergedSegments, seenSegments, machineVal); | ||
| } | ||
|
|
||
| if (includeUser) | ||
| { | ||
| string userVal = Environment.GetEnvironmentVariable(variableName, EnvironmentVariableTarget.User); | ||
| AppendUniqueListSegments(mergedSegments, seenSegments, userVal); | ||
| } | ||
|
|
||
| int registrySegmentCount = mergedSegments.Count; | ||
|
|
||
| // Preserve entries that exist only in the current process value | ||
| AppendUniqueListSegments(mergedSegments, seenSegments, processVal); | ||
|
|
||
| string mergedValue = string.Join(Path.PathSeparator.ToString(), mergedSegments); | ||
|
|
||
| if (!string.Equals(mergedValue, processVal, StringComparison.OrdinalIgnoreCase)) | ||
| { | ||
| // Verify the user confirms the action if -Confirm or -WhatIf was supplied | ||
| if (ShouldProcess($"Environment Variable: {variableName}", $"Update to: {mergedValue}")) | ||
| { | ||
| Environment.SetEnvironmentVariable(variableName, mergedValue, EnvironmentVariableTarget.Process); |
| IDictionary envVars = Environment.GetEnvironmentVariables(target); | ||
|
|
||
| foreach (DictionaryEntry entry in envVars) | ||
| { | ||
| string key = (string)entry.Key; | ||
| string value = (string)entry.Value; | ||
|
|
||
| if (s_ignoredVariables.Contains(key)) | ||
| { | ||
| continue; | ||
| } | ||
|
|
||
| string currentValue = Environment.GetEnvironmentVariable(key, EnvironmentVariableTarget.Process); | ||
|
|
…mpty segments and improving test cases for variable merging and -WhatIf behavior
|
I have updated the PR description to clarify this cmdlet only pulls and merges newly created/updated variables, rather than syncing (which can mean the removal of environment variables). Removing variables from active processes automatically can potentially cause unexpected instability for the user during an active session. |
…ancing tests for variable merging and blocklist behavior
…Windows platforms
| /// <summary> | ||
| /// Implements the Update-Environment cmdlet. | ||
| /// </summary> | ||
| [Cmdlet(VerbsData.Update, "Environment", SupportsShouldProcess = true)] |
There was a problem hiding this comment.
Since Update-Environment is not in PowerShell yet, there is no official Microsoft Docs page for it yet. I have left this out for now until there is documentation for it.
| if (updateMachine) | ||
| { | ||
| WriteVerbose("Updating Machine environment variables..."); | ||
| UpdateFromTarget(EnvironmentVariableTarget.Machine); | ||
| } | ||
|
|
||
| if (updateUser) | ||
| { | ||
| WriteVerbose("Updating User environment variables..."); | ||
| UpdateFromTarget(EnvironmentVariableTarget.User); | ||
| } |
There was a problem hiding this comment.
Since these strings are mainly for debugging, I am leaving them hardcoded for now, unless strongly required by maintainers.
…s while preserving process-only segments
PR Summary
Adds a new
Update-Environmentcmdlet toMicrosoft.PowerShell.Managementto pull and merge the process environment from the Machine and User registry targets.PR Context
Previously, users needed to restart their shell to inherit updated/created environment variables, which can be quite irritating, especially if the user has set temporary environment variables in their session. This cmdlet safely pulls the newly created or updated environment variables without the need to restart the shell.
PR Checklist
.h,.cpp,.cs,.ps1and.psm1files have the correct copyright header