playing with PATH on Windows

This commit is contained in:
AJ ONeal 2019-07-16 01:54:58 -06:00
parent 4e0724327e
commit c88dcf22b6
6 changed files with 298 additions and 0 deletions

2
.gitignore vendored
View File

@ -1,3 +1,5 @@
*~
# ---> Go
# Binaries for programs and plugins
*.exe

63
winpath/README.md Normal file
View File

@ -0,0 +1,63 @@
# winpath
An example of getting, setting, and broadcasting PATHs on Windows.
This requires the `unsafe` package to use a syscall with special message poitners to update `PATH` without a reboot.
It will also build without `unsafe`.
```bash
go build -tags unsafe -o winpath.exe
```
```bash
winpath show
%USERPROFILE%\AppData\Local\Microsoft\WindowsApps
C:\Users\me\AppData\Local\Programs\Microsoft VS Code\bin
%USERPROFILE%\go\bin
C:\Users\me\AppData\Roaming\npm
C:\Users\me\AppData\Local\Keybase\
```
```bash
winpath append C:\someplace\special
Run the following for changes to take affect immediately:
PATH %PATH%;C:\someplace\special
```
```bash
winpath prepend C:\someplace\special
Run the following for changes to take affect immediately:
PATH C:\someplace\special;%PATH%
```
```bash
winpath remove C:\someplace\special
```
# Special Considerations
Giving away the secret sauce right here:
* `HWND_BROADCAST`
* `WM_SETTINGCHANGE`
This is essentially the snippet you need to have the HKCU and HKLM Environment registry keys propagated without rebooting:
```go
HWND_BROADCAST := uintptr(0xffff)
WM_SETTINGCHANGE := uintptr(0x001A)
_, _, err := syscall.
NewLazyDLL("user32.dll").
NewProc("SendMessageW").
Call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("ENVIRONMENT"))))
```
* `os.Getenv("COMSPEC")`
* `os.Getenv("SHELL")`
If you check `SHELL` and it isn't empty, then you're probably in MINGW or some such.
If that's empty but `COMSPEC` isn't, you can be reasonably sure that you're in cmd.exe or Powershell.

5
winpath/go.mod Normal file
View File

@ -0,0 +1,5 @@
module git.coolaj86.com\coolaj86\go-examples\winpath
go 1.12
require golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7

2
winpath/go.sum Normal file
View File

@ -0,0 +1,2 @@
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 h1:LepdCS8Gf/MVejFIt8lsiexZATdoGVyp5bcyS+rYoUI=
golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=

197
winpath/winpath.go Normal file
View File

