v0.5.0: initial publishable version

This commit is contained in:
AJ ONeal 2019-07-28 04:04:53 -06:00
parent 9254e307e3
commit 8a699044be
14 changed files with 316 additions and 53 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
/pathman
dist
# ---> Go # ---> Go
# Binaries for programs and plugins # Binaries for programs and plugins
*.exe *.exe

View File

@ -1,3 +1,82 @@
# go-envpath # [pathman](https://git.rootprojects.org/root/pathman)
Manage PATH on Windows, Mac, and Linux with various Shells 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
```

43
build-all.sh Normal file
View File

@ -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

View File

@ -48,12 +48,12 @@ func Add(entry string) (bool, error) {
return false, err return false, err
} }
_, ok := isInPath(home, paths, pathentry) index := IndexOf(paths, pathentry)
if ok { if index >= 0 {
return false, nil return false, nil
} }
paths = append([]string{pathentry}, paths...) paths = append(paths, pathentry)
err = writeEnv(fullpath, paths) err = writeEnv(fullpath, paths)
if nil != err { if nil != err {
return false, err return false, err
@ -86,8 +86,8 @@ func Remove(entry string) (bool, error) {
return false, err return false, err
} }
index, exists := isInPath(home, oldpaths, pathentry) index := IndexOf(oldpaths, pathentry)
if !exists { if index < 0 {
return false, nil return false, nil
} }
@ -119,7 +119,8 @@ func getEnv(home string, env string) (string, []string, error) {
return "", nil, err 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 { for i := range nodes {
name := nodes[i].Name() name := nodes[i].Name()
if fmt.Sprintf("%s.env", env) == name || strings.HasSuffix(name, fmt.Sprintf("-%s.env", env)) { 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() 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 index := -1
for i := range paths { for i := range paths {
entry, _ := normalizePathEntry(home, paths[i]) entry, _ := normalizePathEntry(home, paths[i])
if pathentry == entry { if p == entry {
index = i index = i
break break
} }
} }
if index >= 0 { return index
return index, true
}
return -1, false
} }
func normalizePathEntry(home, pathentry string) (string, error) { func normalizePathEntry(home, pathentry string) (string, error) {

View File

@ -2,6 +2,8 @@ package envpath
import ( import (
"fmt" "fmt"
"os"
"path/filepath"
"testing" "testing"
) )
@ -11,9 +13,6 @@ func TestAddRemove(t *testing.T) {
t.Error(err) t.Error(err)
return return
} }
for i := range paths {
fmt.Println(paths[i])
}
modified, err := Remove("/tmp/doesnt/exist") modified, err := Remove("/tmp/doesnt/exist")
if nil != err { if nil != err {
@ -35,10 +34,16 @@ func TestAddRemove(t *testing.T) {
return return
} }
var exists bool
paths, err = Paths() 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)) 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 return
} }
@ -52,9 +57,16 @@ func TestAddRemove(t *testing.T) {
return return
} }
exists = false
paths, err = Paths() paths, err = Paths()
if 1 != len(paths) || "/tmp/delete/me" != paths[0] { for i := range paths {
t.Error(fmt.Errorf("Paths: should have had exactly one entry: /tmp/delete/me")) 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 return
} }
@ -78,9 +90,16 @@ func TestAddRemove(t *testing.T) {
return return
} }
exists = false
paths, err = Paths() paths, err = Paths()
if 0 != len(paths) { for i := range paths {
t.Error(fmt.Errorf("Paths: should have had no entries")) 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 return
} }
@ -94,3 +113,73 @@ func TestAddRemove(t *testing.T) {
return 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
}
}

View File

@ -72,7 +72,7 @@ func initializeShells(home string) error {
for i := range confs { for i := range confs {
c := confs[i] c := confs[i]
if os.Getenv("SHELL") == c.shell { if filepath.Base(os.Getenv("SHELL")) == c.shell {
nativeMatch = c nativeMatch = c
} }
@ -181,7 +181,7 @@ func (c *envConfig) initializeShell() (bool, error) {
} }
// Generate our script // 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, // If there's not a newline before our template,
// include it in the template. We want nice things. // 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 // TODO maybe don't write every time
if err := ioutil.WriteFile( if err := ioutil.WriteFile(
loadFile, 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), os.FileMode(0755),
); nil != err { ); nil != err {
return err return err

View File

@ -35,17 +35,16 @@ PATH=""
` `
var paths = []string{ func TestParse(t *testing.T) {
exppaths := []string{
`PATH="/foo"`, `PATH="/foo"`,
`PATH="/foo:$PATH"`, `PATH="/foo:$PATH"`,
`PATH=""`, `PATH=""`,
`PATH="/boo:$PATH"`, `PATH="/boo:$PATH"`,
} }
func TestParse(t *testing.T) {
newlines, warnings := Parse([]byte(file), "PATH") newlines, warnings := Parse([]byte(file), "PATH")
newfile := `PATH="` + strings.Join(newlines, "\"\n\tPATH=\"") + `"` newfile := `PATH="` + strings.Join(newlines, "\"\n\tPATH=\"") + `"`
expfile := strings.Join(paths, "\n\t") expfile := strings.Join(exppaths, "\n\t")
if newfile != expfile { if newfile != expfile {
t.Errorf("\nExpected:\n\t%s\nGot:\n\t%s", expfile, newfile) t.Errorf("\nExpected:\n\t%s\nGot:\n\t%s", expfile, newfile)
} }

