Browse Source

v0.5.0: initial publishable version

wip v0.5.0
AJ ONeal 5 years ago
parent
commit
8a699044be
  1. 3
      .gitignore
  2. 83
      README.md
  3. 43
      build-all.sh
  4. 30
      envpath/envpath.go
  5. 107
      envpath/envpath_test.go
  6. 6
      envpath/manager.go
  7. 15
      envpath/parse_test.go
  8. 5
      go.mod
  9. 4
      go.sum
  10. 58
      pathman.go
  11. 4
      pathman_unixes.go
  12. 2
      pathman_windows.go
  13. 7
      tools/tools.go
  14. 2
      winpath/winpath_windows.go

3
.gitignore

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

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

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

30
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) {

107
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
}
}

6
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

15
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)
}

5
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
)

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

58
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 <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")
fmt.Fprintf(os.Stdout, "Usage: pathman <action> [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")
}

4
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 {

2
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 {

7
tools/tools.go

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

2
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

Loading…
Cancel
Save