AJ ONeal
5 years ago
5 changed files with 582 additions and 2 deletions
@ -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 |
|||
} |
Loading…
Reference in new issue