Created
June 2, 2025 17:27
-
-
Save eabase/24af97bf1ae50359648e5588d96bf1c9 to your computer and use it in GitHub Desktop.
Windows System/User PATH variable manager and analyzer
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
#!/usr/bin/env pwsh | |
# TAB:4sp + EOL:CRLF | |
#------------------------------------------------------------------------------ | |
# Filename : SystemPath.ps1 | |
# Author : eabase | |
# Date : 2025-06-02 | |
# Version : 1.0.0 | |
# Encoding : UTF-8 | |
# License : CC-BY-SA-4.0 | |
#------------------------------------------------------------------------------ | |
# | |
# Description | |
# Get/Set and save/load User & System PATH variables | |
# | |
# NOTE | |
# - The User PATH is appended to the Machine (System) PATH Variable when a session starts. | |
# - When a process starts, Windows combines the System PATH and User PATH. | |
# - The System PATH is loaded first, followed by the User PATH. | |
# - If a program exists in both, the System PATH version takes precedence. | |
# - The theoretical limit for an environment variable in Windows is 32,767 characters | |
# - The System PATH variable is often restricted by registry editors, which may truncate values at 2047 characters. | |
# - The User PATH variable may also be affected by certain tools, such as setx, which limits values to 1023 characters. | |
# | |
# To Directly Open Windows Settings UI for Environment Variables (from powershell/terminal) | |
# rundll32 sysdm.cpl,EditEnvironmentVariables | |
# | |
# Some Examples: | |
# .\SystemPath.ps1 -u -l | |
# .\SystemPath -l | |
# .\SystemPath -p | |
# .\SystemPath -s | |
# .\SystemPath -u -d | |
# .\SystemPath -i .\some_paths.txt | |
# .\SystemPath -t -o .\spath_reverse_sorted.txt | |
# .\SystemPath -o . | |
# | |
# Other Examples: | |
# $UPATH = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::User) | |
# $SPATH = [System.Environment]::GetEnvironmentVariable('PATH', [System.EnvironmentVariableTarget]::Machine) | |
# $SPATH | Measure-Object -Character | |
# [System.Environment]::SetEnvironmentVariable("Path", $NewPath, $PathType) | |
# | |
#------------------------------------------------------------------------------ | |
param ( | |
[Alias("i")] [string] $InputFile, # <input-file> | |
[Alias("o")] [string] $OutputFile, # <output-file> | |
[Alias("l")] [switch] $PrintPathLength, # Print true length of PATH variable | |
[Alias("p")] [switch] $PrintPathLines, # Print PATH Line-by-line | |
[Alias("r")] [switch] $PrintRawPath, # Print "Raw" PATH variable | |
[Alias("s")] [switch] $SortPaths, # Print PATH lexically sorted | |
[Alias("t")] [switch] $SortPathsReverse, # Print PATH lexically sorted in reverse | |
[Alias("w")] [switch] $WriteToSystemPath, # WRITE !! | |
[Alias("u")] [switch] $UseUserPath, # [User | Machine] PATH | |
[Alias("d")] [switch] $CheckForDuplicates # Print duplicated paths | |
# This doesn't work for unknown reason: | |
#[Alias("o")][AllowNull()][AllowEmptyString()][string] $OutputFile, # <output-file> | |
) | |
#-------------------------------------- | |
# Constants | |
#-------------------------------------- | |
$MAX_VARL = 32767 # Maximum environment variable length | |
$MAX_SPATH = 2047 # Changing the path variable via the Windows UI will no longer work | |
$MAX_SETXL = 1023 # setx will no longer work | |
#-------------------------------------- | |
# Check CLI options | |
#-------------------------------------- | |
function popt() { | |
param([string] $opt, [string] $txt) | |
Write-Host -f White " -$opt" -Non; Write-Host -f Gray "`t $txt" | |
} | |
function usage() { | |
#$sfp = Split-Path -Path $PSCommandPath -Leaf # Script filename + extension | |
$sfp = [System.IO.Path]::GetFileNameWithoutExtension($PSCommandPath) # Script filename | |
#Write-Error "No parameters passed" | |
Write-Host -f White "`nUsage:`n" | |
Write-Host -f DarkYellow "$sfp [-d] [-l] [-p] [-r] [-s] [-t] [-u] [-w] [-i <input-filename>] [-o <output-filename>]`n" | |
Write-Host -f DarkGray "Command Line Options:" | |
popt 'i' 'Specify an <input-file>' | |
popt 'o' 'Specify an <output-file>' | |
popt 'd' 'Print duplicated paths' | |
#popt 'k' 'Use raw format with semicolons in saved files.' | |
popt 'l' 'Print true length of PATH variable' | |
popt 'p' 'Print PATH Line-by-line' | |
popt 'r' 'Print "Raw" PATH variable' | |
popt 's' 'Print PATH lexically sorted' | |
popt 't' 'Print PATH lexically sorted in reverse' | |
popt 'u' 'Select User PATH variable (default: Machine)' | |
popt 'w' 'Write to specified Windows PATH variable (default: Machine)' | |
Write-Host -f DarkGray "`n--------------------------------------------------------------------------" | |
Write-Host -f Green "NOTE:" | |
Write-Host -f DarkGray "- The Maximum environment variable length is: $MAX_VARL." | |
Write-Host -f DarkGray "- The Maximum Windows UI editable PATH variable length is: $MAX_SPATH." | |
Write-Host -f DarkGray "- Files are saved line-by-line without semicolons (;) for easy editing." | |
Write-Host -f DarkGray "- If you specify a dot ('.') for the filename with the -o option," | |
Write-Host -f DarkGray " you automatically get a time stamped filename in the format:" | |
Write-Host -f DarkGray " SPATH_2025_0601_1731.txt" | |
Write-Host -f DarkGray "--------------------------------------------------------------------------`n" | |
exit 0 | |
} | |
if ( $PSBoundParameters.Values.Count -eq 0 ) { | |
usage | |
} elseif ($PSBoundParameters.Values.Count -eq 1 ) { | |
# This is not quite working ... see backup file! | |
#Write-Host -f Red "`n[ERROR] Invalid parameters provided!" | |
#usage | |
} | |
#-------------------------------------- | |
# Check User vs. System PATH | |
#-------------------------------------- | |
# [-u] Determine whether to use System or User PATH | |
if ($UseUserPath) { $PathType = "User" } else { $PathType = "Machine" } | |
# Get the selected PATH variable | |
$SystemPath = [System.Environment]::GetEnvironmentVariable("Path", $PathType) -split ";" | |
#-------------------------------------- | |
# Handle Options | |
#-------------------------------------- | |
# [-i] | |
# If we are reading from an external file: | |
# - we don't use/load the Windows PATH variable. | |
# - we assume its a line-by-line file without semicolons (';') | |
# - we then join the lines with semicolons for storing in local variable for further use. | |
if ($InputFile) { | |
$InternalPaths = '' | |
try { | |
#$FileContent = Get-Content -Encoding UTF8 $InputFile # -Delimiter '\n' ? | |
$FileContent = Get-Content -Encoding UTF8 $InputFile -Delimiter '\n' | |
} catch { | |
Write-Host -f Red "`n[ERROR] Could not open file...`n" | |
Return | |
} | |
Write-Host -f DarkYellow "`nFile Content:" | |
Write-Host -f DarkGray "$FileContent`n" | |
#$InternalPaths += $FileContent # -split ";" | |
#$InternalPaths += $FileContent -split ";" | |
$InternalPaths += $FileContent -join ";" | |
} else { | |
# Internal variable to hold User/System paths | |
$InternalPaths = $SystemPath | |
} | |
# [-l] | |
# Print true windows registry length of the User|Machine PATH variable. | |
if($PrintPathLength) { | |
# PATH Character Counter | |
# TODO: Use raw path with ";" | |
$RawPath = [System.Environment]::GetEnvironmentVariable("Path", $PathType) | |
$PLEN = ($RawPath | Measure-Object -Character).Characters | |
# Define some messages | |
$PLD1 = ($PLEN - $MAX_SPATH) # Windows GUI limit | |
$PLD2 = ($PLEN - $MAX_SETXL) # CMD setx limit | |
$MSG1 = "`nYour $PathType PATH variable length exceeds the maximum allowed value of " # "$MAX_SPATH | $MAX_SETXL by $PLD1 characters" | |
$MSG2 = "- You will not be able to use 'setx' from command line." | |
$MSG3 = "- You will not be able to use the Windows Settings GUI to change your PATH variable." | |
$MSG4 = "- You can still change the Machine PATH from Windows GUI or an Admin Powershell." | |
$MSG5 = "- You can only change the Machine PATH from an Admin Powershell, using:" | |
$MSG6 = ' $SPATH = [System.Environment]::GetEnvironmentVariable("Path", "Machine")' | |
$MSG7 = ' [System.Environment]::SetEnvironmentVariable("Path", $NewPath, "Machine")' | |
$MSG8 = "- The PATH shown in your terminal shell is the combined System (Machine) + User PATH's" | |
Write-Host -f DarkYellow "`nYour Current $PathType PATH length is: " -Non; Write-Host -f White "$PLEN" -Non; Write-Host -f DarkYellow " characters." | |
if ($PathType -eq "Machine") { | |
if ($PLEN -ge $MAX_SPATH) { | |
Write-Host -f Red "[WARNING]" -Non; Write-Host -f DarkGray "$MSG1" -Non; | |
Write-Host -f Red "$MAX_SPATH" -Non; Write-Host -f DarkGray " by " -Non; Write-Host -f Red "$PLD1" -Non; Write-Host -f DarkGray " characters." | |
Write-Host -f DarkGray "$MSG2`n$MSG3`n$MSG5`n$MSG6`n$MSG7" | |
Return | |
} elseif ($PLEN -gt $MAX_SETXL) { | |
Write-Host -f Red "[WARNING]" -Non; Write-Host -f DarkGray "$MSG1" -Non; | |
Write-Host -f Red "$MAX_SETXL" -Non; Write-Host -f DarkGray " by " -Non; Write-Host -f Red "$PLD2" -Non; Write-Host -f DarkGray " characters." | |
Write-Host -f DarkGray "$MSG2`n$MSG4" | |
} | |
} elseif ($PathType -eq "User") { | |
if ($PLEN -gt $MAX_SPATH) { | |
Write-Host -f Red "[WARNING]" -Non; Write-Host -f DarkGray "$MSG1" -Non; | |
Write-Host -f Red "$MAX_SPATH" -Non; Write-Host -f DarkGray " by " -Non; Write-Host -f Red "$PLD1" -Non; Write-Host -f DarkGray " characters." | |
Write-Host -f DarkGray "$MSG2`n$MSG3`n$MSG5`n$MSG7" | |
Return | |
} elseif ($PLEN -gt $MAX_SETXL) { | |
Write-Host -f Red "[WARNING]" -Non; Write-Host -f DarkGray "$MSG1" -Non; | |
Write-Host -f Red "$MAX_SETXL" -Non; Write-Host -f DarkGray " by " -Non; Write-Host -f Red "$PLD2" -Non; Write-Host -f DarkGray " characters." | |
Write-Host -f DarkGray "$MSG2`n$MSG4" | |
} | |
} | |
Write-Host -f DarkGray "$MSG8" | |
return | |
} | |
# [-s, -t] | |
# Print lexically Sorted paths | |
if ($SortPaths -or $SortPathsReverse) { | |
if ($SortPathsReverse) { | |
$SortedPaths = $InternalPaths | Sort-Object -Descending | |
} else { | |
$SortedPaths = $InternalPaths | Sort-Object | |
} | |
$SortedPaths | ForEach-Object { Write-Output $_ } | |
# Copy so we can use it | |
$InternalPaths = $SortedPaths | |
#return | |
} | |
# [-r] | |
# Print the raw PATH variable in one line (with semicolons ';') | |
if ($PrintRawPath) { | |
Write-Host "`n" | |
$InternalPaths -join ";" | |
Write-Host "`n" | |
return | |
} | |
# [-p] | |
# Print the raw PATH variable line-by-line (without semicolons) | |
if ($PrintPathLines) { | |
$InternalPaths | ForEach-Object { Write-Output $_ } | |
return | |
} | |
# [-d] | |
# Check for duplicate paths and print them | |
if ($CheckForDuplicates) { | |
$Duplicates = $InternalPaths | Group-Object | Where-Object { $_.Count -gt 1 } | ForEach-Object { $_.Name } | |
if ($Duplicates) { | |
Write-Host -f Red "`n[WARNING]" -Non | |
Write-Host -f Yellow " Duplicate $PathType paths detected!`n" | |
$Duplicates | ForEach-Object { Write-Output $_ } | |
} else { | |
Write-Host -f Green "`nNo duplicates found in $PathType PATH.`n" | |
} | |
return | |
} | |
# [-w] | |
# Write sorted paths to the System or User PATH variable | |
if ($WriteToSystemPath) { | |
$NewPath = $InternalPaths -join ";" | |
Write-Host -f DarkGray "`n$NewPath`n" | |
# TODO: | |
# - Add Admin check | |
# - Use try/catch | |
[System.Environment]::SetEnvironmentVariable("Path", $NewPath, $PathType) | |
Write-Host -f DarkYellow "`nUpdated Windows Registry " -Non; Write-Host -f Yellow "$PathType" -Non; Write-Host -f DarkYellow " PATH with above path modification." | |
Write-Host -f DarkGray "Refresh your environment for new PATH to take effect." | |
return | |
} | |
# [-o] | |
# Write to file if specified | |
# TODO: Add option to write as raw PATH with semicolons. | |
if ($OutputFile) { | |
# Set Default Filename | |
$PathFilePrefix = 'SPATH' # Default is: Machine (System) Path | |
if ($PathType -eq 'User') { | |
$PathFilePrefix = 'UPATH' | |
} | |
$DateString = Get-Date -Format "yyyy_MMdd_HHmm" # 2025_0601_1731 | |
$DefaultFileName = "${PathFilePrefix}_${DateString}.txt" # SPATH_2025_0601_1731.txt | |
#if ([string]::IsNullOrEmpty($OutputFile)) { | |
if ($OutputFile -eq '.') { | |
$OutputFile = $DefaultFileName | |
} | |
if ($SortPaths) { | |
$SortedPaths | Out-File -Encoding UTF8 $OutputFile | |
} else { | |
$InternalPaths | Out-File -Encoding UTF8 $OutputFile | |
} | |
Write-Host -f DarkYellow "`n$PathType PATH saved to: " -Non; Write-Host -f White "$OutputFile`n" | |
return | |
} | |
#------------------------------------------------------------------------------ | |
# END | |
#------------------------------------------------------------------------------ |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Caution
I haven't tested the writing option (
-w
) when used together with other options such as (-i, -s, -t
etc.)Please make sure to review the code before using.
Tip
If you find any issues or have ideas for improvement, please let me know below.