5
go.mod
View File

@ -2,4 +2,7 @@ module git.rootprojects.org/root/pathman
go 1.12 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
)

4
go.sum Normal file
View File

@ -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=

View File

@ -5,13 +5,24 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"strings" "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() { func usage() {
fmt.Fprintf(os.Stdout, "Usage: envpath <action> [path]\n") fmt.Fprintf(os.Stdout, "Usage: pathman <action> [path]\n")
fmt.Fprintf(os.Stdout, "\tex: envpath list\n") fmt.Fprintf(os.Stdout, "\tex: pathman list\n")
fmt.Fprintf(os.Stdout, "\tex: envpath add ~/.local/bin\n") fmt.Fprintf(os.Stdout, "\tex: pathman add ~/.local/bin\n")
fmt.Fprintf(os.Stdout, "\tex: envpath remove ~/.local/bin\n") fmt.Fprintf(os.Stdout, "\tex: pathman remove ~/.local/bin\n")
fmt.Fprintf(os.Stdout, "\tex: pathman version\n")
} }
func main() { func main() {
@ -29,7 +40,7 @@ func main() {
} }
action = os.Args[1] action = os.Args[1]
if 2 == len(os.Args) { if 3 == len(os.Args) {
entry = os.Args[2] entry = os.Args[2]
} }
@ -37,6 +48,7 @@ func main() {
// https://github.com/rust-lang-nursery/rustup.rs/issues/686#issuecomment-253982841 // https://github.com/rust-lang-nursery/rustup.rs/issues/686#issuecomment-253982841
// exec source $HOME/.profile // exec source $HOME/.profile
shell := os.Getenv("SHELL") shell := os.Getenv("SHELL")
shell = filepath.Base(shell)
switch shell { switch shell {
case "": case "":
if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") { if strings.HasSuffix(os.Getenv("COMSPEC"), "/cmd.exe") {
@ -52,15 +64,27 @@ func main() {
// warn and try anyway // warn and try anyway
fmt.Fprintf( fmt.Fprintf(
os.Stderr, 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,
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 { 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": case "list":
if 2 == len(os.Args) { if 2 != len(os.Args) {
usage() usage()
os.Exit(1) os.Exit(1)
} }
@ -91,18 +115,24 @@ func list() {
fmt.Println("other PATH entries:\n") fmt.Println("other PATH entries:\n")
// All managed paths // All managed paths
pathsmap := map[string]bool{} pathsmap := map[string]bool{}
home, _ := os.UserHomeDir()
for i := range managedpaths { for i := range managedpaths {
// TODO normalize
pathsmap[managedpaths[i]] = true pathsmap[managedpaths[i]] = true
} }
// Paths in the environment which are not managed // Paths in the environment which are not managed
var hasExtras bool var hasExtras bool
envpaths := Paths() paths := Paths()
for i := range envpaths { for i := range paths {
// TODO normalize // TODO normalize
path := envpaths[i] path := paths[i]
if !pathsmap[path] { 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 hasExtras = true
fmt.Println("\t" + path) fmt.Println("\t" + path)
} }
@ -185,7 +215,7 @@ func remove(entry string) {
fmt.Fprintf(os.Stderr, "%s", err) 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") fmt.Println(msg + "\n")
@ -224,6 +254,6 @@ func Remove(entries []string) string {
return fmt.Sprintf(`export PATH="%s"`, strings.Join(entries, ":")) 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") return "" == os.Getenv("SHELL") && strings.Contains(strings.ToLower(os.Getenv("COMSPEC")), "/cmd.exe")
} }

View File

@ -1,4 +1,4 @@
// +build windows // +build !windows
package main package main
@ -15,7 +15,7 @@ func removePath(p string) (bool, error) {
} }
func listPaths() ([]string, error) { func listPaths() ([]string, error) {
return envpath.List() return envpath.Paths()
} }
func indexOfPath(cur []string, p string) int { func indexOfPath(cur []string, p string) int {

View File

@ -15,7 +15,7 @@ func removePath(p string) (bool, error) {
} }
func listPaths() ([]string, error) { func listPaths() ([]string, error) {
return winpath.List() return winpath.Paths()
} }
func indexOfPath(cur []string, p string) int { func indexOfPath(cur []string, p string) int {

7
tools/tools.go Normal file
View File

@ -0,0 +1,7 @@
// +build tools
package tools
import (
_ "git.rootprojects.org/root/go-gitver"
)

View File

@ -50,7 +50,7 @@ func remove(p string) (bool, error) {
return false, err return false, err
} }
index := findMatch(cur, p) index := IndexOf(cur, p)
// skip silently, successfully // skip silently, successfully
if index < 0 { if index < 0 {
return false, nil return false, nil