happy parser
This commit is contained in:
parent
a1a5aa81e3
commit
d850aff673
|
@ -0,0 +1,97 @@
|
||||||
|
package envpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Warning struct {
|
||||||
|
LineNumber int
|
||||||
|
Line string
|
||||||
|
Message string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse will return a list of paths from an export file
|
||||||
|
func Parse(envname string, b []byte) ([]string, []Warning) {
|
||||||
|
s := string(b)
|
||||||
|
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||||
|
|
||||||
|
badlines := []Warning{}
|
||||||
|
newlines := []string{}
|
||||||
|
entries := make(map[string]bool)
|
||||||
|
lines := strings.Split(s, "\n")
|
||||||
|
for i := range lines {
|
||||||
|
line := strings.TrimPrefix(strings.TrimSpace(lines[i]), "export ")
|
||||||
|
if "" == line {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if "# Generated for envman. Do not edit." == line {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if '#' == line[0] {
|
||||||
|
badlines = append(badlines, Warning{
|
||||||
|
LineNumber: i,
|
||||||
|
Line: line,
|
||||||
|
Message: "comment",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
index := strings.Index(line, "=")
|
||||||
|
if index < 1 {
|
||||||
|
badlines = append(badlines, Warning{
|
||||||
|
LineNumber: i,
|
||||||
|
Line: line,
|
||||||
|
Message: "invalid assignment",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
env := line[:index]
|
||||||
|
if env != envname {
|
||||||
|
badlines = append(badlines, Warning{
|
||||||
|
LineNumber: i,
|
||||||
|
Line: line,
|
||||||
|
Message: fmt.Sprintf("wrong name (%s != %s)", env, envname),
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
val := line[index+1:]
|
||||||
|
if len(val) < 2 || '"' != val[0] || '"' != val[len(val)-1] {
|
||||||
|
badlines = append(badlines, Warning{
|
||||||
|
LineNumber: i,
|
||||||
|
Line: line,
|
||||||
|
Message: "value not quoted",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = val[1 : len(val)-1]
|
||||||
|
|
||||||
|
if strings.Contains(val, `"`) {
|
||||||
|
badlines = append(badlines, Warning{
|
||||||
|
LineNumber: i,
|
||||||
|
Line: line,
|
||||||
|
Message: "invalid quotes",
|
||||||
|
})
|
||||||
|
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO normalize $HOME
|
||||||
|
if entries[val] {
|
||||||
|
badlines = append(badlines, Warning{
|
||||||
|
LineNumber: i,
|
||||||
|
Line: line,
|
||||||
|
Message: "duplicate entry",
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
entries[val] = true
|
||||||
|
|
||||||
|
newlines = append(newlines, val)
|
||||||
|
}
|
||||||
|
|
||||||
|
return newlines, badlines
|
||||||
|
}
|
|
@ -0,0 +1,56 @@
|
||||||
|
package envpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const file = `# Generated for envman. Do not edit.
|
||||||
|
PATH="/foo"
|
||||||
|
|
||||||
|
|
||||||
|
# ignore
|
||||||
|
# ignore
|
||||||
|
|
||||||
|
PATH="/foo"
|
||||||
|
PATH="/foo:$PATH"
|
||||||
|
PATH="/foo:$PATH"
|
||||||
|
PATH="/foo:"$PATH"
|
||||||
|
PATH="/foo:""$PATH"
|
||||||
|
PATH=""
|
||||||
|
|
||||||
|
PATH=
|
||||||
|
|
||||||
|
JUNK=""
|
||||||
|
JUNK=
|
||||||
|
=""
|
||||||
|
=
|
||||||
|
|
||||||
|
whatever
|
||||||
|
|
||||||
|
|
||||||
|
PATH="/boo:$PATH"
|
||||||
|
PATH=""
|
||||||
|
|
||||||
|
`
|
||||||
|
|
||||||
|
var paths = []string{
|
||||||
|
`PATH="/foo"`,
|
||||||
|
`PATH="/foo:$PATH"`,
|
||||||
|
`PATH=""`,
|
||||||
|
`PATH="/boo:$PATH"`,
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestParse(t *testing.T) {
|
||||||
|
newlines, warnings := Parse("PATH", []byte(file))
|
||||||
|
newfile := `PATH="` + strings.Join(newlines, "\"\n\tPATH=\"") + `"`
|
||||||
|
expfile := strings.Join(paths, "\n\t")
|
||||||
|
if newfile != expfile {
|
||||||
|
t.Errorf("\nExpected:\n\t%s\nGot:\n\t%s", expfile, newfile)
|
||||||
|
}
|
||||||
|
for i := range warnings {
|
||||||
|
w := warnings[i]
|
||||||
|
fmt.Printf("warning dropping %q from line %d: %s\n", w.Message, w.LineNumber, w.Line)
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,5 @@
|
||||||
|
module git.rootprojects.org/root/pathman
|
||||||
|
|
||||||
|
go 1.12
|
||||||
|
|
||||||
|
require golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7
|
|
@ -0,0 +1,229 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func usage() {
|
||||||
|
fmt.Fprintf(os.Stdout, "Usage: envpath <action> [path]\n")
|
||||||
|
fmt.Fprintf(os.Stdout, "\tex: envpath list\n")
|
||||||
|
fmt.Fprintf(os.Stdout, "\tex: envpath add ~/.local/bin\n")
|
||||||
|
fmt.Fprintf(os.Stdout, "\tex: envpath remove ~/.local/bin\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
var action string
|
||||||
|
var entry string
|
||||||
|
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
} else if len(os.Args) > 3 {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
action = os.Args[1]
|
||||||
|
if 2 == len(os.Args) {
|
||||||
|
entry = os.Args[2]
|
||||||
|
}
|
||||||
|
|
||||||
|
// https://superuser.com/a/69190/73857
|
||||||
|
// https://github.com/rust-lang-nursery/rustup.rs/issues/686#issuecomment-253982841
|
||||||
|
// exec source $HOME/.profile
|
||||||
|
shell := os.Getenv("SHELL")
|
||||||
|
switch shell {
|
||||||
|
case "":
|
||||||
|
if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") {
|
||||||
|
shell = "cmd"
|
||||||
|
}
|
||||||
|
case "fish":
|
||||||
|
// ignore
|
||||||
|
case "zsh":
|
||||||
|
// ignore
|
||||||
|
case "bash":
|
||||||
|
// ignore
|
||||||
|
default:
|
||||||
|
// warn and try anyway
|
||||||
|
fmt.Fprintf(
|
||||||
|
os.Stderr,
|
||||||
|
"%q isn't a recognized shell. Please open an issue at https://git.rootprojects.org/envpath/issues?q=%s",
|
||||||
|
shell,
|
||||||
|
shell,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "list":
|
||||||
|
if 2 == len(os.Args) {
|
||||||
|
usage()
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
list()
|
||||||
|
case "add":
|
||||||
|
add(entry)
|
||||||
|
case "remove":
|
||||||
|
remove(entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func list() {
|
||||||
|
managedpaths, err := listPaths()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "%s", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("pathman-managed PATH entries:\n")
|
||||||
|
for i := range managedpaths {
|
||||||
|
fmt.Println("\t" + managedpaths[i])
|
||||||
|
}
|
||||||
|
if 0 == len(managedpaths) {
|
||||||
|
fmt.Println("\t(none)")
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
|
||||||
|
fmt.Println("other PATH entries:\n")
|
||||||
|
// All managed paths
|
||||||
|
pathsmap := map[string]bool{}
|
||||||
|
for i := range managedpaths {
|
||||||
|
// TODO normalize
|
||||||
|
pathsmap[managedpaths[i]] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths in the environment which are not managed
|
||||||
|
var hasExtras bool
|
||||||
|
envpaths := Paths()
|
||||||
|
for i := range envpaths {
|
||||||
|
// TODO normalize
|
||||||
|
path := envpaths[i]
|
||||||
|
if !pathsmap[path] {
|
||||||
|
hasExtras = true
|
||||||
|
fmt.Println("\t" + path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !hasExtras {
|
||||||
|
fmt.Println("\t(none)")
|
||||||
|
}
|
||||||
|
fmt.Println("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(entry string) {
|
||||||
|
// TODO noramlize away $HOME, %USERPROFILE%, etc
|
||||||
|
abspath, err := filepath.Abs(entry)
|
||||||
|
stat, err := os.Stat(entry)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: couldn't access %q: %s\n", abspath, err)
|
||||||
|
} else if !stat.IsDir() {
|
||||||
|
fmt.Fprintf(os.Stderr, "warning: %q is not a directory", abspath)
|
||||||
|
}
|
||||||
|
|
||||||
|
modified, err := addPath(entry)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to add %q to PATH: %s", entry, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg string
|
||||||
|
if modified {
|
||||||
|
msg = "Saved PATH changes."
|
||||||
|
} else {
|
||||||
|
msg = "PATH not changed."
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := Paths()
|
||||||
|
index := indexOfPath(Paths(), entry)
|
||||||
|
if -1 == index {
|
||||||
|
// TODO is os.PathListSeparator correct in MINGW / git bash?
|
||||||
|
// generally this has no effect, but just in case this is included in a library with children processes
|
||||||
|
paths = append([]string{entry}, paths...)
|
||||||
|
err = os.Setenv(`PATH`, strings.Join(paths, string(os.PathListSeparator)))
|
||||||
|
if nil != err {
|
||||||
|
// ignore and carry on, as this is optional
|
||||||
|
fmt.Fprintf(os.Stderr, "%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg += " To set the PATH immediately, update the current session:\n\n\t" + Add(entry) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(msg + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(entry string) {
|
||||||
|
modified, err := removePath(entry)
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "failed to add %q to PATH: %s", entry, err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg string
|
||||||
|
if modified {
|
||||||
|
msg = "Saved PATH changes."
|
||||||
|
} else {
|
||||||
|
msg = "PATH not changed."
|
||||||
|
}
|
||||||
|
|
||||||
|
paths := Paths()
|
||||||
|
index := indexOfPath(Paths(), entry)
|
||||||
|
if index >= 0 {
|
||||||
|
newpaths := []string{}
|
||||||
|
for i := range paths {
|
||||||
|
if i != index {
|
||||||
|
newpaths = append(newpaths, paths[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// TODO is os.PathListSeparator correct in MINGW / git bash?
|
||||||
|
// generally this has no effect, but just in case this is included in a library with children processes
|
||||||
|
err = os.Setenv(`PATH`, strings.Join(newpaths, string(os.PathListSeparator)))
|
||||||
|
if nil != err {
|
||||||
|
// ignore and carry on, as this is optional
|
||||||
|
fmt.Fprintf(os.Stderr, "%s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
msg += " To set the PATH immediately, update the current session:\n\n\t" + Remove(entry) + "\n"
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println(msg + "\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths returns path entries in the current environment
|
||||||
|
func Paths() []string {
|
||||||
|
cur := os.Getenv("PATH")
|
||||||
|
if "" == cur {
|
||||||
|
// unlikely, but possible... so whatever
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isCmdExe() {
|
||||||
|
//return strings.Split(cur, string(os.PathListSeparator))
|
||||||
|
return strings.Split(cur, ";")
|
||||||
|
}
|
||||||
|
return strings.Split(cur, string(os.PathListSeparator))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add returns a string which can be used to add the given
|
||||||
|
// path entry to the current shell session
|
||||||
|
func Add(p string) string {
|
||||||
|
if isCmdExe() {
|
||||||
|
return fmt.Sprintf(`PATH %s;%PATH%`, p)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`export PATH="%s:$PATH"`, p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove returns a string which can be used to remove the given
|
||||||
|
// path entry from the current shell session
|
||||||
|
func Remove(entries []string) string {
|
||||||
|
if isCmdExe() {
|
||||||
|
return fmt.Sprintf(`PATH %s`, strings.Join(entries, ";"))
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`export PATH="%s"`, strings.Join(entries, ":"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func isCmdExe() {
|
||||||
|
return "" == os.Getenv("SHELL") && strings.Contains(strings.ToLower(os.Getenv("COMSPEC")), "/cmd.exe")
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.rootprojects.org/root/pathman/envpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addPath(p string) (bool, error) {
|
||||||
|
return envpath.Add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePath(p string) (bool, error) {
|
||||||
|
return envpath.Remove(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listPaths() ([]string, error) {
|
||||||
|
return envpath.List()
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOfPath(cur []string, p string) int {
|
||||||
|
return envpath.IndexOf(cur, p)
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"git.rootprojects.org/root/pathman/winpath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addPath(p string) (bool, error) {
|
||||||
|
return winpath.Add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removePath(p string) (bool, error) {
|
||||||
|
return winpath.Remove(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func listPaths() ([]string, error) {
|
||||||
|
return winpath.List()
|
||||||
|
}
|
||||||
|
|
||||||
|
func indexOfPath(cur []string, p string) int {
|
||||||
|
return winpath.IndexOf(cur, p)
|
||||||
|
}
|
|
@ -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.
|
|
@ -0,0 +1,16 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winpath
|
||||||
|
|
||||||
|
import "testing"
|
||||||
|
|
||||||
|
func TestShow(t *testing.T) {
|
||||||
|
paths, err := Paths()
|
||||||
|
if nil != err {
|
||||||
|
t.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(paths) < 1 {
|
||||||
|
t.Error("should have paths")
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,94 @@
|
||||||
|
// Package winpath is useful for managing PATH as part of the Environment
|
||||||
|
// in the Windows HKey Local User registry. It returns an error for most
|
||||||
|
// operations on non-Windows systems.
|
||||||
|
package winpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ErrWrongPlatform indicates that this was not built for Windows
|
||||||
|
var ErrWrongPlatform = fmt.Errorf("method not implemented on this platform")
|
||||||
|
|
||||||
|
// sendmsg uses a syscall to broadcast the registry change so that
|
||||||
|
// new shells will get the new PATH immediately, without a reboot
|
||||||
|
var sendmsg func()
|
||||||
|
|
||||||
|
// Paths returns all PATHs according to the Windows HKLU registry
|
||||||
|
// (or nil on non-windows platforms)
|
||||||
|
func Paths() ([]string, error) {
|
||||||
|
return paths()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add will rewrite the Windows registry HKLU Environment,
|
||||||
|
// prepending the given directory path to the user's PATH.
|
||||||
|
// It will return whether the PATH was modified and an
|
||||||
|
// error if it should have been modified, but wasn't.
|
||||||
|
func Add(p string) (bool, error) {
|
||||||
|
return add(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove will rewrite the Windows registry HKLU Environment
|
||||||
|
// without the given directory path.
|
||||||
|
// It will return whether the PATH was modified and an
|
||||||
|
// error if it should have been modified, but wasn't.
|
||||||
|
func Remove(p string) (bool, error) {
|
||||||
|
return remove(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizePathEntry will return the given directory path relative
|
||||||
|
// from its absolute path to the %USERPROFILE% (home) directory.
|
||||||
|
func NormalizePathEntry(pathentry string) (string, string) {
|
||||||
|
home, err := os.UserHomeDir()
|
||||||
|
if nil != err {
|
||||||
|
fmt.Fprintf(os.Stderr, "Couldn't get HOME directory. That's an unrecoverable hard fail.")
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sep := string(os.PathSeparator)
|
||||||
|
absentry, _ := filepath.Abs(pathentry)
|
||||||
|
home, _ = filepath.Abs(home)
|
||||||
|
|
||||||
|
var homeentry string
|
||||||
|
if strings.HasPrefix(strings.ToLower(absentry)+sep, strings.ToLower(home)+sep) {
|
||||||
|
// %USERPROFILE% is allowed, but only for user PATH
|
||||||
|
// https://superuser.com/a/442163/73857
|
||||||
|
homeentry = `%USERPROFILE%` + pathentry[len(home):]
|
||||||
|
}
|
||||||
|
|
||||||
|
if absentry == pathentry {
|
||||||
|
absentry = ""
|
||||||
|
}
|
||||||
|
if homeentry == pathentry {
|
||||||
|
homeentry = ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return absentry, homeentry
|
||||||
|
}
|
||||||
|
|
||||||
|
// IndexOf searches the given path list for first occurence
|
||||||
|
// of the given path entry and returns the index, or -1
|
||||||
|
func IndexOf(paths []string, p string) int {
|
||||||
|
abspath, homepath := NormalizePathEntry(p)
|
||||||
|
|
||||||
|
index := -1
|
||||||
|
for i := range paths {
|
||||||
|
if strings.ToLower(p) == strings.ToLower(paths[i]) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.ToLower(abspath) == strings.ToLower(paths[i]) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if strings.ToLower(homepath) == strings.ToLower(paths[i]) {
|
||||||
|
index = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return index
|
||||||
|
}
|
|
@ -0,0 +1,15 @@
|
||||||
|
// +build !windows
|
||||||
|
|
||||||
|
package winpath
|
||||||
|
|
||||||
|
func paths() ([]string, error) {
|
||||||
|
return nil, ErrWrongPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func add(string) (bool, error) {
|
||||||
|
return false, ErrWrongPlatform
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(string) (bool, error) {
|
||||||
|
return false, ErrWrongPlatform
|
||||||
|
}
|
|
@ -0,0 +1,23 @@
|
||||||
|
package winpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalize(t *testing.T) {
|
||||||
|
home, _ := os.UserHomeDir()
|
||||||
|
|
||||||
|
absexp := ""
|
||||||
|
homeexp := "%USERPROFILE%" + string(os.PathSeparator) + "foo"
|
||||||
|
abspath, homepath := NormalizePathEntry(home + string(os.PathSeparator) + "foo")
|
||||||
|
|
||||||
|
if absexp != abspath {
|
||||||
|
t.Error(fmt.Errorf("Expected %q, but got %q", absexp, abspath))
|
||||||
|
}
|
||||||
|
|
||||||
|
if homeexp != homepath {
|
||||||
|
t.Error(fmt.Errorf("Expected %q, but got %q", homeexp, homepath))
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,32 @@
|
||||||
|
// +build windows,unsafe
|
||||||
|
|
||||||
|
package winpath
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"syscall"
|
||||||
|
"unsafe"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
HWND_BROADCAST = uintptr(0xffff)
|
||||||
|
WM_SETTINGCHANGE = uintptr(0x001A)
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
|
||||||
|
// WM_SETTING_CHANGE
|
||||||
|
// https://gist.github.com/microo8/c1b9525efab9bb462adf9d123e855c52
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,120 @@
|
||||||
|
// +build windows
|
||||||
|
|
||||||
|
package winpath
|
||||||
|
|
||||||
|
// Needs 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 )
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"golang.org/x/sys/windows/registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
func add(p string) (bool, error) {
|
||||||
|
cur, err := paths()
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
index := IndexOf(cur, p)
|
||||||
|
// skip silently, successfully
|
||||||
|
if index >= 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.SET_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
cur = append([]string{p}, cur...)
|
||||||
|
err = write(cur)
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove(p string) (bool, error) {
|
||||||
|
cur, err := paths()
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
index := findMatch(cur, p)
|
||||||
|
// skip silently, successfully
|
||||||
|
if index < 0 {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var newpaths []string
|
||||||
|
for i := range cur {
|
||||||
|
if i != index {
|
||||||
|
newpaths = append(newpaths, cur[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = write(cur)
|
||||||
|
if nil != err {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func write(cur []string) error {
|
||||||
|
// TODO --system to add to the system PATH rather than the user PATH
|
||||||
|
|
||||||
|
k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
err = k.SetStringValue(`Path`, strings.Join(cur, string(os.PathListSeparator)))
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = k.Close()
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if nil != sendmsg {
|
||||||
|
sendmsg()
|
||||||
|
} else {
|
||||||
|
fmt.Fprintf(os.Stderr, "Warning: added PATH, but you must reboot for changes to take effect\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func paths() ([]string, error) {
|
||||||
|
// This is the canonical reference, which is actually quite nice to have.
|
||||||
|
// TBH, it's a mess to do this on *nix systems.
|
||||||
|
k, err := registry.OpenKey(registry.CURRENT_USER, `Environment`, registry.QUERY_VALUE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer k.Close()
|
||||||
|
|
||||||
|
// This is case insensitive on Windows.
|
||||||
|
// PATH, Path, path will all work.
|
||||||
|
s, _, err := k.GetStringValue("Path")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// ";" on Windows
|
||||||
|
return strings.Split(s, string(os.PathListSeparator)), nil
|
||||||
|
}
|
Loading…
Reference in New Issue