got some tests done
This commit is contained in:
parent
d850aff673
commit
9254e307e3
|
@ -0,0 +1,230 @@
|
|||
package envpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Paths parses the PATH.env file and returns a slice of valid paths
|
||||
func Paths() ([]string, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
home = filepath.ToSlash(home)
|
||||
|
||||
_, paths, err := getEnv(home, "PATH")
|
||||
if nil != err {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// ":" on *nix
|
||||
return paths, nil
|
||||
}
|
||||
|
||||
// Add adds a path entry to the PATH env file
|
||||
func Add(entry string) (bool, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
home = filepath.ToSlash(home)
|
||||
|
||||
pathentry, err := normalizePathEntry(home, entry)
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = initializeShells(home)
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fullpath, paths, err := getEnv(home, "PATH")
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
_, ok := isInPath(home, paths, pathentry)
|
||||
if ok {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
paths = append([]string{pathentry}, paths...)
|
||||
err = writeEnv(fullpath, paths)
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fmt.Println("Wrote " + fullpath)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// Remove adds a path entry to the PATH env file
|
||||
func Remove(entry string) (bool, error) {
|
||||
home, err := os.UserHomeDir()
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
home = filepath.ToSlash(home)
|
||||
|
||||
pathentry, err := normalizePathEntry(home, entry)
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
err = initializeShells(home)
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fullpath, oldpaths, err := getEnv(home, "PATH")
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
index, exists := isInPath(home, oldpaths, pathentry)
|
||||
if !exists {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
paths := []string{}
|
||||
for i := range oldpaths {
|
||||
if index != i {
|
||||
paths = append(paths, oldpaths[i])
|
||||
}
|
||||
}
|
||||
|
||||
err = writeEnv(fullpath, paths)
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
fmt.Println("Wrote " + fullpath)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func getEnv(home string, env string) (string, []string, error) {
|
||||
envmand := filepath.Join(home, ".config/envman")
|
||||
err := os.MkdirAll(envmand, 0755)
|
||||
if nil != err {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
nodes, err := ioutil.ReadDir(envmand)
|
||||
if nil != err {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
filename := fmt.Sprintf("00-%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)) {
|
||||
filename = name
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
fullpath := filepath.Join(envmand, filename)
|
||||
f, err := os.OpenFile(fullpath, os.O_CREATE|os.O_RDONLY, 0644)
|
||||
if nil != err {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadAll(f)
|
||||
f.Close()
|
||||
if nil != err {
|
||||
return "", nil, err
|
||||
}
|
||||
|
||||
paths, warnings := Parse(b, env)
|
||||
for i := range warnings {
|
||||
w := warnings[i]
|
||||
fmt.Printf("warning: dropped %q from %s:%d: %s\n", w.Line, filename, w.LineNumber, w.Message)
|
||||
}
|
||||
|
||||
pathlines := []string{}
|
||||
for i := range paths {
|
||||
pathname := strings.TrimSuffix(paths[i], ":$PATH")
|
||||
if strings.HasPrefix(pathname, "$PATH:") {
|
||||
fixed := strings.TrimPrefix(pathname, "$PATH:")
|
||||
fmt.Fprintf(os.Stderr, "warning: re-arranging $PATH:%s to %s:$PATH\n", fixed, fixed)
|
||||
pathname = fixed
|
||||
}
|
||||
pathlines = append(pathlines, pathname)
|
||||
}
|
||||
|
||||
if len(warnings) > 0 {
|
||||
err := writeEnv(fullpath, pathlines)
|
||||
if nil != err {
|
||||
return "", nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return fullpath, pathlines, nil
|
||||
}
|
||||
|
||||
func writeEnv(fullpath string, paths []string) error {
|
||||
f, err := os.OpenFile(fullpath, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, 0644)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = f.Write([]byte("# Generated for envman. Do not edit.\n"))
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := range paths {
|
||||
_, err := f.Write([]byte(fmt.Sprintf("export PATH=\"%s:$PATH\"\n", paths[i])))
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func isInPath(home string, paths []string, pathentry string) (int, bool) {
|
||||
index := -1
|
||||
for i := range paths {
|
||||
entry, _ := normalizePathEntry(home, paths[i])
|
||||
if pathentry == entry {
|
||||
index = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if index >= 0 {
|
||||
return index, true
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
|
||||
func normalizePathEntry(home, pathentry string) (string, error) {
|
||||
var err error
|
||||
|
||||
// We add the slashes so that we don't get false matches
|
||||
// ex: foo should match foo/bar, but should NOT match foobar
|
||||
home, err = filepath.Abs(home)
|
||||
if nil != err {
|
||||
// I'm not sure how it's possible to get an error with Abs...
|
||||
return "", err
|
||||
}
|
||||
home += "/"
|
||||
pathentry, err = filepath.Abs(pathentry)
|
||||
if nil != err {
|
||||
return "", err
|
||||
}
|
||||
pathentry += "/"
|
||||
|
||||
// Next we make the path relative to / or ~/
|
||||
// ex: /Users/me/.local/bin/ => .local/bin/
|
||||
if strings.HasPrefix(pathentry, home) {
|
||||
pathentry = "$HOME/" + strings.TrimPrefix(pathentry, home)
|
||||
}
|
||||
|
||||
return strings.TrimSuffix(pathentry, "/"), nil
|
||||
}
|
|
@ -0,0 +1,96 @@
|
|||
package envpath
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAddRemove(t *testing.T) {
|
||||
paths, err := Paths()
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
for i := range paths {
|
||||
fmt.Println(paths[i])
|
||||
}
|
||||
|
||||
modified, err := Remove("/tmp/doesnt/exist")
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if modified {
|
||||
t.Error(fmt.Errorf("Remove /tmp/doesnt/exist: should not have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Add("/tmp/delete/me")
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if !modified {
|
||||
t.Error(fmt.Errorf("Add /tmp/delete/me: should have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
paths, err = Paths()
|
||||
if 1 != len(paths) || "/tmp/delete/me" != paths[0] {
|
||||
fmt.Println("len(paths):", len(paths))
|
||||
t.Error(fmt.Errorf("Paths: should have had exactly one entry: /tmp/delete/me"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Add("/tmp/delete/me")
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if modified {
|
||||
t.Error(fmt.Errorf("Add /tmp/delete/me: should not have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
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"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Remove("/tmp/doesnt/exist")
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if modified {
|
||||
t.Error(fmt.Errorf("Remove /tmp/doesnt/exist: should not have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Remove("/tmp/delete/me")
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if !modified {
|
||||
t.Error(fmt.Errorf("Remove /tmp/delete/me: should have modified"))
|
||||
return
|
||||
}
|
||||
|
||||
paths, err = Paths()
|
||||
if 0 != len(paths) {
|
||||
t.Error(fmt.Errorf("Paths: should have had no entries"))
|
||||
return
|
||||
}
|
||||
|
||||
modified, err = Remove("/tmp/delete/me")
|
||||
if nil != err {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
if modified {
|
||||
t.Error(fmt.Errorf("Remove /tmp/delete/me: should not have modified"))
|
||||
return
|
||||
}
|
||||
}
|
|
@ -0,0 +1,254 @@
|
|||
package envpath
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type envConfig struct {
|
||||
shell string
|
||||
shellDesc string
|
||||
home string
|
||||
rcFile string
|
||||
rcScript string
|
||||
loadFile string
|
||||
loadScript string
|
||||
}
|
||||
|
||||
var confs []*envConfig
|
||||
|
||||
func init() {
|
||||
home, err := os.UserHomeDir()
|
||||
if nil != err {
|
||||
panic(err) // Must get home directory
|
||||
}
|
||||
home = filepath.ToSlash(home)
|
||||
|
||||
confs = []*envConfig{
|
||||
&envConfig{
|
||||
home: home,
|
||||
shell: "bash",
|
||||
shellDesc: "bourne-compatible shell (bash)",
|
||||
rcFile: ".bashrc",
|
||||
rcScript: "[ -s \"$HOME/.config/envman/load.sh\" ] && source \"$HOME/.config/envman/load.sh\"\n",
|
||||
loadFile: ".config/envman/load.sh",
|
||||
loadScript: "for x in ~/.config/envman/*.env; do\n\tsource \"$x\"\ndone\n",
|
||||
},
|
||||
&envConfig{
|
||||
home: home,
|
||||
shell: "zsh",
|
||||
shellDesc: "bourne-compatible shell (zsh)",
|
||||
rcFile: ".zshrc",
|
||||
rcScript: "[ -s \"$HOME/.config/envman/load.sh\" ] && source \"$HOME/.config/envman/load.sh\"\n",
|
||||
loadFile: ".config/envman/load.sh",
|
||||
loadScript: "for x in ~/.config/envman/*.env; do\n\tsource \"$x\"\ndone\n",
|
||||
},
|
||||
&envConfig{
|
||||
home: home,
|
||||
shell: "fish",
|
||||
shellDesc: "fish shell",
|
||||
rcFile: ".config/fish/config.fish",
|
||||
rcScript: "test -s \"$HOME/.config/envman/load.fish\"; and source \"$HOME/.config/envman/load.fish\"\n",
|
||||
loadFile: ".config/envman/load.fish",
|
||||
loadScript: "for x in ~/.config/envman/*.env\n\tsource \"$x\"\nend\n",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func initializeShells(home string) error {
|
||||
envmand := filepath.Join(home, ".config/envman")
|
||||
err := os.MkdirAll(envmand, 0755)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
var hasRC bool
|
||||
var nativeMatch *envConfig
|
||||
for i := range confs {
|
||||
c := confs[i]
|
||||
|
||||
if os.Getenv("SHELL") == c.shell {
|
||||
nativeMatch = c
|
||||
}
|
||||
|
||||
_, err := os.Stat(filepath.Join(home, c.rcFile))
|
||||
if nil != err {
|
||||
continue
|
||||
}
|
||||
hasRC = true
|
||||
}
|
||||
|
||||
// ensure rc
|
||||
if !hasRC {
|
||||
if nil == nativeMatch {
|
||||
return fmt.Errorf(
|
||||
"%q is not a recognized shell and found none of .bashrc, .zshrc, .config/fish/config.fish",
|
||||
os.Getenv("SHELL"),
|
||||
)
|
||||
}
|
||||
|
||||
// touch the rc file
|
||||
f, err := os.OpenFile(filepath.Join(home, nativeMatch.rcFile), os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); nil != err {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// MacOS is special. It *requires* .bash_profile in order to read .bashrc
|
||||
if "darwin" == runtime.GOOS && "bash" == os.Getenv("SHELL") {
|
||||
if err := ensureBashProfile(home); nil != err {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
//
|
||||
// Bash (sh, dash, zsh, ksh)
|
||||
//
|
||||
// http://www.joshstaiger.org/archives/2005/07/bash_profile_vs.html
|
||||
for i := range confs {
|
||||
c := confs[i]
|
||||
err := c.maybeInitializeShell()
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *envConfig) maybeInitializeShell() error {
|
||||
if _, err := os.Stat(filepath.Join(c.home, c.rcFile)); nil != err {
|
||||
if !os.IsNotExist(err) {
|
||||
fmt.Fprintf(os.Stderr, "%s\n", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
changed, err := c.initializeShell()
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
if changed {
|
||||
fmt.Printf(
|
||||
"Detected %s shell and updated ~/%s\n",
|
||||
c.shellDesc,
|
||||
strings.TrimPrefix(c.rcFile, c.home),
|
||||
)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *envConfig) initializeShell() (bool, error) {
|
||||
if err := c.ensurePathsLoader(); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Get current config
|
||||
// ex: ~/.bashrc
|
||||
// ex: ~/.config/fish/config.fish
|
||||
b, err := ioutil.ReadFile(filepath.Join(c.home, c.rcFile))
|
||||
if nil != err {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// For Windows, just in case
|
||||
s := strings.Replace(string(b), "\r\n", "\n", -1)
|
||||
|
||||
// Looking to see if loader script has been added to rc file
|
||||
lines := strings.Split(strings.TrimSpace(s), "\n")
|
||||
for i := range lines {
|
||||
line := lines[i]
|
||||
if line == strings.TrimSpace(c.rcScript) {
|
||||
// indicate that it was not neccesary to change the rc file
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
// Open rc file to append and write
|
||||
f, err := os.OpenFile(filepath.Join(c.home, c.rcFile), os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Generate our script
|
||||
script := fmt.Sprintf("# Generated for envpath. 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.
|
||||
n := len(lines)
|
||||
if "" != strings.TrimSpace(lines[n-1]) {
|
||||
script = "\n" + script
|
||||
}
|
||||
|
||||
// Write and close the rc file
|
||||
if _, err := f.Write([]byte(script)); err != nil {
|
||||
return false, err
|
||||
}
|
||||
if err := f.Close(); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
// indicate that we have changed the rc file
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (c *envConfig) ensurePathsLoader() error {
|
||||
loadFile := filepath.Join(c.home, c.loadFile)
|
||||
|
||||
if _, err := os.Stat(loadFile); nil != err {
|
||||
// Write the loop file. For example:
|
||||
// $HOME/.config/envman/load.sh
|
||||
// $HOME/.config/envman/load.fish
|
||||
// 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)),
|
||||
os.FileMode(0755),
|
||||
); nil != err {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Created %s\n", "~/"+c.loadFile)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// I think this issue only affects darwin users with bash as the default shell
|
||||
func ensureBashProfile(home string) error {
|
||||
profileFile := filepath.Join(home, ".bash_profile")
|
||||
|
||||
// touch the profile file
|
||||
f, err := os.OpenFile(profileFile, os.O_CREATE|os.O_WRONLY, 0644)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
if err := f.Close(); nil != err {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(profileFile)
|
||||
if !bytes.Contains(b, []byte(".bashrc")) {
|
||||
f, err := os.OpenFile(profileFile, os.O_APPEND|os.O_WRONLY, 0644)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
sourceBashRC := "[ -s \"$HOME/.bashrc\" ] && source \"$HOME/.bashrc\"\n"
|
||||
b := []byte(fmt.Sprintf("# Generated for MacOS bash. Do not edit.\n%s\n", sourceBashRC))
|
||||
_, err = f.Write(b)
|
||||
if nil != err {
|
||||
return err
|
||||
}
|
||||
fmt.Printf("Updated ~/.bash_profile to source ~/.bashrc\n")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
|
@ -12,7 +12,7 @@ type Warning struct {
|
|||
}
|
||||
|
||||
// Parse will return a list of paths from an export file
|
||||
func Parse(envname string, b []byte) ([]string, []Warning) {
|
||||
func Parse(b []byte, envname string) ([]string, []Warning) {
|
||||
s := string(b)
|
||||
s = strings.Replace(s, "\r\n", "\n", -1)
|
||||
|
||||
|
|
|
@ -43,7 +43,7 @@ var paths = []string{
|
|||
}
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
newlines, warnings := Parse("PATH", []byte(file))
|
||||
newlines, warnings := Parse([]byte(file), "PATH")
|
||||
newfile := `PATH="` + strings.Join(newlines, "\"\n\tPATH=\"") + `"`
|
||||
expfile := strings.Join(paths, "\n\t")
|
||||
if newfile != expfile {
|
||||
|
|
Loading…
Reference in New Issue