270 lines
6.3 KiB
Go
270 lines
6.3 KiB
Go
//go:generate go run -mod=vendor git.rootprojects.org/root/go-gitver
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"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: 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() {
|
|
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 3 == len(os.Args) {
|
|
entry = os.Args[2]
|
|
}
|
|
|
|
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, 1)
|
|
}
|
|
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) {
|
|
usage()
|
|
os.Exit(1)
|
|
}
|
|
list()
|
|
case "add":
|
|
checkShell()
|
|
add(entry)
|
|
case "remove":
|
|
checkShell()
|
|
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{}
|
|
home, _ := os.UserHomeDir()
|
|
for i := range managedpaths {
|
|
pathsmap[managedpaths[i]] = true
|
|
}
|
|
|
|
// Paths in the environment which are not managed
|
|
var hasExtras bool
|
|
paths := Paths()
|
|
for i := range paths {
|
|
// TODO normalize
|
|
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)
|
|
}
|
|
}
|
|
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, "%sfailed to add %q to PATH: %s", pathstore, 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(newpaths) + "\n"
|
|
}
|
|
|
|
fmt.Println(msg + "\n")
|
|
}
|
|
|
|
// warns if this is an unknown / untested shell
|
|
func checkShell() {
|
|
// https://superuser.com/a/69190/73857
|
|
// https://github.com/rust-lang-nursery/rustup.rs/issues/686#issuecomment-253982841
|
|
// exec source $HOME/.profile
|
|
shellexe := filepath.Base(os.Getenv("SHELL"))
|
|
shell := strings.TrimSuffix(shellexe, ".exe")
|
|
switch shell {
|
|
case ".":
|
|
shell = ""
|
|
fallthrough
|
|
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/root/pathman/issues?q=%s\n",
|
|
shellexe,
|
|
shellexe,
|
|
)
|
|
}
|
|
}
|
|
|
|
// 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%%`, strings.Replace(p, "%", "%%", -1))
|
|
}
|
|
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() bool {
|
|
return "" == os.Getenv("SHELL") && strings.HasSuffix(strings.ToLower(os.Getenv("COMSPEC")), "\\cmd.exe")
|
|
}
|