1package homedir
2
3import (
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.
17var DisableCache bool
18
19var homedirCache string
20var 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.
26func 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.
58func 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.
83func Reset() {
84	cacheLock.Lock()
85	defer cacheLock.Unlock()
86	homedirCache = ""
87}
88
89func 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
148func 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