@ -0,0 +1,197 @@
// +build windows
// We both need to
// * use the registry editor directly to avoid possible PATH truncation
// ( https://stackoverflow.com/questions/9546324/adding-directory-to-path-environment-variable-in-windows )
// ( https://superuser.com/questions/387619/overcoming-the-1024-character-limit-with-setx )
// * explicitly send WM_SETTINGCHANGE
// ( https://github.com/golang/go/issues/18680#issuecomment-275582179 )
// * also install as a service
// ( https://github.com/golang/sys/blob/master/windows/svc/example/install.go )
package main
import (
"fmt"
"os"
"path/filepath"
"strings"
"golang.org/x/sys/windows/registry"
)
var sendmsg func()
func usage() {
fmt.Fprintf(os.Stderr, "Usage: winpath show|append|prepend|remove <path>\n")
}
func main() {
fmt.Println("PATH:", os.Getenv("PATH"))
//fpath, err := exec.LookPath("reg")
//fmt.Println("LookPath(\"reg\"):", fpath, err)
shell := os.Getenv("SHELL")
if "" == shell {
if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") {
shell = "cmd"
}
}
fmt.Println("SHELL?", shell)
fmt.Println("os.PathListSeparator:", string(os.PathListSeparator))
// WM_SETTING_CHANGE
// https://gist.github.com/microo8/c1b9525efab9bb462adf9d123e855c52
// os.Setenv("PATH")
// TODO --system to add to the system PATH rather than the user PATH
if len(os.Args) < 2 {
usage()
os.Exit(1)
}
action := os.Args[1]
paths, err := Paths()
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(2)
}
if "show" == action {
if len(os.Args) > 2 {
usage()
}
fmt.Println()
for i := range paths {
fmt.Println("\t" + paths[i])
}
fmt.Println()
return
}
if len(os.Args) < 3 {
usage()
os.Exit(1)
}
pathname := os.Args[2]
abspath, err := filepath.Abs(pathname)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(2)
}
if "remove" != action {
stat, err := os.Stat(pathname)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(2)
}
if !stat.IsDir() {
fmt.Fprintf(os.Stderr, "%q is not a directory (folder)\n", pathname)
os.Exit(2)
}
}
index := -1
for i := range paths {
if pathname == paths[i] {
index = i
break
}
if abspath == paths[i] {
index = i
break
}
}
switch action {
default:
usage()
os.Exit(1)
case "append":
if index >= 0 {
fmt.Fprintf(os.Stderr, "%q is already in PATH at position %d. Remove it first to re-order.\n", pathname, index)
os.Exit(3)
}
paths = append(paths, pathname)
fmt.Println("Run this to cause settings to take affect immediately:")
fmt.Println("\tPATH %PATH%;" + pathname)
case "prepend":
if index >= 0 {
fmt.Fprintf(os.Stderr, "%q is already in PATH at position %d. Remove it first to re-order.\n", pathname, index)
os.Exit(3)
}
paths = append([]string{pathname}, paths...)
fmt.Println("Run this to cause settings to take affect immediately:")
fmt.Println("\tPATH " + pathname + ";%PATH%")
case "remove":
if index < 0 {
fmt.Fprintf(os.Stderr, "%q is NOT in PATH.\n", pathname)
os.Exit(3)
}
oldpaths := paths
paths = []string{}
for i := range oldpaths {
if i != index {
paths = append(paths, oldpaths[i])
}
}
}
k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.SET_VALUE)
if err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(4)
}
defer k.Close()
// ";" on Windows
err = k.SetStringValue(`Path`, strings.Join(paths, string(os.PathListSeparator)))
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(4)
}
err = k.Close()
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(4)
}
err = os.Setenv(`PATH`, strings.Join(paths, string(os.PathListSeparator)))
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(4)
}
if nil != sendmsg {
fmt.Println("Open a new Terminal for the updated PATH")
sendmsg()
} else {
fmt.Println("You'll need to reboot for setting to take affect")
}
}
func Paths() ([]string, error) {
k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE)
if err != nil {
return nil, err
}
defer k.Close()
// This is case insensitive (PATH, Path, path)
s, _, err := k.GetStringValue("Path")
if err != nil {
return nil, err
}
// ";" on Windows
return strings.Split(s, string(os.PathListSeparator)), nil
/*
shstr := loader()
if "" == shstr {
shstr = "NO_SHELL_DETECTED"
}
if "" == s {
s = "NO_WINREG_PATH"
}
return fmt.Sprintf("%s\n%s", s, shstr)
*/
}

29
winpath/winpath_unsafe.go Normal file
View File

@ -0,0 +1,29 @@
// +build windows,unsafe
package main
import (
"os"
"fmt"
"syscall"
"unsafe"
)
const (
HWND_BROADCAST = uintptr(0xffff)
WM_SETTINGCHANGE = uintptr(0x001A)
)
func init() {
sendmsg = func() {
//x, y, err := syscall.
_, _, err := syscall.
NewLazyDLL("user32.dll").
NewProc("SendMessageW").
Call(HWND_BROADCAST, WM_SETTINGCHANGE, 0, uintptr(unsafe.Pointer(syscall.StringToUTF16Ptr("ENVIRONMENT"))))
//fmt.Fprintf(os.Stderr, "%d, %d, %s\n", x, y, err)
if nil != err {
fmt.Fprintf(os.Stderr, "%s\n", err)
}
}
}