Skip to content

Instantly share code, notes, and snippets.

@eabase
Created June 2, 2025 17:27
Show Gist options
  • Save eabase/24af97bf1ae50359648e5588d96bf1c9 to your computer and use it in GitHub Desktop.
Save eabase/24af97bf1ae50359648e5588d96bf1c9 to your computer and use it in GitHub Desktop.
Windows System/User PATH variable manager and analyzer
#!/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
#------------------------------------------------------------------------------
@eabase
Copy link
Author

eabase commented Jun 2, 2025

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.

2025-0602_193356_PowerShell

2025-0602_193845_PowerShell

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment