Last active
March 29, 2026 22:42
-
-
Save fdcastel/38ec25c8fc862e691c6d70d95c22fe4b to your computer and use it in GitHub Desktop.
Windows Powershell functions for system path
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| function Get-SystemPath { | |
| $keyName = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment' | |
| $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($keyName, 'ReadOnly') | |
| try { | |
| return $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split [IO.Path]::PathSeparator | |
| } finally { | |
| if ($null -ne $key) { | |
| $key.Dispose() | |
| } | |
| } | |
| } | |
| function Add-SystemPath([Parameter(Mandatory=$true)][string[]]$Folder) { | |
| $keyName = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment' | |
| $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($keyName, $true) | |
| try { | |
| # Get current PATH | |
| $currentPathFolders = $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split [IO.Path]::PathSeparator | |
| # Add new folders to the current PATH | |
| $newPathFolders = $currentPathFolders + @($Folder) | |
| # Normalize folders to remove trailing slashes and duplicates | |
| $result = [Collections.Generic.HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase) | |
| $newPathFolders | | |
| ForEach-Object { | |
| $normalized = $_.TrimEnd([IO.Path]::DirectorySeparatorChar).Trim() | |
| if ($normalized -ne '') { | |
| $result.Add($normalized) | |
| } | |
| } > $null | |
| # Build new PATH and save it | |
| $newPath = $result -join [IO.Path]::PathSeparator | |
| $key.SetValue('Path', $newPath, 'ExpandString') | |
| return $result | |
| } finally { | |
| if ($null -ne $key) { | |
| $key.Dispose() | |
| } | |
| } | |
| } | |
| function Remove-SystemPath([Parameter(Mandatory=$true)][string[]]$Folder) { | |
| $keyName = 'SYSTEM\CurrentControlSet\Control\Session Manager\Environment' | |
| $key = [Microsoft.Win32.Registry]::LocalMachine.OpenSubKey($keyName, $true) | |
| try { | |
| # Get current PATH | |
| $currentPathFolders = $key.GetValue('Path', '', 'DoNotExpandEnvironmentNames') -split [IO.Path]::PathSeparator | |
| # Normalize folders to remove | |
| $foldersToRemove = $Folder | ForEach-Object { $_.TrimEnd([IO.Path]::DirectorySeparatorChar) } | |
| # Filter out the folders to remove (case-insensitive) | |
| $result = [Collections.Generic.HashSet[string]]::new([StringComparer]::InvariantCultureIgnoreCase) | |
| $currentPathFolders | | |
| Where-Object { | |
| $normalizedFolder = $_.TrimEnd([IO.Path]::DirectorySeparatorChar) | |
| $foldersToRemove -notcontains $normalizedFolder | |
| } | | |
| ForEach-Object { $result.Add($_) } > $null | |
| # Build new PATH and save it | |
| $newPath = $result -join [IO.Path]::PathSeparator | |
| $key.SetValue('Path', $newPath, 'ExpandString') | |
| return $result | |
| } finally { | |
| if ($null -ne $key) { | |
| $key.Dispose() | |
| } | |
| } | |
| } |
Author
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Why go through all this just to update the system
PATH?1. Must go through the registry directly
Using
$env:PATHor[Environment]::SetEnvironmentVariable()reads/writes the already-expanded value —%SystemRoot%\System32becomes System32. Save that back and you've permanently destroyed the portable, machine-agnostic references. So the script opens the registry key manually.2.
DoNotExpandEnvironmentNameswhen readingThis flag is how you get the raw, unexpanded string (
%SystemRoot%\System32) instead of the resolved path. Without it, round-tripping PATH corrupts it.3.
ExpandString(REG_EXPAND_SZ) when writingPATH must be stored as
REG_EXPAND_SZin the registry, not as a plainREG_SZstring. If you write it as the wrong type, Windows stops expanding%...%references for all processes.4. Case-insensitive deduplication
Windows paths are case-insensitive, so
C:\Fooandc:\fooare the same entry. A plain array comparison would miss this, hence theHashSet<string>withInvariantCultureIgnoreCase. Trailing-slash normalization (TrimEnd) handlesC:\Foo\vsC:\Foo.