diff --git a/.gitignore b/.gitignore index 9a3a8d8..7559971 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +/pathman +dist + # ---> Go # Binaries for programs and plugins *.exe diff --git a/README.md b/README.md index f71930f..3f505f5 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,82 @@ -# go-envpath +# [pathman](https://git.rootprojects.org/root/pathman) -Manage PATH on Windows, Mac, and Linux with various Shells \ No newline at end of file +Manage PATH on Windows, Mac, and Linux with various Shells + +```bash +pathman list +pathman add ~/.local/bin +pathman remove ~/.local/bin +``` + +Windows: stores PATH in the registry. + +Mac & Linux: stores PATH in `~/.config/envman/PATH.sh` + +# add + +```bash +pathman add ~/.local/bin +``` + +```txt +Saved PATH changes. To set the PATH immediately, update the current session: + + export PATH="/Users/me/.local/bin:$PATH" +``` + +# remove + +```bash +pathman remove ~/.local/bin +``` + +```txt +Saved PATH changes. To set the PATH immediately, update the current session: + + export PATH="/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin" +``` + +# list + +```bash +pathman list +``` + +```txt +pathman-managed PATH entries: + + $HOME/.local/bin + +other PATH entries: + + /usr/local/bin + /usr/bin + /bin + /usr/sbin + /sbin + +``` + +# Windows + +You can use `~` as a shortcut for `%USERPROFILE%`. + +```bash +pathman add ~\.local\bin +``` + +The registry will be used, even when your using Node Bash, Git Bash, or MINGW. + +# build + +```bash +git clone https://git.rootprojects.org/root/pathman.git +``` + +```bash +go mod tidy +go mod vendor +go generate -mod=vendor ./... +go build -mod=vendor +./pathman list +``` diff --git a/build-all.sh b/build-all.sh new file mode 100644 index 0000000..fb0bfa6 --- /dev/null +++ b/build-all.sh @@ -0,0 +1,43 @@ +#GOOS=windows GOARCH=amd64 go install +#go tool dist list + +# TODO move this into tools/build.go + +export CGO_ENABLED=0 +exe=pathman +gocmd=. + +echo "" +go generate -mod=vendor ./... + +echo "" +echo "Windows amd64" +GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.exe -ldflags "-s -w -H=windowsgui" $gocmd +GOOS=windows GOARCH=amd64 go build -mod=vendor -o dist/windows/amd64/${exe}.debug.exe +echo "Windows 386" +GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.exe -ldflags "-s -w -H=windowsgui" $gocmd +GOOS=windows GOARCH=386 go build -mod=vendor -o dist/windows/386/${exe}.debug.exe + +echo "" +echo "Darwin (macOS) amd64" +GOOS=darwin GOARCH=amd64 go build -mod=vendor -o dist/darwin/amd64/${exe} -ldflags "-s -w" $gocmd + +echo "" +echo "Linux amd64" +GOOS=linux GOARCH=amd64 go build -mod=vendor -o dist/linux/amd64/${exe} -ldflags "-s -w" $gocmd +echo "Linux 386" +GOOS=linux GOARCH=386 go build -mod=vendor -o dist/linux/386/${exe} -ldflags "-s -w" $gocmd + +echo "" +echo "RPi 4 (64-bit) ARMv8" +GOOS=linux GOARCH=arm64 go build -mod=vendor -o dist/linux/armv8/${exe} -ldflags "-s -w" $gocmd +echo "RPi 3 B+ ARMv7" +GOOS=linux GOARCH=arm GOARM=7 go build -mod=vendor -o dist/linux/armv7/${exe} -ldflags "-s -w" $gocmd +echo "ARMv6" +GOOS=linux GOARCH=arm GOARM=6 go build -mod=vendor -o dist/linux/armv6/${exe} -ldflags "-s -w" $gocmd +echo "RPi Zero ARMv5" +GOOS=linux GOARCH=arm GOARM=5 go build -mod=vendor -o dist/linux/armv5/${exe} -ldflags "-s -w" $gocmd + +echo "" +#rsync -av ./dist/ ubuntu@rootprojects.org:/srv/www/rootprojects.org/pathman/dist/ +# https://rootprojects.org/pathman/dist/windows/amd64/pathman.exe diff --git a/envpath/envpath.go b/envpath/envpath.go index b6c4688..e72a4ba 100644 --- a/envpath/envpath.go +++ b/envpath/envpath.go @@ -48,12 +48,12 @@ func Add(entry string) (bool, error) { return false, err } - _, ok := isInPath(home, paths, pathentry) - if ok { + index := IndexOf(paths, pathentry) + if index >= 0 { return false, nil } - paths = append([]string{pathentry}, paths...) + paths = append(paths, pathentry) err = writeEnv(fullpath, paths) if nil != err { return false, err @@ -86,8 +86,8 @@ func Remove(entry string) (bool, error) { return false, err } - index, exists := isInPath(home, oldpaths, pathentry) - if !exists { + index := IndexOf(oldpaths, pathentry) + if index < 0 { return false, nil } @@ -119,7 +119,8 @@ func getEnv(home string, env string) (string, []string, error) { return "", nil, err } - filename := fmt.Sprintf("00-%s.env", env) + //filename := fmt.Sprintf("00-%s.env", env) + filename := fmt.Sprintf("%s.env", env) for i := range nodes { name := nodes[i].Name() if fmt.Sprintf("%s.env", env) == name || strings.HasSuffix(name, fmt.Sprintf("-%s.env", env)) { @@ -188,19 +189,24 @@ func writeEnv(fullpath string, paths []string) error { return f.Close() } -func isInPath(home string, paths []string, pathentry string) (int, bool) { +// 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 { + home, err := os.UserHomeDir() + if nil != err { + panic(err) + } + + p, _ = normalizePathEntry(home, p) index := -1 for i := range paths { entry, _ := normalizePathEntry(home, paths[i]) - if pathentry == entry { + if p == entry { index = i break } } - if index >= 0 { - return index, true - } - return -1, false + return index } func normalizePathEntry(home, pathentry string) (string, error) { diff --git a/envpath/envpath_test.go b/envpath/envpath_test.go index fc2d11b..f9842a7 100644 --- a/envpath/envpath_test.go +++ b/envpath/envpath_test.go @@ -2,6 +2,8 @@ package envpath import ( "fmt" + "os" + "path/filepath" "testing" ) @@ -11,9 +13,6 @@ func TestAddRemove(t *testing.T) { t.Error(err) return } - for i := range paths { - fmt.Println(paths[i]) - } modified, err := Remove("/tmp/doesnt/exist") if nil != err { @@ -35,10 +34,16 @@ func TestAddRemove(t *testing.T) { return } + var exists bool paths, err = Paths() - if 1 != len(paths) || "/tmp/delete/me" != paths[0] { + for i := range paths { + if "/tmp/delete/me" == paths[i] { + exists = true + } + } + if !exists { fmt.Println("len(paths):", len(paths)) - t.Error(fmt.Errorf("Paths: should have had exactly one entry: /tmp/delete/me")) + t.Error(fmt.Errorf("Paths: should have had the entry: /tmp/delete/me")) return } @@ -52,9 +57,16 @@ func TestAddRemove(t *testing.T) { return } + exists = false paths, err = Paths() - if 1 != len(paths) || "/tmp/delete/me" != paths[0] { - t.Error(fmt.Errorf("Paths: should have had exactly one entry: /tmp/delete/me")) + for i := range paths { + if "/tmp/delete/me" == paths[i] { + exists = true + } + } + if !exists { + fmt.Println("len(paths):", len(paths)) + t.Error(fmt.Errorf("Paths: should have had the entry: /tmp/delete/me")) return } @@ -78,9 +90,16 @@ func TestAddRemove(t *testing.T) { return } + exists = false paths, err = Paths() - if 0 != len(paths) { - t.Error(fmt.Errorf("Paths: should have had no entries")) + for i := range paths { + if "/tmp/delete/me" == paths[i] { + exists = true + } + } + if exists { + fmt.Println("len(paths):", len(paths)) + t.Error(fmt.Errorf("Paths: should not have had the entry: /tmp/delete/me")) return } @@ -94,3 +113,73 @@ func TestAddRemove(t *testing.T) { return } } + +func TestHome(t *testing.T) { + home, _ := os.UserHomeDir() + + modified, err := Add(filepath.Join(home, "deleteme")) + if nil != err { + t.Error(err) + return + } + if !modified { + t.Error(fmt.Errorf("Add $HOME/deleteme: should have modified")) + return + } + + modified, err = Add(filepath.Join(home, "deleteme")) + if nil != err { + t.Error(err) + return + } + if modified { + t.Error(fmt.Errorf("Add $HOME/deleteme: should not have modified")) + return + } + + exists := false + paths, err := Paths() + for i := range paths { + if "$HOME/deleteme" == paths[i] { + exists = true + } + } + if !exists { + fmt.Println("len(paths):", len(paths)) + t.Error(fmt.Errorf("Paths: should have had the entry: $HOME/deleteme")) + return + } + + modified, err = Remove(filepath.Join(home, "deleteme")) + if nil != err { + t.Error(err) + return + } + if !modified { + t.Error(fmt.Errorf("Remove $HOME/deleteme: should have modified")) + return + } + + exists = false + paths, err = Paths() + for i := range paths { + if "$HOME/deleteme" == paths[i] { + exists = true + } + } + if exists { + fmt.Println("len(paths):", len(paths)) + t.Error(fmt.Errorf("Paths: should not have had the entry: $HOME/deleteme")) + return + } + + modified, err = Remove(filepath.Join(home, "deleteme")) + if nil != err { + t.Error(err) + return + } + if modified { + t.Error(fmt.Errorf("Remove $HOME/deleteme: should not have modified")) + return + } +} diff --git a/envpath/manager.go b/envpath/manager.go index e7c6741..1520c32 100644 --- a/envpath/manager.go +++ b/envpath/manager.go @@ -72,7 +72,7 @@ func initializeShells(home string) error { for i := range confs { c := confs[i] - if os.Getenv("SHELL") == c.shell { + if filepath.Base(os.Getenv("SHELL")) == c.shell { nativeMatch = c } @@ -181,7 +181,7 @@ func (c *envConfig) initializeShell() (bool, error) { } // Generate our script - script := fmt.Sprintf("# Generated for envpath. Do not edit.\n%s\n", c.rcScript) + script := fmt.Sprintf("# Generated for envman. Do not edit.\n%s\n", c.rcScript) // If there's not a newline before our template, // include it in the template. We want nice things. @@ -212,7 +212,7 @@ func (c *envConfig) ensurePathsLoader() error { // TODO maybe don't write every time if err := ioutil.WriteFile( loadFile, - []byte(fmt.Sprintf("# Generated for envpath. Do not edit.\n%s\n", c.loadScript)), + []byte(fmt.Sprintf("# Generated for envman. Do not edit.\n%s\n", c.loadScript)), os.FileMode(0755), ); nil != err { return err diff --git a/envpath/parse_test.go b/envpath/parse_test.go index a1d0726..2dcdf36 100644 --- a/envpath/parse_test.go +++ b/envpath/parse_test.go @@ -35,17 +35,16 @@ PATH="" ` -var paths = []string{ - `PATH="/foo"`, - `PATH="/foo:$PATH"`, - `PATH=""`, - `PATH="/boo:$PATH"`, -} - func TestParse(t *testing.T) { + exppaths := []string{ + `PATH="/foo"`, + `PATH="/foo:$PATH"`, + `PATH=""`, + `PATH="/boo:$PATH"`, + } newlines, warnings := Parse([]byte(file), "PATH") newfile := `PATH="` + strings.Join(newlines, "\"\n\tPATH=\"") + `"` - expfile := strings.Join(paths, "\n\t") + expfile := strings.Join(exppaths, "\n\t") if newfile != expfile { t.Errorf("\nExpected:\n\t%s\nGot:\n\t%s", expfile, newfile) } diff --git a/go.mod b/go.mod index 6f7798c..5451a68 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,7 @@ module git.rootprojects.org/root/pathman go 1.12 -require golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 +require ( + git.rootprojects.org/root/go-gitver v1.1.3 + golang.org/x/sys v0.0.0-20190712062909-fae7ac547cb7 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..1773d3b --- /dev/null +++ b/go.sum @@ -0,0 +1,4 @@ +git.rootprojects.org/root/go-gitver v1.1.3 h1:/qR9z53vY+IFhWRxLkF9cjaiWh8xRJIm6gyuW+MG81A= +git.rootprojects.org/root/go-gitver v1.1.3/go.mod h1:Rj1v3TBhvdaSphFEqMynUYwAz/4f+wY/+syBTvRrmlI= +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= diff --git a/pathman.go b/pathman.go index caba2ad..37458b1 100644 --- a/pathman.go +++ b/pathman.go @@ -5,13 +5,24 @@ import ( "os" "path/filepath" "strings" + "time" ) +// GitRev is the git commit hash of the build +var GitRev = "000000000" + +// GitVersion is the git description converted to semver +var GitVersion = "v0.5.2-pre+dirty" + +// GitTimestamp is the timestamp of the latest commit +var GitTimestamp = time.Now().Format(time.RFC3339) + func usage() { - fmt.Fprintf(os.Stdout, "Usage: envpath [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") + fmt.Fprintf(os.Stdout, "Usage: pathman [path]\n") + fmt.Fprintf(os.Stdout, "\tex: pathman list\n") + fmt.Fprintf(os.Stdout, "\tex: pathman add ~/.local/bin\n") + fmt.Fprintf(os.Stdout, "\tex: pathman remove ~/.local/bin\n") + fmt.Fprintf(os.Stdout, "\tex: pathman version\n") } func main() { @@ -29,7 +40,7 @@ func main() { } action = os.Args[1] - if 2 == len(os.Args) { + if 3 == len(os.Args) { entry = os.Args[2] } @@ -37,6 +48,7 @@ func main() { // https://github.com/rust-lang-nursery/rustup.rs/issues/686#issuecomment-253982841 // exec source $HOME/.profile shell := os.Getenv("SHELL") + shell = filepath.Base(shell) switch shell { case "": if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") { @@ -52,15 +64,27 @@ func main() { // 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", + "%q isn't a recognized shell. Please open an issue at https://git.rootprojects.org/root/pathman/issues?q=%s", shell, shell, ) } + home, _ := os.UserHomeDir() + if "" != entry && '~' == entry[0] { + // Let windows users not to have to type %USERPROFILE% or \Users\me every time + entry = strings.Replace(entry, "~", home, 0) + } switch action { + default: + usage() + os.Exit(1) + case "version": + fmt.Printf("pathman %s (%s) %s\n", GitVersion, GitRev, GitTimestamp) + os.Exit(0) + return case "list": - if 2 == len(os.Args) { + if 2 != len(os.Args) { usage() os.Exit(1) } @@ -91,18 +115,24 @@ func list() { fmt.Println("other PATH entries:\n") // All managed paths pathsmap := map[string]bool{} + home, _ := os.UserHomeDir() 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 { + paths := Paths() + for i := range paths { // TODO normalize - path := envpaths[i] - if !pathsmap[path] { + path := paths[i] + path1 := "" + path2 := "" + if strings.HasPrefix(path, home) { + path1 = "$HOME" + strings.TrimPrefix(path, home) + path2 = "%USERPROFILE%" + strings.TrimPrefix(path, home) + } + if !pathsmap[path] && !pathsmap[path1] && !pathsmap[path2] { hasExtras = true fmt.Println("\t" + path) } @@ -185,7 +215,7 @@ func remove(entry string) { fmt.Fprintf(os.Stderr, "%s", err) } - msg += " To set the PATH immediately, update the current session:\n\n\t" + Remove(entry) + "\n" + msg += " To set the PATH immediately, update the current session:\n\n\t" + Remove(newpaths) + "\n" } fmt.Println(msg + "\n") @@ -224,6 +254,6 @@ func Remove(entries []string) string { return fmt.Sprintf(`export PATH="%s"`, strings.Join(entries, ":")) } -func isCmdExe() { +func isCmdExe() bool { return "" == os.Getenv("SHELL") && strings.Contains(strings.ToLower(os.Getenv("COMSPEC")), "/cmd.exe") } diff --git a/pathman_unixes.go b/pathman_unixes.go index fdc79c0..455e70e 100644 --- a/pathman_unixes.go +++ b/pathman_unixes.go @@ -1,4 +1,4 @@ -// +build windows +// +build !windows package main @@ -15,7 +15,7 @@ func removePath(p string) (bool, error) { } func listPaths() ([]string, error) { - return envpath.List() + return envpath.Paths() } func indexOfPath(cur []string, p string) int { diff --git a/pathman_windows.go b/pathman_windows.go index 8cfcf67..4ae403f 100644 --- a/pathman_windows.go +++ b/pathman_windows.go @@ -15,7 +15,7 @@ func removePath(p string) (bool, error) { } func listPaths() ([]string, error) { - return winpath.List() + return winpath.Paths() } func indexOfPath(cur []string, p string) int { diff --git a/tools/tools.go b/tools/tools.go new file mode 100644 index 0000000..3160202 --- /dev/null +++ b/tools/tools.go @@ -0,0 +1,7 @@ +// +build tools + +package tools + +import ( + _ "git.rootprojects.org/root/go-gitver" +) diff --git a/winpath/winpath_windows.go b/winpath/winpath_windows.go index 25e81e5..3d0fbee 100644 --- a/winpath/winpath_windows.go +++ b/winpath/winpath_windows.go @@ -50,7 +50,7 @@ func remove(p string) (bool, error) { return false, err } - index := findMatch(cur, p) + index := IndexOf(cur, p) // skip silently, successfully if index < 0 { return false, nil