Skip to content

Commit fe619a1

Browse files
authored
Merge pull request #87 from adrg/parse-user-dirs
Parse user directories config file
2 parents df5884b + ed625ee commit fe619a1

18 files changed

+540
-216
lines changed

README.md

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,17 @@ Sensible fallback locations are used for the folders which are not set.
100100

101101
### XDG user directories
102102

103+
XDG user directories environment variables are usually **not** set on most
104+
operating systems. However, if they are present in the environment, they take
105+
precedence. Appropriate fallback locations are used for the environment
106+
variables which are not set.
107+
108+
- On Unix-like operating systems (except macOS and Plan 9), the package reads the [user-dirs.dirs](https://man.archlinux.org/man/user-dirs.dirs.5.en) config file, if present.
109+
- On Windows, the package uses the appropriate [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid).
110+
111+
Lastly, default locations are used for any user directories which are not set,
112+
as shown in the following tables.
113+
103114
<details open>
104115
<summary><strong>Unix-like operating systems</strong></summary>
105116
<br/>
@@ -156,7 +167,7 @@ Sensible fallback locations are used for the folders which are not set.
156167
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: |
157168
| <kbd><b>Home</b></kbd> | <kbd>Profile</kbd> | <kbd>%USERPROFILE%</kbd> |
158169
| <kbd><b>Applications</b></kbd> | <kbd>Programs</kbd><br/><kbd>CommonPrograms</kbd> | <kbd>%APPDATA%\Microsoft\Windows\Start&nbsp;Menu\Programs</kbd><br/><kbd>%ProgramData%\Microsoft\Windows\Start&nbsp;Menu\Programs</kbd> |
159-
| <kbd><b>Fonts</b></kbd> | <kbd>Fonts</kbd><br/><kbd>-</kbd> | <kbd>%SystemRoot%\Fonts</kbd><br/><kbd>%LOCALAPPDATA%\Microsoft\Windows\Fonts</kbd> |
170+
| <kbd><b>Fonts</b></kbd> | <kbd>Fonts</kbd> | <kbd>%SystemRoot%\Fonts</kbd><br/><kbd>%LOCALAPPDATA%\Microsoft\Windows\Fonts</kbd> |
160171

161172
</details>
162173

internal/pathutil/pathutil.go

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,17 @@ import (
88
)
99

1010
// Unique eliminates the duplicate paths from the provided slice and returns
11-
// the result. The items in the output slice are in the order in which they
12-
// occur in the input slice. If a `home` location is provided, the paths are
13-
// expanded using the `ExpandHome` function.
14-
func Unique(paths []string, home string) []string {
11+
// the result. The paths are expanded using the `ExpandHome` function and only
12+
// absolute paths are kept. The items in the output slice are in the order in
13+
// which they occur in the input slice.
14+
func Unique(paths []string) []string {
1515
var (
1616
uniq []string
1717
registry = map[string]struct{}{}
1818
)
1919

2020
for _, p := range paths {
21-
p = ExpandHome(p, home)
22-
if p != "" && filepath.IsAbs(p) {
21+
if p = ExpandHome(p); p != "" && filepath.IsAbs(p) {
2322
if _, ok := registry[p]; ok {
2423
continue
2524
}
@@ -32,6 +31,18 @@ func Unique(paths []string, home string) []string {
3231
return uniq
3332
}
3433

34+
// First returns the first absolute path from the provided slice.
35+
// The paths in the input slice are expanded using the `ExpandHome` function.
36+
func First(paths []string) string {
37+
for _, p := range paths {
38+
if p = ExpandHome(p); p != "" && filepath.IsAbs(p) {
39+
return p
40+
}
41+
}
42+
43+
return ""
44+
}
45+
3546
// Create returns a suitable location relative to which the file with the
3647
// specified `name` can be written. The first path from the provided `paths`
3748
// slice which is successfully created (or already exists) is used as a base
@@ -78,3 +89,29 @@ func Search(name string, paths []string) (string, error) {
7889
return "", fmt.Errorf("could not locate `%s` in any of the following paths: %s",
7990
filepath.Base(name), strings.Join(searchedPaths, ", "))
8091
}
92+
93+
// EnvPath returns the value of the environment variable with the specified
94+
// `name` if it is an absolute path, or the first absolute fallback path.
95+
// All paths are expanded using the `ExpandHome` function.
96+
func EnvPath(name string, fallbackPaths ...string) string {
97+
dir := ExpandHome(os.Getenv(name))
98+
if dir != "" && filepath.IsAbs(dir) {
99+
return dir
100+
}
101+
102+
return First(fallbackPaths)
103+
}
104+
105+
// EnvPathList reads the value of the environment variable with the specified
106+
// `name` and attempts to extract a list of absolute paths from it. If there
107+
// are none, a list of absolute fallback paths is returned instead. Duplicate
108+
// paths are removed from the returned slice. All paths are expanded using the
109+
// `ExpandHome` function.
110+
func EnvPathList(name string, fallbackPaths ...string) []string {
111+
dirs := Unique(filepath.SplitList(os.Getenv(name)))
112+
if len(dirs) != 0 {
113+
return dirs
114+
}
115+
116+
return Unique(fallbackPaths)
117+
}

internal/pathutil/pathutil_plan9.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ import (
88
"strings"
99
)
1010

11+
// UserHomeDir returns the home directory of the current user.
12+
func UserHomeDir() string {
13+
if home := os.Getenv("home"); home != "" {
14+
return home
15+
}
16+
17+
return "/"
18+
}
19+
1120
// Exists returns true if the specified path exists.
1221
func Exists(path string) bool {
1322
_, err := os.Stat(path)
1423
return err == nil || errors.Is(err, fs.ErrExist)
1524
}
1625

17-
// ExpandHome substitutes `~` and `$home` at the start of the specified
18-
// `path` using the provided `home` location.
19-
func ExpandHome(path, home string) string {
26+
// ExpandHome substitutes `~` and `$home` at the start of the specified `path`.
27+
func ExpandHome(path string) string {
28+
home := UserHomeDir()
2029
if path == "" || home == "" {
2130
return path
2231
}

internal/pathutil/pathutil_plan9_test.go

Lines changed: 50 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -3,49 +3,73 @@
33
package pathutil_test
44

55
import (
6+
"os"
67
"path/filepath"
78
"testing"
89

9-
"github.com/adrg/xdg/internal/pathutil"
1010
"github.com/stretchr/testify/require"
11+
12+
"github.com/adrg/xdg/internal/pathutil"
1113
)
1214

15+
func TestUserHomeDir(t *testing.T) {
16+
home := os.Getenv("home")
17+
defer os.Setenv("home", home)
18+
19+
require.Equal(t, home, pathutil.UserHomeDir())
20+
21+
os.Unsetenv("home")
22+
require.Equal(t, "/", pathutil.UserHomeDir())
23+
}
24+
1325
func TestExpandHome(t *testing.T) {
14-
home := "/home/test"
15-
16-
require.Equal(t, home, pathutil.ExpandHome("~", home))
17-
require.Equal(t, home, pathutil.ExpandHome("$home", home))
18-
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname", home))
19-
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$home/appname", home))
20-
21-
require.Equal(t, "", pathutil.ExpandHome("", home))
22-
require.Equal(t, home, pathutil.ExpandHome(home, ""))
23-
require.Equal(t, "", pathutil.ExpandHome("", ""))
24-
25-
require.Equal(t, home, pathutil.ExpandHome(home, home))
26-
require.Equal(t, "/", pathutil.ExpandHome("~", "/"))
27-
require.Equal(t, "/", pathutil.ExpandHome("$home", "/"))
28-
require.Equal(t, "/usr/bin", pathutil.ExpandHome("~/bin", "/usr"))
29-
require.Equal(t, "/usr/bin", pathutil.ExpandHome("$home/bin", "/usr"))
26+
home := pathutil.UserHomeDir()
27+
28+
require.Equal(t, "", pathutil.ExpandHome(""))
29+
require.Equal(t, home, pathutil.ExpandHome(home))
30+
require.Equal(t, home, pathutil.ExpandHome("~"))
31+
require.Equal(t, home, pathutil.ExpandHome("$home"))
32+
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname"))
33+
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$home/appname"))
3034
}
3135

3236
func TestUnique(t *testing.T) {
37+
home := pathutil.UserHomeDir()
38+
3339
input := []string{
3440
"",
35-
"/home",
36-
"/home/test",
41+
home,
42+
filepath.Join(home, "foo"),
3743
"a",
38-
"~/appname",
39-
"$home/appname",
44+
"~/foo",
45+
"$home/foo",
4046
"a",
41-
"/home",
47+
"~",
48+
"$home",
4249
}
4350

4451
expected := []string{
45-
"/home",
46-
"/home/test",
47-
"/home/test/appname",
52+
home,
53+
filepath.Join(home, "foo"),
4854
}
4955

50-
require.EqualValues(t, expected, pathutil.Unique(input, "/home/test"))
56+
require.EqualValues(t, expected, pathutil.Unique(input))
57+
}
58+
59+
func TestFirst(t *testing.T) {
60+
home := pathutil.UserHomeDir()
61+
62+
require.Equal(t, "", pathutil.First([]string{}))
63+
require.Equal(t, home, pathutil.First([]string{home}))
64+
require.Equal(t, home, pathutil.First([]string{"$home"}))
65+
require.Equal(t, home, pathutil.First([]string{"~"}))
66+
require.Equal(t, home, pathutil.First([]string{home, ""}))
67+
require.Equal(t, home, pathutil.First([]string{"", home}))
68+
require.Equal(t, home, pathutil.First([]string{"$home", ""}))
69+
require.Equal(t, home, pathutil.First([]string{"", "$home"}))
70+
require.Equal(t, home, pathutil.First([]string{"~", ""}))
71+
require.Equal(t, home, pathutil.First([]string{"", "~"}))
72+
require.Equal(t, "/home/test/foo", pathutil.First([]string{"/home/test/foo", "/home/test/bar"}))
73+
require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"$home/foo", "$home/bar"}))
74+
require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"~/foo", "~/bar"}))
5175
}

internal/pathutil/pathutil_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package pathutil_test
33
import (
44
"os"
55
"path/filepath"
6+
"strings"
67
"testing"
78

89
"github.com/stretchr/testify/require"
@@ -107,3 +108,36 @@ func TestSearch(t *testing.T) {
107108

108109
require.NoError(t, os.RemoveAll(filepath.Dir(expected)))
109110
}
111+
112+
func TestEnvPath(t *testing.T) {
113+
home := pathutil.UserHomeDir()
114+
val := filepath.Join(home, "test")
115+
116+
os.Setenv("PATHUTIL_TEST_VAR", val)
117+
defer os.Unsetenv("PATHUTIL_TEST_VAR")
118+
119+
require.Equal(t, val, pathutil.EnvPath("PATHUTIL_TEST_VAR"))
120+
121+
os.Setenv("PATHUTIL_TEST_VAR", "")
122+
require.Equal(t, val, pathutil.EnvPath("PATHUTIL_TEST_VAR", val))
123+
require.Equal(t, val, pathutil.EnvPath("", val))
124+
}
125+
126+
func TestEnvPathList(t *testing.T) {
127+
home := pathutil.UserHomeDir()
128+
pathList := []string{
129+
filepath.Join(home, "test1"),
130+
filepath.Join(home, "test2"),
131+
filepath.Join(home, "test3"),
132+
}
133+
val := strings.Join(pathList, string(os.PathListSeparator))
134+
135+
os.Setenv("PATHUTIL_TEST_VAR", val)
136+
defer os.Unsetenv("PATHUTIL_TEST_VAR")
137+
138+
require.Equal(t, val, strings.Join(pathutil.EnvPathList("PATHUTIL_TEST_VAR"), string(os.PathListSeparator)))
139+
140+
os.Setenv("PATHUTIL_TEST_VAR", "")
141+
require.Equal(t, val, strings.Join(pathutil.EnvPathList("PATHUTIL_TEST_VAR", pathList...), string(os.PathListSeparator)))
142+
require.Equal(t, val, strings.Join(pathutil.EnvPathList("", pathList...), string(os.PathListSeparator)))
143+
}

internal/pathutil/pathutil_unix.go

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,24 @@ import (
1010
"strings"
1111
)
1212

13+
// UserHomeDir returns the home directory of the current user.
14+
func UserHomeDir() string {
15+
if home := os.Getenv("HOME"); home != "" {
16+
return home
17+
}
18+
19+
return "/"
20+
}
21+
1322
// Exists returns true if the specified path exists.
1423
func Exists(path string) bool {
1524
_, err := os.Stat(path)
1625
return err == nil || errors.Is(err, fs.ErrExist)
1726
}
1827

19-
// ExpandHome substitutes `~` and `$HOME` at the start of the specified
20-
// `path` using the provided `home` location.
21-
func ExpandHome(path, home string) string {
28+
// ExpandHome substitutes `~` and `$HOME` at the start of the specified `path`.
29+
func ExpandHome(path string) string {
30+
home := UserHomeDir()
2231
if path == "" || home == "" {
2332
return path
2433
}

internal/pathutil/pathutil_unix_test.go

Lines changed: 48 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
package pathutil_test
44

55
import (
6+
"os"
67
"path/filepath"
78
"testing"
89

@@ -11,42 +12,64 @@ import (
1112
"github.com/adrg/xdg/internal/pathutil"
1213
)
1314

15+
func TestUserHomeDir(t *testing.T) {
16+
home := os.Getenv("HOME")
17+
defer os.Setenv("HOME", home)
18+
19+
require.Equal(t, home, pathutil.UserHomeDir())
20+
21+
os.Unsetenv("HOME")
22+
require.Equal(t, "/", pathutil.UserHomeDir())
23+
}
24+
1425
func TestExpandHome(t *testing.T) {
15-
home := "/home/test"
16-
17-
require.Equal(t, home, pathutil.ExpandHome("~", home))
18-
require.Equal(t, home, pathutil.ExpandHome("$HOME", home))
19-
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname", home))
20-
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$HOME/appname", home))
21-
22-
require.Equal(t, "", pathutil.ExpandHome("", home))
23-
require.Equal(t, home, pathutil.ExpandHome(home, ""))
24-
require.Equal(t, "", pathutil.ExpandHome("", ""))
25-
26-
require.Equal(t, home, pathutil.ExpandHome(home, home))
27-
require.Equal(t, "/", pathutil.ExpandHome("~", "/"))
28-
require.Equal(t, "/", pathutil.ExpandHome("$HOME", "/"))
29-
require.Equal(t, "/usr/bin", pathutil.ExpandHome("~/bin", "/usr"))
30-
require.Equal(t, "/usr/bin", pathutil.ExpandHome("$HOME/bin", "/usr"))
26+
home := pathutil.UserHomeDir()
27+
28+
require.Equal(t, "", pathutil.ExpandHome(""))
29+
require.Equal(t, home, pathutil.ExpandHome(home))
30+
require.Equal(t, home, pathutil.ExpandHome("~"))
31+
require.Equal(t, home, pathutil.ExpandHome("$HOME"))
32+
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname"))
33+
require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$HOME/appname"))
3134
}
3235

3336
func TestUnique(t *testing.T) {
37+
home := pathutil.UserHomeDir()
38+
3439
input := []string{
3540
"",
36-
"/home",
37-
"/home/test",
41+
home,
42+
filepath.Join(home, "foo"),
3843
"a",
39-
"~/appname",
40-
"$HOME/appname",
44+
"~/foo",
45+
"$HOME/foo",
4146
"a",
42-
"/home",
47+
"~",
48+
"$HOME",
4349
}
4450

4551
expected := []string{
46-
"/home",
47-
"/home/test",
48-
"/home/test/appname",
52+
home,
53+
filepath.Join(home, "foo"),
4954
}
5055

51-
require.EqualValues(t, expected, pathutil.Unique(input, "/home/test"))
56+
require.EqualValues(t, expected, pathutil.Unique(input))
57+
}
58+
59+
func TestFirst(t *testing.T) {
60+
home := pathutil.UserHomeDir()
61+
62+
require.Equal(t, "", pathutil.First([]string{}))
63+
require.Equal(t, home, pathutil.First([]string{home}))
64+
require.Equal(t, home, pathutil.First([]string{"$HOME"}))
65+
require.Equal(t, home, pathutil.First([]string{"~"}))
66+
require.Equal(t, home, pathutil.First([]string{home, ""}))
67+
require.Equal(t, home, pathutil.First([]string{"", home}))
68+
require.Equal(t, home, pathutil.First([]string{"$HOME", ""}))
69+
require.Equal(t, home, pathutil.First([]string{"", "$HOME"}))
70+
require.Equal(t, home, pathutil.First([]string{"~", ""}))
71+
require.Equal(t, home, pathutil.First([]string{"", "~"}))
72+
require.Equal(t, "/home/test/foo", pathutil.First([]string{"/home/test/foo", "/home/test/bar"}))
73+
require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"$HOME/foo", "$HOME/bar"}))
74+
require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"~/foo", "~/bar"}))
5275
}

0 commit comments

Comments
 (0)