My god, it's full of variables
There is three types of environment variables:
Machine
The environment variable is stored or retrieved from the
HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Session
Manager\Environment key in the Windows operating system registry. When
a user creates the environment variable in a set operation, the
operating system stores the environment variable in the system
registry, but not in the current process. If any user on the local
machine starts a new process, the operating system copies the
environment variable from the registry to that process. When the
process terminates, the operating system destroys the environment
variable in that process. However, the environment variable in the
registry persists until a user removes it programmatically or by means
of an operating system tool.
User
The environment variable is stored or retrieved from the
HKEY_CURRENT_USER\Environment key in the Windows operating system
registry. When the user creates the environment variable in a set
operation, the operating system stores the environment variable in the
system registry, but not in the current process. If the user starts a
new process, the operating system copies the environment variable from
the registry to that process. When the process terminates, the
operating system destroys the environment variable in that process.
However, the environment variable in the registry persists until the
user removes it programmatically or by means of an operating system
tool.
Process
The environment variable is stored or retrieved from the environment
block associated with the current process. The user creates the
environment variable in a set operation. When the process terminates,
the operating system destroys the environment variable in that
process.
Newly created with $env:
environment variables in PowerShell are of Process
type. For variable to display in the Advanced System Settings
it has be the Machine
or User
level variable. To create variable with specific level use Environment.SetEnvironmentVariable method:
[Environment]::SetEnvironmentVariable('MyPath', 'c:\mypath', 'User')
[Environment]::SetEnvironmentVariable('MyPath', 'c:\mypath', 'Machine')
Note, that setting Machine
level environment variables requires elevation.
Do you read me, HAL?
So you want to embed one environment variable inside another and have it expanded? After all, Microsoft does it all the way, for example per user TEMP
and TMP
variables contain USERPROFILE
env.var. Unfortunately, there is a number of quirks attached:
The underlying registry entry for such variable has to be REG_EXPAND_SZ type
The containee env. variable has to be alphabetically less than container env. variable:
If the definition of an environment variable var1 contains another
environment variable var2 and the name of var1 is alphabetically less
than the name of var2 (i.e. strcmp(var1, var2) < 0), then var2 won't
get expanded. This seems to be because when Windows first sets up the
environment variables, they are created in alphabetical order, so var2
does not exist until after var1 has already been created (and so the
expansion can't be done).
For PATH
variable, there should be no spaces between entries:
Incorrect: c:\path1; c:\Maven\bin\; c:\path2\
Correct: c:\path1;c:\Maven\bin\;c:\path2\
Furthermore, if you try to make use of Environment.SetEnvironmentVariable method like this:
$Path = [Environment]::GetEnvironmentVariable('Path', 'Machine')
[Environment]::SetEnvironmentVariable('MyPath', 'c:\mypath', 'Machine')
[Environment]::SetEnvironmentVariable('Path', "%MyPath%;$Path", 'Machine')
It will not yield to a desired result, because a newly created PATH
variable will not have have type REG_EXPAND_SZ
, but REG_SZ
.
Open the pod bay doors, HAL
Given that SetEnvironmentVariable
has no means to control resulting registry entry type, you have to employ alternative: directly modify registry to create an entry of the REG_EXPAND_SZ
type.
$Path = [Environment]::GetEnvironmentVariable('Path','Machine')
[Environment]::SetEnvironmentVariable('MyPath', 'c:\mypath', 'Machine')
[Microsoft.Win32.Registry]::SetValue(
'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment',
'Path',
"%MyPath%;$Path",
[Microsoft.Win32.RegistryValueKind]::ExpandString
)
The disadvantages of this method is that it doesn't broadcast a WM_SETTINGCHANGE
message to all windows in the system, so that any interested applications (such as Windows Explorer, Program Manager, Task Manager, Control Panel, and so forth) can perform an update.
To mitigate that, you can broadcast message yourself:
if (-not ('Win32.NativeMethods' -as [type])) {
# import SendMessageTimeout from Win32
Add-Type -Namespace Win32 -Name NativeMethods -MemberDefinition @'
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SendMessageTimeout(
IntPtr hWnd,
uint Msg,
UIntPtr wParam,
string lParam,
uint fuFlags,
uint uTimeout,
out UIntPtr lpdwResult);
'@
}
$HWND_BROADCAST = [System.IntPtr]0xffff
$WM_SETTINGCHANGE = 0x1a
$result = [System.UIntPtr]::Zero
# Notify all windows of environment block change
[Win32.NativeMethods]::SendMessageTimeout(
$HWND_BROADCAST, $WM_SETTINGCHANGE,
[System.UIntPtr]::Zero,
'Environment',
2,
5000,
[ref]$result
)