homedir.go raw

   1  package homedir
   2  
   3  import (
   4  	"bytes"
   5  	"errors"
   6  	"os"
   7  	"os/exec"
   8  	"path/filepath"
   9  	"runtime"
  10  	"strconv"
  11  	"strings"
  12  	"sync"
  13  )
  14  
  15  // DisableCache will disable caching of the home directory. Caching is enabled
  16  // by default.
  17  var DisableCache bool
  18  
  19  var homedirCache string
  20  var cacheLock sync.RWMutex
  21  
  22  // Dir returns the home directory for the executing user.
  23  //
  24  // This uses an OS-specific method for discovering the home directory.
  25  // An error is returned if a home directory cannot be detected.
  26  func Dir() (string, error) {
  27  	if !DisableCache {
  28  		cacheLock.RLock()
  29  		cached := homedirCache
  30  		cacheLock.RUnlock()
  31  		if cached != "" {
  32  			return cached, nil
  33  		}
  34  	}
  35  
  36  	cacheLock.Lock()
  37  	defer cacheLock.Unlock()
  38  
  39  	var result string
  40  	var err error
  41  	if runtime.GOOS == "windows" {
  42  		result, err = dirWindows()
  43  	} else {
  44  		// Unix-like system, so just assume Unix
  45  		result, err = dirUnix()
  46  	}
  47  
  48  	if err != nil {
  49  		return "", err
  50  	}
  51  	homedirCache = result
  52  	return result, nil
  53  }
  54  
  55  // Expand expands the path to include the home directory if the path
  56  // is prefixed with `~`. If it isn't prefixed with `~`, the path is
  57  // returned as-is.
  58  func Expand(path string) (string, error) {
  59  	if len(path) == 0 {
  60  		return path, nil
  61  	}
  62  
  63  	if path[0] != '~' {
  64  		return path, nil
  65  	}
  66  
  67  	if len(path) > 1 && path[1] != '/' && path[1] != '\\' {
  68  		return "", errors.New("cannot expand user-specific home dir")
  69  	}
  70  
  71  	dir, err := Dir()
  72  	if err != nil {
  73  		return "", err
  74  	}
  75  
  76  	return filepath.Join(dir, path[1:]), nil
  77  }
  78  
  79  // Reset clears the cache, forcing the next call to Dir to re-detect
  80  // the home directory. This generally never has to be called, but can be
  81  // useful in tests if you're modifying the home directory via the HOME
  82  // env var or something.
  83  func Reset() {
  84  	cacheLock.Lock()
  85  	defer cacheLock.Unlock()
  86  	homedirCache = ""
  87  }
  88  
  89  func dirUnix() (string, error) {
  90  	homeEnv := "HOME"
  91  	if runtime.GOOS == "plan9" {
  92  		// On plan9, env vars are lowercase.
  93  		homeEnv = "home"
  94  	}
  95  
  96  	// First prefer the HOME environmental variable
  97  	if home := os.Getenv(homeEnv); home != "" {
  98  		return home, nil
  99  	}
 100  
 101  	var stdout bytes.Buffer
 102  
 103  	// If that fails, try OS specific commands
 104  	if runtime.GOOS == "darwin" {
 105  		cmd := exec.Command("sh", "-c", `dscl -q . -read /Users/"$(whoami)" NFSHomeDirectory | sed 's/^[^ ]*: //'`)
 106  		cmd.Stdout = &stdout
 107  		if err := cmd.Run(); err == nil {
 108  			result := strings.TrimSpace(stdout.String())
 109  			if result != "" {
 110  				return result, nil
 111  			}
 112  		}
 113  	} else {
 114  		cmd := exec.Command("getent", "passwd", strconv.Itoa(os.Getuid()))
 115  		cmd.Stdout = &stdout
 116  		if err := cmd.Run(); err != nil {
 117  			// If the error is ErrNotFound, we ignore it. Otherwise, return it.
 118  			if err != exec.ErrNotFound {
 119  				return "", err
 120  			}
 121  		} else {
 122  			if passwd := strings.TrimSpace(stdout.String()); passwd != "" {
 123  				// username:password:uid:gid:gecos:home:shell
 124  				passwdParts := strings.SplitN(passwd, ":", 7)
 125  				if len(passwdParts) > 5 {
 126  					return passwdParts[5], nil
 127  				}
 128  			}
 129  		}
 130  	}
 131  
 132  	// If all else fails, try the shell
 133  	stdout.Reset()
 134  	cmd := exec.Command("sh", "-c", "cd && pwd")
 135  	cmd.Stdout = &stdout
 136  	if err := cmd.Run(); err != nil {
 137  		return "", err
 138  	}
 139  
 140  	result := strings.TrimSpace(stdout.String())
 141  	if result == "" {
 142  		return "", errors.New("blank output when reading home directory")
 143  	}
 144  
 145  	return result, nil
 146  }
 147  
 148  func dirWindows() (string, error) {
 149  	// First prefer the HOME environmental variable
 150  	if home := os.Getenv("HOME"); home != "" {
 151  		return home, nil
 152  	}
 153  
 154  	// Prefer standard environment variable USERPROFILE
 155  	if home := os.Getenv("USERPROFILE"); home != "" {
 156  		return home, nil
 157  	}
 158  
 159  	drive := os.Getenv("HOMEDRIVE")
 160  	path := os.Getenv("HOMEPATH")
 161  	home := drive + path
 162  	if drive == "" || path == "" {
 163  		return "", errors.New("HOMEDRIVE, HOMEPATH, or USERPROFILE are blank")
 164  	}
 165  
 166  	return home, nil
 167  }
 168