add and source new PATHs
This commit is contained in:
		
							parent
							
								
									7ca8158a1c
								
							
						
					
					
						commit
						a17b60d46a
					
				
							
								
								
									
										378
									
								
								envpath/envpath.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										378
									
								
								envpath/envpath.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,378 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
import (
 | 
			
		||||
	"fmt"
 | 
			
		||||
	"io/ioutil"
 | 
			
		||||
	"math/rand"
 | 
			
		||||
	"os"
 | 
			
		||||
	"path/filepath"
 | 
			
		||||
	"strconv"
 | 
			
		||||
	"strings"
 | 
			
		||||
	"time"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
// ppsep is used as the replacement for slashes in path
 | 
			
		||||
// ex: ~/bin => ~bin
 | 
			
		||||
// ex: ~/bin => home~bin
 | 
			
		||||
// ex: ~/.local/opt/foo/bin => ~.local»opt»foo»bin
 | 
			
		||||
// other patterns considered:
 | 
			
		||||
//   ~/.local/opt/foo/bin => ~.local·opt·foo·bin
 | 
			
		||||
//   ~/.local/opt/foo/bin => home».local»opt»foo»bin
 | 
			
		||||
//   ~/.local/opt/foo/bin => HOME•.local•opt•foo•bin
 | 
			
		||||
//   ~/.local/opt/foo/bin => HOME_.local_opt_foo_bin
 | 
			
		||||
//   ~/.local/opt/foo/bin => HOME·.local·opt·foo·bin
 | 
			
		||||
const ppsep = "»"
 | 
			
		||||
 | 
			
		||||
func usage() {
 | 
			
		||||
	fmt.Fprintf(os.Stderr, "Usage: envpath show|add|append|remove <path>\n")
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func main() {
 | 
			
		||||
	// TODO --system to add to the system PATH rather than the user PATH
 | 
			
		||||
 | 
			
		||||
	// Usage:
 | 
			
		||||
	if len(os.Args) < 2 {
 | 
			
		||||
		usage()
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
	action := os.Args[1]
 | 
			
		||||
 | 
			
		||||
	paths, err := Paths()
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "%s\n", err)
 | 
			
		||||
		os.Exit(2)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if "show" == action {
 | 
			
		||||
		if len(os.Args) > 2 {
 | 
			
		||||
			usage()
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
		for i := range paths {
 | 
			
		||||
			fmt.Println("\t" + paths[i])
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println()
 | 
			
		||||
		return
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if len(os.Args) < 3 {
 | 
			
		||||
		usage()
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	pathentry := os.Args[2]
 | 
			
		||||
	if "remove" != action {
 | 
			
		||||
		stat, err := os.Stat(pathentry)
 | 
			
		||||
		if nil != err {
 | 
			
		||||
			// TODO --force
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%s\n", err)
 | 
			
		||||
			os.Exit(2)
 | 
			
		||||
		}
 | 
			
		||||
		if !stat.IsDir() {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%q is not a directory (folder)\n", pathentry)
 | 
			
		||||
			os.Exit(2)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	switch action {
 | 
			
		||||
	default:
 | 
			
		||||
		usage()
 | 
			
		||||
		os.Exit(1)
 | 
			
		||||
	case "append":
 | 
			
		||||
		newpath, _, err := addPath(pathentry, appendOrder)
 | 
			
		||||
		if nil != err {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%s\n", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println("New sessions will have " + pathentry + " in their PATH.")
 | 
			
		||||
		fmt.Println("To update this session run\n")
 | 
			
		||||
		//fmt.Println("\tsource", pathfile)
 | 
			
		||||
		fmt.Printf(`%sexport PATH="$PATH:%s"%s`, "\t", newpath, "\n")
 | 
			
		||||
	case "add":
 | 
			
		||||
		_, pathfile, err := addPath(pathentry, prependOrder)
 | 
			
		||||
		if nil != err {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%s\n", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
		fmt.Println("\nRun this command (or open a new shell) to finish:\n")
 | 
			
		||||
		fmt.Printf("\tsource %s\n\n", pathfile)
 | 
			
		||||
		//fmt.Printf(`%sexport PATH="%s:$PATH"%s`, "\t", newpath, "\n\n")
 | 
			
		||||
	case "remove":
 | 
			
		||||
		_, err := removePath(pathentry)
 | 
			
		||||
		if nil != err {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%s\n", err)
 | 
			
		||||
			return
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO support both of these usages:
 | 
			
		||||
	// 1. export PATH="$(envpath append /opt/whatever/bin)"
 | 
			
		||||
	// 2. envpath append /opt/whatever/bin
 | 
			
		||||
	//    export PATH="$PATH:/opt/whatever/bin"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Paths returns the slice of PATHs from the Environment
 | 
			
		||||
func Paths() ([]string, error) {
 | 
			
		||||
	// ":" on *nix
 | 
			
		||||
	return strings.Split(os.Getenv("PATH"), string(os.PathListSeparator)), nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	rand.Seed(time.Now().UnixNano())
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type setOrder bool
 | 
			
		||||
 | 
			
		||||
const prependOrder setOrder = true
 | 
			
		||||
const appendOrder setOrder = false
 | 
			
		||||
 | 
			
		||||
// returns newpath, error
 | 
			
		||||
func addPath(oldpathentry string, order setOrder) (string, string, error) {
 | 
			
		||||
	home, err := os.UserHomeDir()
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
	home = filepath.ToSlash(home)
 | 
			
		||||
 | 
			
		||||
	pathentry, fname, err := normalizeEntryAndFile(home, oldpathentry)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	envpathd := filepath.Join(home, ".config/envpath/path.d")
 | 
			
		||||
	err = os.MkdirAll(envpathd, 0755)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	err = initializeShells(home)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	nodes, err := ioutil.ReadDir(envpathd)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var priority int
 | 
			
		||||
	if prependOrder == order {
 | 
			
		||||
		// Counter-intuitively later PATHs, prepended are placed earlier
 | 
			
		||||
		priority, err = getOrder(nodes, pathentry, fname, envpathd, sortForward)
 | 
			
		||||
	} else {
 | 
			
		||||
		// Counter-intuitively earlier PATHs are placed later
 | 
			
		||||
		priority, err = getOrder(nodes, pathentry, fname, envpathd, sortBackward)
 | 
			
		||||
	}
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	if err := ensureNotInPath(home, pathentry); nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ex: 100-opt»foo»bin.sh
 | 
			
		||||
	// ex: 105-home»bar»bin.sh
 | 
			
		||||
	pathfile := fmt.Sprintf("%03d-%s", priority, fname)
 | 
			
		||||
 | 
			
		||||
	fullname := filepath.Join(envpathd, pathfile)
 | 
			
		||||
	export := []byte(fmt.Sprintf("# Generated for envpath. Do not edit.\nexport PATH=\"%s:$PATH\"\n", pathentry))
 | 
			
		||||
	err = ioutil.WriteFile(fullname, export, 0755)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
	// If we change from having the user source the path directory
 | 
			
		||||
	// then we should uncomment this so the user knows where the path files are
 | 
			
		||||
	//fmt.Printf("Wrote %s\n", fullname)
 | 
			
		||||
 | 
			
		||||
	return pathentry, fullname, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// TODO don't check in parser before add/append functions actually run
 | 
			
		||||
func ensureNotInPath(home, pathentry string) error {
 | 
			
		||||
	paths, err := Paths()
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	index := -1
 | 
			
		||||
	for i := range paths {
 | 
			
		||||
		entry, _ := normalizePathEntry(home, paths[i])
 | 
			
		||||
		if pathentry == entry {
 | 
			
		||||
			index = i
 | 
			
		||||
			break
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if index >= 0 {
 | 
			
		||||
		fmt.Fprintf(
 | 
			
		||||
			os.Stderr,
 | 
			
		||||
			"%q is in your PATH at position %d and must be removed manually to re-order\n",
 | 
			
		||||
			pathentry,
 | 
			
		||||
			index,
 | 
			
		||||
		)
 | 
			
		||||
		os.Exit(3)
 | 
			
		||||
	}
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func normalizeEntryAndFile(home, pathentry string) (string, string, error) {
 | 
			
		||||
	var err error
 | 
			
		||||
 | 
			
		||||
	pathentry, err = normalizePathEntry(home, pathentry)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// Now we split and rejoin the paths as a unique name
 | 
			
		||||
	// ex: /opt/foo/bin/ => opt/foo/bin => [opt foo bin]
 | 
			
		||||
	// ex: ~/bar/bin/ => bar/bin => [bar bin]
 | 
			
		||||
	names := strings.Split(strings.Trim(filepath.ToSlash(pathentry), "/"), "/")
 | 
			
		||||
	if strings.HasPrefix(pathentry, "$HOME/") {
 | 
			
		||||
		// ~/bar/bin/ => [home bar bin]
 | 
			
		||||
		names[0] = "home"
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// ex: /opt/foo/bin/ => opt»foo»bin.sh
 | 
			
		||||
	fname := strings.Join(names, ppsep) + ".sh"
 | 
			
		||||
 | 
			
		||||
	return pathentry, fname, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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 pathentry, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sortForward(priority, n int) int {
 | 
			
		||||
	// Pick a number such that 99 > newpriority > priority
 | 
			
		||||
	if n >= priority {
 | 
			
		||||
		m := n % 5
 | 
			
		||||
		if 0 == m {
 | 
			
		||||
			priority += 5
 | 
			
		||||
		} else {
 | 
			
		||||
			priority += (5 - m)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return priority
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func sortBackward(priority, n int) int {
 | 
			
		||||
	// Pick a number such that 0 < newpriority < priority
 | 
			
		||||
	if n <= priority {
 | 
			
		||||
		m := n % 5
 | 
			
		||||
		if 0 == m {
 | 
			
		||||
			m = 5
 | 
			
		||||
		}
 | 
			
		||||
		priority -= m
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return priority
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type sorter = func(int, int) int
 | 
			
		||||
 | 
			
		||||
func getOrder(nodes []os.FileInfo, pathentry, fname, envpathd string, fn sorter) (int, error) {
 | 
			
		||||
	// assuming people will append more often than prepend
 | 
			
		||||
	// default the priority to less than halfway
 | 
			
		||||
	priority := 100
 | 
			
		||||
	for i := range nodes {
 | 
			
		||||
		f := nodes[i]
 | 
			
		||||
		name := f.Name()
 | 
			
		||||
		if !strings.HasSuffix(name, ".sh") {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
		if strings.HasSuffix(name, "-"+fname) {
 | 
			
		||||
			return 0, fmt.Errorf(
 | 
			
		||||
				"Error: %s already exports %s",
 | 
			
		||||
				filepath.Join("~/.config/envpath/path.d", f.Name()),
 | 
			
		||||
				pathentry,
 | 
			
		||||
			)
 | 
			
		||||
		}
 | 
			
		||||
		n, err := strconv.Atoi(strings.Split(name, "-")[0])
 | 
			
		||||
		if nil != err {
 | 
			
		||||
			continue
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		priority = fn(priority, n)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return priority, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func removePath(oldpathentry string) (string, error) {
 | 
			
		||||
	// TODO Show current PATH sans this item
 | 
			
		||||
	/*
 | 
			
		||||
		oldpaths := paths
 | 
			
		||||
		paths = []string{}
 | 
			
		||||
		for i := range oldpaths {
 | 
			
		||||
			if i != index {
 | 
			
		||||
				paths = append(paths, oldpaths[i])
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	home, err := os.UserHomeDir()
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
	home = filepath.ToSlash(home)
 | 
			
		||||
 | 
			
		||||
	pathentry, fname, err := normalizeEntryAndFile(home, oldpathentry)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	envpathd := filepath.Join(home, ".config/envpath/path.d")
 | 
			
		||||
	err = os.MkdirAll(envpathd, 0755)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		err = ensureNotInPath(home, pathentry)
 | 
			
		||||
		if nil != err {
 | 
			
		||||
			return "", err
 | 
			
		||||
		}
 | 
			
		||||
	*/
 | 
			
		||||
 | 
			
		||||
	nodes, err := ioutil.ReadDir(envpathd)
 | 
			
		||||
	if nil != err {
 | 
			
		||||
		return "", err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// TODO rename getOrder to getNext
 | 
			
		||||
	_, err = getOrder(nodes, pathentry, fname, envpathd, sortForward)
 | 
			
		||||
	if nil == err {
 | 
			
		||||
		fmt.Fprintf(os.Stderr, "%q is not managed by envpath.\n", pathentry)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	/*
 | 
			
		||||
		if index < 0 {
 | 
			
		||||
			fmt.Fprintf(os.Stderr, "%q is NOT in PATH.\n", pathentry)
 | 
			
		||||
			os.Exit(3)
 | 
			
		||||
		}
 | 
			
		||||
	*/
 | 
			
		||||
	return "", nil
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										248
									
								
								envpath/manager.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										248
									
								
								envpath/manager.go
									
									
									
									
									
										Normal file
									
								
							@ -0,0 +1,248 @@
 | 
			
		||||
package main
 | 
			
		||||
 | 
			
		||||
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/envpath/load.sh\" ] && source \"$HOME/.config/envpath/load.sh\"\n",
 | 
			
		||||
			loadFile:   ".config/envpath/load.sh",
 | 
			
		||||
			loadScript: "for x in ~/.config/envpath/path.d/*.sh; do\n\tsource \"$x\"\ndone\n",
 | 
			
		||||
		},
 | 
			
		||||
		&envConfig{
 | 
			
		||||
			home:       home,
 | 
			
		||||
			shell:      "zsh",
 | 
			
		||||
			shellDesc:  "bourne-compatible shell (zsh)",
 | 
			
		||||
			rcFile:     ".zshrc",
 | 
			
		||||
			rcScript:   "[ -s \"$HOME/.config/envpath/load.sh\" ] && source \"$HOME/.config/envpath/load.sh\"\n",
 | 
			
		||||
			loadFile:   ".config/envpath/load.sh",
 | 
			
		||||
			loadScript: "for x in ~/.config/envpath/path.d/*.sh; do\n\tsource \"$x\"\ndone\n",
 | 
			
		||||
		},
 | 
			
		||||
		&envConfig{
 | 
			
		||||
			home:       home,
 | 
			
		||||
			shell:      "fish",
 | 
			
		||||
			shellDesc:  "fish shell",
 | 
			
		||||
			rcFile:     ".config/fish/config.fish",
 | 
			
		||||
			rcScript:   "test -s \"$HOME/.config/envpath/load.fish\"; and source \"$HOME/.config/envpath/load.fish\"\n",
 | 
			
		||||
			loadFile:   ".config/envpath/load.fish",
 | 
			
		||||
			loadScript: "for x in ~/.config/envpath/path.d/*.sh\n\tsource \"$x\"\nend\n",
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func initializeShells(home string) error {
 | 
			
		||||
	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/envpath/load.sh
 | 
			
		||||
		// $HOME/.config/envpath/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…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user