Environment.ahk - Change User and System Environment Variables Permanently

Post your working scripts, libraries and tools.
iseahound
Posts: 1434
Joined: 13 Aug 2016, 21:04
Contact:

Environment.ahk - Change User and System Environment Variables Permanently

Post by iseahound » 20 Jan 2022, 12:33

Environment.ahk
Provides two sets of functions, one starting with "Env_UserXXX" and another starting with "Env_SystemXXX"

Features

  • Automatic REG_SZ and REG_EXPAND_SZ detection
  • Backup before you make any changes with EnvUserBackup() and EnvSystemBackup()
  • Sort your messy Windows PATH in alphabetical order.
  • Edit both system and user path with separate commands.
  • Broadcast changes to PATH in the current AutoHotKey script and System-wide.
Backup First

Code: Select all

Env_UserBackup()    ; Creates a backup of the user's environment variables. 
Env_SystemBackup()  ; Creates a backup of the system environment variables.
Stuff you can do

Code: Select all

; Add your binaries to the user PATH
Env_UserAdd("PATH", "C:\bin")
; Remove a directory from user PATH
Env_UserSub("PATH", "C:\bin")
; Create a new Environment Variable
Env_UserNew("ANSWER", "42")
; Read an existing Environment Variable
key := Env_UserRead("ANSWER") ; returns 42
; Delete an Environment Variable
Env_UserDel("ANSWER")
Example System Commands - RUN AS ADMINISTRATOR (except EnvSystemRead() and EnvSystemBackup())

Code: Select all

; Use EnvSystem to edit the System Environment Variables
Env_SystemAdd("PATH", "X:\Backup\bin")
; Sort System PATH in alphabetical order
Env_SystemSort("PATH")
; Remove pesky duplicate entries in your system PATH
Env_SystemRemoveDuplicates("PATH")
Full Script

Code: Select all

; Script     Environment.ahk
; License:   MIT License
; Author:    Edison Hua (iseahound)
; Github:    https://github.com/iseahound/Environment.ahk
; Date       2022-01-20
; Version    1.1.0
;
; ExpandEnvironmentStrings(), RefreshEnvironment()   by NoobSawce + DavidBiesack (modified by BatRamboZPM)
;   https://autohotkey.com/board/topic/63312-reload-systemuser-environment-variables/
;
; Global Error Values
;   0 - Success.
;       The error value "-1" is never returned in v2, instead an OSError is thrown.
;  -2 - Value already added or value already deleted.
;  -3 - Need to Run As Administrator.
;
; Notes
;   SendMessage 0x1A, 0, "Environment",, ahk_id 0xFFFF ; 0x1A is WM_SETTINGCHANGE
;      - The above code will broadcast a message stating there has been a change of environment variables.
;      - Some programs have not implemented this message.
;      - v1.00 replaces this with a powershell command using asyncronous execution providing 10x speedup.
;   RefreshEnvironment()
;      - This function will update the environment variables within AutoHotkey.
;      - Command prompts launched by AutoHotkey inherit AutoHotkey's environment.
;   Any command prompts currently open will not have their environment variables changed.
;      - Please use the RefreshEnv.cmd batch script found at:
;        https://github.com/chocolatey-archive/chocolatey/blob/master/src/redirects/RefreshEnv.cmd

#Requires AutoHotkey v2.0-beta.3+

Env_UserAdd(name, value, type := "", location := ""){
   value    := (value ~= "^\.\.\\") ? GetFullPathName(value) : value
   location := (location == "")     ? "HKCU\Environment"     : location

   registry := RegRead(location, name)


   Loop Parse, registry, ";"
      if (A_LoopField == value)
         return -2
   registry .= (registry ~= "(^$|;$)") ? "" : ";"
   value := registry . value
   type := (type) ? type : (value ~= "%") ? "REG_EXPAND_SZ" : "REG_SZ"
   RegWrite value, type, location, name
   SettingChange()
   RefreshEnvironment()
   return 0
}

