I trust that most of you are aware that the reason it is often easy to run command and programs in Windows, especially items from the command prompt, is thanks to a system environment variable called PATH. When you tell Windows to run a command, without using the complete path to the program, Windows looks in the locations specified by the %PATH% variable. Depending on an application that you install, it might update the %PATH% variable. Although those changes may not take effect until your next reboot or command session. You've probably seen messages to that effect. However, when you uninstall an application, it may not clean up and remove entries to %PATH%. This means you will have values that probably don't exist in %PATH%.
ManageEngine ADManager Plus - Download Free Trial
Exclusive offer on ADManager Plus for US and UK regions. Claim now!
From a technical perspective, I don't think there's any harm or penalty. But if you are like me you like to keep things neat and tidy. Plus this provides an opportunity to teach a few PowerShell concepts and techniques. Let's see how to use PowerShell to manage the %PATH% environment variable.
Displaying the Path
The first thing to do is to view the current value of %PATH%. This is an environment variable so you can use the ENV: PSDrive. The easy way to reference items is like $env:Path or $env:Computername.
That's a lot to take in. This is one long string of directories separated by a semi-colon, which means you can split the string into an array.
$env:path -split ";"
Another option would be to use an expression like this to invoke the Split() method on a String object.
($env:path).split(";")
The end result is the same in either case, an array of locations.
Validating Path Locations
I already know how to use the Test-Path cmdlet. To find locations that no longer exist I need to test each one.
$env:path -split ";" | Where-Object {-not (Test-Path $_) }
This expression says "Take every path from the array and filter where Test-Path returns $False." Remember that the -Not operator turns Boolean values on their head. So if Test-Path gives me $False, the -Not operator turns it into $True which means Where-Object keeps it in the pipeline.
These locations no longer exist on my computer, even though they are listed in %PATH%. Again, this doesn't hurt anything as far as I know, but I like to keep things neat.
Working Cross-Platform
Windows machines should have $env:Path defined. But if not, you can also use the .NET Framework directly to achieve the same results.
[System.Environment]::GetEnvironmentVariable("PATH") -split ";" | Where-Object {-not (Test-Path $_) }
On Linux, and I'm assuming MacOS although I don't have anything the test, you should also have a %PATH% variable. Although be careful because PATH is case sensitive.
Although if you notice there is a different separator. But no worries. If you are writing some code to run cross-platform, .NET includes a helpful class that tells you what character to use as the splitter or separator.
$splitter = [System.IO.Path]::PathSeparator [System.Environment]::GetEnvironmentVariable("PATH") -split $splitter | Where-Object {-not (Test-Path $_) }
Thinking Objects
Another approach I'd like you to consider, especially as you think about creating a reusable tool, is to think about writing an object to the pipeline. Here's a prototype function.
Function Test-PathEnv { [cmdletbinding()] Param() $splitter = [System.IO.Path]::PathSeparator $pathenv = [System.Environment]::GetEnvironmentVariable("PATH") if ($pathenv) { $env:PATH -split $splitter | Foreach-Object { # create a custom object based on each path [pscustomobject]@{ Computername = [System.Environment]::MachineName Path = $_ Exists = Test-Path $_ } } } else { Write-Warning "Failed to find an environmental path" } }
Normally I'd steer away from calling .NET directly but because I want this to work cross-platform, I need to use these classes. The ENV: drive on a Linux box isn't the same on Windows.
Clean Up
I suppose I shouldn't leave you without mentioning how to clean up problems. I'm going to leave non-Windows remediation to you because it might vary depending on the platform. In Windows, your %PATH% is actually composed of user-specific settings which are stored in the registry under HKCU:\Environment
And system values found under HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment.
The safe way to clean up would be to use the Windows 10 Control panel and manually edit these settings. But PowerShell has tools to modify the registry so why not? What could possibly go wrong?
[This is where I make the standard disclaimer that modifying the registry is a potentially risky task and that you take full responsibility and have backup or recovery plans in place.]
I'm going to use my function to get the missing locations and also create backup copies of the registry locations.
$missing = Test-PathEnv | Where-object {-not $_.exists} $savedUser = (Get-itemproperty -path HKCU:\Environment\).path $savedSystem = (Get-ItemProperty 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').path
I don't know if the missing path is a user or system value so I need to check both. I can split each saved value into an array and use the -Contains operator to verify.
foreach ($p in $missing.path) { Write-Host "Searching for $p" -ForegroundColor Yellow $user = (Get-itemproperty -path 'HKCU:\Environment\').path -split ";" if ($user -contains $p) { Write-Host "Found in HKCU" -ForegroundColor green #insert clean up code } $system = (Get-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').path -split ";" if ($system -contains $p) { Write-Host "Found in HKLM" -ForegroundColor Green #insert clean up code } }
I'm slowly building a code solution. My plan is to rebuild the appropriate path array without the missing locations and then turn it back into a string which I can use to update the registry property.
foreach ($p in $missing.path) { Write-Host "Searching for $p" -ForegroundColor Yellow $user = (Get-itemproperty -path 'HKCU:\Environment\').path -split ";" if ($user -contains $p) { Write-Host "Found in HKCU" -ForegroundColor green $fix = $user | Where-Object {$_ -ne $p} $value = $fix -join ";" Set-ItemProperty -Path 'HKCU:\Environment\' -Name Path -Value $value -WhatIf } $system = (Get-ItemProperty -path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment').path -split ";" if ($system -contains $p) { Write-Host "Found in HKLM" -ForegroundColor Green $fix = $system | Where-Object {$_ -ne $p} $value = $fix -join ";" Set-ItemProperty -Path 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' -Name Path -Value $value -WhatIf } }
This is definitely code that you'd want to include support for -WhatIf.
I'm going to live on the edge and run this for real. Because my paths were user specific I won't see the change until I log off and on again. Now I have a clean %PATH% variable.
Wrap-Up
As with many of my articles, you may not necessarily need to address this specific task. But hopefully, you learned something new about PowerShell that might help in other ways. I will leave it to you to create a cleanup function.
That’s 3 minutes of my life I won’t get back.
If it ain’t broken, don’t fix it.
Sorry you feel that way. My articles are written to teach not necessarily to provide an answer to a problem you may not have.
Why are you using the .net environment function to get the path and then ignore the value and use $env:path anyhow?
In any case there are .net functions to modify environment variables which is much saner (and easier) than modifying the registry directly.
You can also avoid all the kerfuffle with figuring out whether the value is from the user specific or system specific part by using the .net function overload that let’s you specify the environment target.
A practical function should provide the ability to specify what target you’re interested in since normal users can’t edit stem environment variables.
I probably wasn’t clear in the article. I was showing the .NET approach as an alternative. In PowerShell Core on non-Windows platforms, you can’t always assume the ENV: PSDrive has the values you want. Before PowerShell Core I would have encouraged people to avoid using the framework directly and stick to the PSDrive. That’s no longer the case. And the function I provided is far from production ready. As you point out, there are a number of other steps you could include. The bottom line is that I was trying to show a variety of techniques. You can decide what works best for you.
What the previous users are missing is the potential for good use and ideas, in this case, this code might seem silly but I take it as a pebble of gold, might not be a lot but it’s gold anyway.
I do too like to keep things clean and tidy.
Thanks. I am almost always using my posts to teach.
I personally take lots of notes when learning! “Never memorize something that you can look up”
Comparing my notes on paths to this awesome article, there is one I didn’t see that I’d like to contribute.
It’s the .NET overload for “Machine” and “User”.
[System.Environment]::GetEnvironmentVariable(“PATH”,”Machine”)
[System.Environment]::GetEnvironmentVariable(“PATH”,”User”)
You could even stretch this one to “Man v. Machine”
Several people mentioned this. I’m going to do a followup article. I tend to avoid using the .NET Framework directly when there is a cmdlet that will work.
H Jeffery
To comment on your message
“..These locations no longer exist on my computer, even though they are listed in %PATH%. Again, this doesn’t hurt anything as far as I know, but I like to keep things neat…”
Something similar has already bitten me several times in the past with products from IBM, BMC,.. where sometimes you remove/reinstall or upgrade a program several times and ending up with a PATH variable that is becoming longer than 255 characters because of duplicate entries in the PATH. If these entries contains many chars it quickly can become ugly. So I can highly recommend a tide and clean approach like you do
One one of my systems, the code failed with the error, “Test-Path : Cannot bind argument to parameter ‘Path’ because it is an empty string.”
By changing to code to remove the empty string:
$env:PATH -split $splitter | Where-Object { $_ } |
the code continued as expected.
I’m assuming you ran the code to define $splitter? Does the first part work before you pipe it to Where-Object? What OS and version are you running?