Browse Source

got some tests done

wip
AJ ONeal 5 years ago
parent
commit
9254e307e3
  1. 230
      envpath/envpath.go
  2. 96
      envpath/envpath_test.go
  3. 254
      envpath/manager.go
  4. 2
      envpath/parse.go
  5. 2
      envpath/parse_test.go

230
envpath/envpath.go

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

96
envpath/envpath_test.go

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

254
envpath/manager.go

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

2
envpath/parse.go

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

2
envpath/parse_test.go

@ -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…
Cancel
Save