Env_SystemAdd(name, value, type := ""){
   return (A_IsAdmin) ? Env_UserAdd(name, value, type, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") : -3
}

Env_UserSub(name, value, type := "", location := ""){
   value    := (value ~= "^\.\.\\") ? GetFullPathName(value) : value
   location := (location == "")     ? "HKCU\Environment"     : location

   registry := RegRead(location, name)
   output := ""


   Loop Parse, registry, ";"
      if (A_LoopField != value) {
         output .= (A_Index > 1 && output != "") ? ";" : ""
         output .= A_LoopField
      }

   if (output == registry)
      return -2

   if (output != "") {
      type := (type) ? type : (output ~= "%") ? "REG_EXPAND_SZ" : "REG_SZ"
      RegWrite output, type, location, name
   }
   else
      RegDelete location, name
   SettingChange()
   RefreshEnvironment()
   return 0
}

Env_SystemSub(name, value, type := ""){
   return (A_IsAdmin) ? Env_UserSub(name, value, type, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") : -3
}

Env_UserNew(name, value := "", type := "", location := ""){
   type := (type) ? type : (value ~= "%") ? "REG_EXPAND_SZ" : "REG_SZ"
   RegWrite value, type, (location == "") ? "HKCU\Environment" : location, name
   SettingChange()
   RefreshEnvironment()
   return 0
}

Env_SystemNew(name, value := "", type := ""){
   return (A_IsAdmin) ? Env_UserNew(name, value, type, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") : -3
}

; Value does nothing except let me easily change between functions.
Env_UserDel(name, value := "", location := ""){
   RegDelete (location == "") ? "HKCU\Environment" : location, name
   SettingChange()
   RefreshEnvironment()
   return 0
}

Env_SystemDel(name, value := ""){
   return (A_IsAdmin) ? Env_UserDel(name, value, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") : -3
}

Env_UserRead(name, value := "", location := ""){
   registry := RegRead((location == "") ? "HKCU\Environment" : location, name)
   if (value != "") {
      Loop Parse, registry, ";"
         if (A_LoopField = value)
            return A_LoopField
      return ; Value not found
   }
   return registry
}

Env_SystemRead(name, value := ""){
   return Env_UserRead(name, value, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment")
}

; Value does nothing except let me easily change between functions.
Env_UserSort(name, value := "", location := ""){
   registry := RegRead((location == "") ? "HKCU\Environment" : location, name)
   registry := Sort(registry, "D;")
   type := (registry ~= "%") ? "REG_EXPAND_SZ" : "REG_SZ"
   RegWrite registry, type, (location == "") ? "HKCU\Environment" : location, name
   return 0
}

Env_SystemSort(name, value := ""){
   return (A_IsAdmin) ? Env_UserSort(name, value, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") : -3
}

; Value does nothing except let me easily change between functions.
Env_UserRemoveDuplicates(name, value := "", location := ""){
   registry := RegRead((location == "") ? "HKCU\Environment" : location, name)
   registry := Sort(registry, "U D;")
   type := (type) ? type : (registry ~= "%") ? "REG_EXPAND_SZ" : "REG_SZ"
   RegWrite registry, type, (location == "") ? "HKCU\Environment" : location, name
   return 0
}

Env_SystemRemoveDuplicates(name, value := ""){
   return (A_IsAdmin) ? Env_UserRemoveDuplicates(name, value, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment") : -3
}

Env_UserBackup(fileName := "UserEnvironment.reg", location := ""){
   _cmd .= (A_Is64bitOS != A_PtrSize >> 3)    ? A_WinDir "\SysNative\cmd.exe"   : A_ComSpec
   _cmd .= " /K " Chr(0x22) "reg export " Chr(0x22)
   _cmd .= (location == "")                   ? "HKCU\Environment" : location
   _cmd .= Chr(0x22) " " Chr(0x22)
   _cmd .= fileName
   _cmd .= Chr(0x22) . Chr(0x22) . " && pause && exit"
   try RunWait _cmd
   catch
      return "FAIL"
   return "SUCCESS"
}

Env_SystemBackup(fileName := "SystemEnvironment.reg"){
   return Env_UserBackup(fileName, "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment")
}

Env_UserRestore(fileName := "UserEnvironment.reg"){
   try RunWait fileName
   catch
      return "FAIL"
   return "SUCCESS"
}

Env_SystemRestore(fileName := "SystemEnvironment.reg"){
   try RunWait fileName
   catch
      return "FAIL"
   return "SUCCESS"
}


RefreshEnvironment()
{
   Path := ""
   PathExt := ""
   RegKeys := "HKLM\SYSTEM\CurrentControlSet\Control\Session Manager\Environment,HKCU\Environment"
   Loop Parse, RegKeys, "CSV"
   {
      Loop Reg, A_LoopField, "V"
      {
         Value := RegRead()
         If (A_LoopRegType == "REG_EXPAND_SZ" && !ExpandEnvironmentStrings(&Value))
            Continue
         If (A_LoopRegName = "PATH")
            Path .= Value . ";"
         Else If (A_LoopRegName = "PATHEXT")
            PathExt .= Value . ";"
         Else
            EnvSet A_LoopRegName, Value
      }
   }
   EnvSet PATH, Path
   EnvSet PATHEXT, PathExt
}

ExpandEnvironmentStrings(&vInputString)
{
   ; get the required size for the expanded string
   vSizeNeeded := DllCall("ExpandEnvironmentStrings", "Str", vInputString, "Int", 0, "Int", 0)
   If (vSizeNeeded == "" || vSizeNeeded <= 0)
      return False ; unable to get the size for the expanded string for some reason

   vByteSize := vSizeNeeded + 1
   VarSetStrCapacity(&vTempValue, vByteSize)

   ; attempt to expand the environment string
   If (!DllCall("ExpandEnvironmentStrings", "Str", vInputString, "Str", vTempValue, "Int", vSizeNeeded))
      return False ; unable to expand the environment string
   vInputString := vTempValue

   ; return success
   Return True
}

GetFullPathName(path) {
    cc := DllCall("GetFullPathName", "str", path, "uint", 0, "ptr", 0, "ptr", 0, "uint")
    VarSetStrCapacity(&buf, cc)
    DllCall("GetFullPathName", "str", path, "uint", cc, "str", buf, "ptr", 0, "uint")
    return buf
}


; Source: https://gist.github.com/alphp/78fffb6d69e5bb863c76bbfc767effda
/*
$Script = @'
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);
"@

function Send-SettingChange {
  $HWND_BROADCAST = [IntPtr] 0xffff;
  $WM_SETTINGCHANGE = 0x1a;
  $result = [UIntPtr]::Zero

  [void] ([Win32.Nativemethods]::SendMessageTimeout($HWND_BROADCAST, $WM_SETTINGCHANGE, [UIntPtr]::Zero, "Environment", 2, 5000, [ref] $result))
}

Send-SettingChange;
'@

$ByteScript  = [System.Text.Encoding]::Unicode.GetBytes($Script)
[System.Convert]::ToBase64String($ByteScript)
*/

; To verify the encoded command, start a powershell terminal and paste the script above.
; 10x faster than SendMessage 0x1A, 0, "Environment",, ahk_id 0xFFFF ; 0x1A is WM_SETTINGCHANGE
SettingChange() {

   static _cmd := "
   ( LTrim
   QQBkAGQALQBUAHkAcABlACAALQBOAGEAbQBlAHMAcABhAGMAZQAgAFcAaQBuADMA
   MgAgAC0ATgBhAG0AZQAgAE4AYQB0AGkAdgBlAE0AZQB0AGgAbwBkAHMAIAAtAE0A
   ZQBtAGIAZQByAEQAZQBmAGkAbgBpAHQAaQBvAG4AIABAACIACgAgACAAWwBEAGwA
   bABJAG0AcABvAHIAdAAoACIAdQBzAGUAcgAzADIALgBkAGwAbAAiACwAIABTAGUA
   dABMAGEAcwB0AEUAcgByAG8AcgAgAD0AIAB0AHIAdQBlACwAIABDAGgAYQByAFMA
   ZQB0ACAAPQAgAEMAaABhAHIAUwBlAHQALgBBAHUAdABvACkAXQAKACAAIABwAHUA
   YgBsAGkAYwAgAHMAdABhAHQAaQBjACAAZQB4AHQAZQByAG4AIABJAG4AdABQAHQA
   cgAgAFMAZQBuAGQATQBlAHMAcwBhAGcAZQBUAGkAbQBlAG8AdQB0ACgASQBuAHQA
   UAB0AHIAIABoAFcAbgBkACwAIAB1AGkAbgB0ACAATQBzAGcALAAgAFUASQBuAHQA
   UAB0AHIAIAB3AFAAYQByAGEAbQAsACAAcwB0AHIAaQBuAGcAIABsAFAAYQByAGEA
   bQAsACAAdQBpAG4AdAAgAGYAdQBGAGwAYQBnAHMALAAgAHUAaQBuAHQAIAB1AFQA
   aQBtAGUAbwB1AHQALAAgAG8AdQB0ACAAVQBJAG4AdABQAHQAcgAgAGwAcABkAHcA
   UgBlAHMAdQBsAHQAKQA7AAoAIgBAAAoACgBmAHUAbgBjAHQAaQBvAG4AIABTAGUA
   bgBkAC0AUwBlAHQAdABpAG4AZwBDAGgAYQBuAGcAZQAgAHsACgAgACAAJABIAFcA
   TgBEAF8AQgBSAE8AQQBEAEMAQQBTAFQAIAA9ACAAWwBJAG4AdABQAHQAcgBdACAA
   MAB4AGYAZgBmAGYAOwAKACAAIAAkAFcATQBfAFMARQBUAFQASQBOAEcAQwBIAEEA
   TgBHAEUAIAA9ACAAMAB4ADEAYQA7AAoAIAAgACQAcgBlAHMAdQBsAHQAIAA9ACAA
   WwBVAEkAbgB0AFAAdAByAF0AOgA6AFoAZQByAG8ACgAKACAAIABbAHYAbwBpAGQA
   XQAgACgAWwBXAGkAbgAzADIALgBOAGEAdABpAHYAZQBtAGUAdABoAG8AZABzAF0A
   OgA6AFMAZQBuAGQATQBlAHMAcwBhAGcAZQBUAGkAbQBlAG8AdQB0ACgAJABIAFcA
   TgBEAF8AQgBSAE8AQQBEAEMAQQBTAFQALAAgACQAVwBNAF8AUwBFAFQAVABJAE4A
   RwBDAEgAQQBOAEcARQAsACAAWwBVAEkAbgB0AFAAdAByAF0AOgA6AFoAZQByAG8A
   LAAgACIARQBuAHYAaQByAG8AbgBtAGUAbgB0ACIALAAgADIALAAgADUAMAAwADAA
   LAAgAFsAcgBlAGYAXQAgACQAcgBlAHMAdQBsAHQAKQApAAoAfQAKAAoAUwBlAG4A
   ZAAtAFMAZQB0AHQAaQBuAGcAQwBoAGEAbgBnAGUAOwA=
   )"
   Run "powershell -NoProfile -EncodedCommand " _cmd,, "Hide"
}
Feel free to submit any bugs.
https://github.com/iseahound/Environment.ahk <- LATEST VERSION

bonobo
Posts: 75
Joined: 03 Sep 2023, 20:13

Re: Environment.ahk - Change User and System Environment Variables Permanently

Post by bonobo » 03 Dec 2023, 16:34

Thank you for this very useful script.

I noticed that the included GetFullPathName function

Code: Select all

GetFullPathName(path) {
    cc := DllCall("GetFullPathName", "str", path, "uint", 0, "ptr", 0, "ptr", 0, "uint")
    VarSetStrCapacity(&buf, cc)
    DllCall("GetFullPathName", "str", path, "uint", cc, "str", buf, "ptr", 0, "uint")
    return buf
}
differs slightly from the one I have copied previously from some source:

Code: Select all

GetFullPathName(path) {
    cc := DllCall("GetFullPathName", "str", path, "uint", 0, "ptr", 0, "ptr", 0, "uint")
    VarSetStrCapacity(&buf, cc*(1?2:1))
    DllCall("GetFullPathName", "str", path, "uint", cc, "str", buf, "ptr", 0, "uint")
	return buf
}
Is there an actual difference between VarSetStrCapacity(&buf, cc) and VarSetStrCapacity(&buf, cc*(1?2:1))?
I'm not clear on what cc*(1?2:1) is even doing here

ntepa
Posts: 406
Joined: 19 Oct 2022, 20:52

Re: Environment.ahk - Change User and System Environment Variables Permanently

Post by ntepa » 04 Dec 2023, 01:39

bonobo wrote:
03 Dec 2023, 16:34
Is there an actual difference between VarSetStrCapacity(&buf, cc) and VarSetStrCapacity(&buf, cc*(1?2:1))?
(1?2:1) is a ternary operation, which is a shorthand for an if-else statement. It checks if a condition is true or false, and then returns one of two expressions based on the result.
1 is always true, so cc*(1?2:1) will always evaluate to cc*2.
bonobo wrote:
03 Dec 2023, 16:34
I'm not clear on what cc*(1?2:1) is even doing here
In v1, the code would be:

Code: Select all

VarSetCapacity(buf, length*(A_IsUnicode?2:1))
AHK v2 does not have the variable A_IsUnicode since it's only compiled for Unicode. When converting from v1 to v2, A_IsUnicode can be replaced with 1.
The second parameter of VarSetStrCapacity is number of characters, not number of bytes, so cc*(1?2:1) is incorrect.

bonobo
Posts: 75
Joined: 03 Sep 2023, 20:13

Re: Environment.ahk - Change User and System Environment Variables Permanently

Post by bonobo » 04 Dec 2023, 06:50

ntepa wrote:
04 Dec 2023, 01:39
bonobo wrote:
03 Dec 2023, 16:34
Is there an actual difference between VarSetStrCapacity(&buf, cc) and VarSetStrCapacity(&buf, cc*(1?2:1))?
(1?2:1) is a ternary operation, which is a shorthand for an if-else statement. It checks if a condition is true or false, and then returns one of two expressions based on the result.
1 is always true, so cc*(1?2:1) will always evaluate to cc*2.
bonobo wrote:
03 Dec 2023, 16:34
I'm not clear on what cc*(1?2:1) is even doing here
In v1, the code would be:

Code: Select all

VarSetCapacity(buf, length*(A_IsUnicode?2:1))
AHK v2 does not have the variable A_IsUnicode since it's only compiled for Unicode. When converting from v1 to v2, A_IsUnicode can be replaced with 1.
The second parameter of VarSetStrCapacity is number of characters, not number of bytes, so cc*(1?2:1) is incorrect.
I see now, it's an artifact of the v1 to v2 convertor. Thanks for taking the time to explain!

iseahound
Posts: 1434
Joined: 13 Aug 2016, 21:04
Contact:

Re: Environment.ahk - Change User and System Environment Variables Permanently

Post by iseahound » 05 Dec 2023, 04:59

Ah you scared me into thinking I had that in my code. The VarSetStrCapacity - note the bold - means that the *(A_IsUnicode?2:1) is now a part of the memory allocator.

Post Reply

Return to “Scripts and Functions (v2)”