diff --git a/README.md b/README.md index c87af05..73642a3 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,17 @@ Sensible fallback locations are used for the folders which are not set. ### XDG user directories +XDG user directories environment variables are usually **not** set on most +operating systems. However, if they are present in the environment, they take +precedence. Appropriate fallback locations are used for the environment +variables which are not set. + +- 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. +- On Windows, the package uses the appropriate [Known Folders](https://docs.microsoft.com/en-us/windows/win32/shell/knownfolderid). + +Lastly, default locations are used for any user directories which are not set, +as shown in the following tables. +
Unix-like operating systems
@@ -156,7 +167,7 @@ Sensible fallback locations are used for the folders which are not set. | :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: | | Home | Profile | %USERPROFILE% | | Applications | Programs
CommonPrograms | %APPDATA%\Microsoft\Windows\Start Menu\Programs
%ProgramData%\Microsoft\Windows\Start Menu\Programs | -| Fonts | Fonts
- | %SystemRoot%\Fonts
%LOCALAPPDATA%\Microsoft\Windows\Fonts | +| Fonts | Fonts | %SystemRoot%\Fonts
%LOCALAPPDATA%\Microsoft\Windows\Fonts |
diff --git a/internal/pathutil/pathutil.go b/internal/pathutil/pathutil.go index d3bc124..554eda6 100644 --- a/internal/pathutil/pathutil.go +++ b/internal/pathutil/pathutil.go @@ -8,18 +8,17 @@ import ( ) // Unique eliminates the duplicate paths from the provided slice and returns -// the result. The items in the output slice are in the order in which they -// occur in the input slice. If a `home` location is provided, the paths are -// expanded using the `ExpandHome` function. -func Unique(paths []string, home string) []string { +// the result. The paths are expanded using the `ExpandHome` function and only +// absolute paths are kept. The items in the output slice are in the order in +// which they occur in the input slice. +func Unique(paths []string) []string { var ( uniq []string registry = map[string]struct{}{} ) for _, p := range paths { - p = ExpandHome(p, home) - if p != "" && filepath.IsAbs(p) { + if p = ExpandHome(p); p != "" && filepath.IsAbs(p) { if _, ok := registry[p]; ok { continue } @@ -32,6 +31,18 @@ func Unique(paths []string, home string) []string { return uniq } +// First returns the first absolute path from the provided slice. +// The paths in the input slice are expanded using the `ExpandHome` function. +func First(paths []string) string { + for _, p := range paths { + if p = ExpandHome(p); p != "" && filepath.IsAbs(p) { + return p + } + } + + return "" +} + // Create returns a suitable location relative to which the file with the // specified `name` can be written. The first path from the provided `paths` // 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) { return "", fmt.Errorf("could not locate `%s` in any of the following paths: %s", filepath.Base(name), strings.Join(searchedPaths, ", ")) } + +// EnvPath returns the value of the environment variable with the specified +// `name` if it is an absolute path, or the first absolute fallback path. +// All paths are expanded using the `ExpandHome` function. +func EnvPath(name string, fallbackPaths ...string) string { + dir := ExpandHome(os.Getenv(name)) + if dir != "" && filepath.IsAbs(dir) { + return dir + } + + return First(fallbackPaths) +} + +// EnvPathList reads the value of the environment variable with the specified +// `name` and attempts to extract a list of absolute paths from it. If there +// are none, a list of absolute fallback paths is returned instead. Duplicate +// paths are removed from the returned slice. All paths are expanded using the +// `ExpandHome` function. +func EnvPathList(name string, fallbackPaths ...string) []string { + dirs := Unique(filepath.SplitList(os.Getenv(name))) + if len(dirs) != 0 { + return dirs + } + + return Unique(fallbackPaths) +} diff --git a/internal/pathutil/pathutil_plan9.go b/internal/pathutil/pathutil_plan9.go index 389a0b6..a6e378a 100644 --- a/internal/pathutil/pathutil_plan9.go +++ b/internal/pathutil/pathutil_plan9.go @@ -8,15 +8,24 @@ import ( "strings" ) +// UserHomeDir returns the home directory of the current user. +func UserHomeDir() string { + if home := os.Getenv("home"); home != "" { + return home + } + + return "/" +} + // Exists returns true if the specified path exists. func Exists(path string) bool { _, err := os.Stat(path) return err == nil || errors.Is(err, fs.ErrExist) } -// ExpandHome substitutes `~` and `$home` at the start of the specified -// `path` using the provided `home` location. -func ExpandHome(path, home string) string { +// ExpandHome substitutes `~` and `$home` at the start of the specified `path`. +func ExpandHome(path string) string { + home := UserHomeDir() if path == "" || home == "" { return path } diff --git a/internal/pathutil/pathutil_plan9_test.go b/internal/pathutil/pathutil_plan9_test.go index 8c09794..67e48c3 100644 --- a/internal/pathutil/pathutil_plan9_test.go +++ b/internal/pathutil/pathutil_plan9_test.go @@ -3,49 +3,73 @@ package pathutil_test import ( + "os" "path/filepath" "testing" - "github.com/adrg/xdg/internal/pathutil" "github.com/stretchr/testify/require" + + "github.com/adrg/xdg/internal/pathutil" ) +func TestUserHomeDir(t *testing.T) { + home := os.Getenv("home") + defer os.Setenv("home", home) + + require.Equal(t, home, pathutil.UserHomeDir()) + + os.Unsetenv("home") + require.Equal(t, "/", pathutil.UserHomeDir()) +} + func TestExpandHome(t *testing.T) { - home := "/home/test" - - require.Equal(t, home, pathutil.ExpandHome("~", home)) - require.Equal(t, home, pathutil.ExpandHome("$home", home)) - require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname", home)) - require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$home/appname", home)) - - require.Equal(t, "", pathutil.ExpandHome("", home)) - require.Equal(t, home, pathutil.ExpandHome(home, "")) - require.Equal(t, "", pathutil.ExpandHome("", "")) - - require.Equal(t, home, pathutil.ExpandHome(home, home)) - require.Equal(t, "/", pathutil.ExpandHome("~", "/")) - require.Equal(t, "/", pathutil.ExpandHome("$home", "/")) - require.Equal(t, "/usr/bin", pathutil.ExpandHome("~/bin", "/usr")) - require.Equal(t, "/usr/bin", pathutil.ExpandHome("$home/bin", "/usr")) + home := pathutil.UserHomeDir() + + require.Equal(t, "", pathutil.ExpandHome("")) + require.Equal(t, home, pathutil.ExpandHome(home)) + require.Equal(t, home, pathutil.ExpandHome("~")) + require.Equal(t, home, pathutil.ExpandHome("$home")) + require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname")) + require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$home/appname")) } func TestUnique(t *testing.T) { + home := pathutil.UserHomeDir() + input := []string{ "", - "/home", - "/home/test", + home, + filepath.Join(home, "foo"), "a", - "~/appname", - "$home/appname", + "~/foo", + "$home/foo", "a", - "/home", + "~", + "$home", } expected := []string{ - "/home", - "/home/test", - "/home/test/appname", + home, + filepath.Join(home, "foo"), } - require.EqualValues(t, expected, pathutil.Unique(input, "/home/test")) + require.EqualValues(t, expected, pathutil.Unique(input)) +} + +func TestFirst(t *testing.T) { + home := pathutil.UserHomeDir() + + require.Equal(t, "", pathutil.First([]string{})) + require.Equal(t, home, pathutil.First([]string{home})) + require.Equal(t, home, pathutil.First([]string{"$home"})) + require.Equal(t, home, pathutil.First([]string{"~"})) + require.Equal(t, home, pathutil.First([]string{home, ""})) + require.Equal(t, home, pathutil.First([]string{"", home})) + require.Equal(t, home, pathutil.First([]string{"$home", ""})) + require.Equal(t, home, pathutil.First([]string{"", "$home"})) + require.Equal(t, home, pathutil.First([]string{"~", ""})) + require.Equal(t, home, pathutil.First([]string{"", "~"})) + require.Equal(t, "/home/test/foo", pathutil.First([]string{"/home/test/foo", "/home/test/bar"})) + require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"$home/foo", "$home/bar"})) + require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"~/foo", "~/bar"})) } diff --git a/internal/pathutil/pathutil_test.go b/internal/pathutil/pathutil_test.go index af03625..9c2ab21 100644 --- a/internal/pathutil/pathutil_test.go +++ b/internal/pathutil/pathutil_test.go @@ -3,6 +3,7 @@ package pathutil_test import ( "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -107,3 +108,36 @@ func TestSearch(t *testing.T) { require.NoError(t, os.RemoveAll(filepath.Dir(expected))) } + +func TestEnvPath(t *testing.T) { + home := pathutil.UserHomeDir() + val := filepath.Join(home, "test") + + os.Setenv("PATHUTIL_TEST_VAR", val) + defer os.Unsetenv("PATHUTIL_TEST_VAR") + + require.Equal(t, val, pathutil.EnvPath("PATHUTIL_TEST_VAR")) + + os.Setenv("PATHUTIL_TEST_VAR", "") + require.Equal(t, val, pathutil.EnvPath("PATHUTIL_TEST_VAR", val)) + require.Equal(t, val, pathutil.EnvPath("", val)) +} + +func TestEnvPathList(t *testing.T) { + home := pathutil.UserHomeDir() + pathList := []string{ + filepath.Join(home, "test1"), + filepath.Join(home, "test2"), + filepath.Join(home, "test3"), + } + val := strings.Join(pathList, string(os.PathListSeparator)) + + os.Setenv("PATHUTIL_TEST_VAR", val) + defer os.Unsetenv("PATHUTIL_TEST_VAR") + + require.Equal(t, val, strings.Join(pathutil.EnvPathList("PATHUTIL_TEST_VAR"), string(os.PathListSeparator))) + + os.Setenv("PATHUTIL_TEST_VAR", "") + require.Equal(t, val, strings.Join(pathutil.EnvPathList("PATHUTIL_TEST_VAR", pathList...), string(os.PathListSeparator))) + require.Equal(t, val, strings.Join(pathutil.EnvPathList("", pathList...), string(os.PathListSeparator))) +} diff --git a/internal/pathutil/pathutil_unix.go b/internal/pathutil/pathutil_unix.go index be5b802..3114a8c 100644 --- a/internal/pathutil/pathutil_unix.go +++ b/internal/pathutil/pathutil_unix.go @@ -10,15 +10,24 @@ import ( "strings" ) +// UserHomeDir returns the home directory of the current user. +func UserHomeDir() string { + if home := os.Getenv("HOME"); home != "" { + return home + } + + return "/" +} + // Exists returns true if the specified path exists. func Exists(path string) bool { _, err := os.Stat(path) return err == nil || errors.Is(err, fs.ErrExist) } -// ExpandHome substitutes `~` and `$HOME` at the start of the specified -// `path` using the provided `home` location. -func ExpandHome(path, home string) string { +// ExpandHome substitutes `~` and `$HOME` at the start of the specified `path`. +func ExpandHome(path string) string { + home := UserHomeDir() if path == "" || home == "" { return path } diff --git a/internal/pathutil/pathutil_unix_test.go b/internal/pathutil/pathutil_unix_test.go index fd1d0c2..49a323f 100644 --- a/internal/pathutil/pathutil_unix_test.go +++ b/internal/pathutil/pathutil_unix_test.go @@ -3,6 +3,7 @@ package pathutil_test import ( + "os" "path/filepath" "testing" @@ -11,42 +12,64 @@ import ( "github.com/adrg/xdg/internal/pathutil" ) +func TestUserHomeDir(t *testing.T) { + home := os.Getenv("HOME") + defer os.Setenv("HOME", home) + + require.Equal(t, home, pathutil.UserHomeDir()) + + os.Unsetenv("HOME") + require.Equal(t, "/", pathutil.UserHomeDir()) +} + func TestExpandHome(t *testing.T) { - home := "/home/test" - - require.Equal(t, home, pathutil.ExpandHome("~", home)) - require.Equal(t, home, pathutil.ExpandHome("$HOME", home)) - require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname", home)) - require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$HOME/appname", home)) - - require.Equal(t, "", pathutil.ExpandHome("", home)) - require.Equal(t, home, pathutil.ExpandHome(home, "")) - require.Equal(t, "", pathutil.ExpandHome("", "")) - - require.Equal(t, home, pathutil.ExpandHome(home, home)) - require.Equal(t, "/", pathutil.ExpandHome("~", "/")) - require.Equal(t, "/", pathutil.ExpandHome("$HOME", "/")) - require.Equal(t, "/usr/bin", pathutil.ExpandHome("~/bin", "/usr")) - require.Equal(t, "/usr/bin", pathutil.ExpandHome("$HOME/bin", "/usr")) + home := pathutil.UserHomeDir() + + require.Equal(t, "", pathutil.ExpandHome("")) + require.Equal(t, home, pathutil.ExpandHome(home)) + require.Equal(t, home, pathutil.ExpandHome("~")) + require.Equal(t, home, pathutil.ExpandHome("$HOME")) + require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("~/appname")) + require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome("$HOME/appname")) } func TestUnique(t *testing.T) { + home := pathutil.UserHomeDir() + input := []string{ "", - "/home", - "/home/test", + home, + filepath.Join(home, "foo"), "a", - "~/appname", - "$HOME/appname", + "~/foo", + "$HOME/foo", "a", - "/home", + "~", + "$HOME", } expected := []string{ - "/home", - "/home/test", - "/home/test/appname", + home, + filepath.Join(home, "foo"), } - require.EqualValues(t, expected, pathutil.Unique(input, "/home/test")) + require.EqualValues(t, expected, pathutil.Unique(input)) +} + +func TestFirst(t *testing.T) { + home := pathutil.UserHomeDir() + + require.Equal(t, "", pathutil.First([]string{})) + require.Equal(t, home, pathutil.First([]string{home})) + require.Equal(t, home, pathutil.First([]string{"$HOME"})) + require.Equal(t, home, pathutil.First([]string{"~"})) + require.Equal(t, home, pathutil.First([]string{home, ""})) + require.Equal(t, home, pathutil.First([]string{"", home})) + require.Equal(t, home, pathutil.First([]string{"$HOME", ""})) + require.Equal(t, home, pathutil.First([]string{"", "$HOME"})) + require.Equal(t, home, pathutil.First([]string{"~", ""})) + require.Equal(t, home, pathutil.First([]string{"", "~"})) + require.Equal(t, "/home/test/foo", pathutil.First([]string{"/home/test/foo", "/home/test/bar"})) + require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"$HOME/foo", "$HOME/bar"})) + require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{"~/foo", "~/bar"})) } diff --git a/internal/pathutil/pathutil_windows.go b/internal/pathutil/pathutil_windows.go index c29a223..5b18155 100644 --- a/internal/pathutil/pathutil_windows.go +++ b/internal/pathutil/pathutil_windows.go @@ -10,6 +10,11 @@ import ( "golang.org/x/sys/windows" ) +// UserHomeDir returns the home directory of the current user. +func UserHomeDir() string { + return KnownFolder(windows.FOLDERID_Profile, []string{"USERPROFILE"}, nil) +} + // Exists returns true if the specified path exists. func Exists(path string) bool { fi, err := os.Lstat(path) @@ -20,9 +25,9 @@ func Exists(path string) bool { return err == nil || errors.Is(err, fs.ErrExist) } -// ExpandHome substitutes `%USERPROFILE%` at the start of the specified -// `path` using the provided `home` location. -func ExpandHome(path, home string) string { +// ExpandHome substitutes `%USERPROFILE%` at the start of the specified `path`. +func ExpandHome(path string) string { + home := UserHomeDir() if path == "" || home == "" { return path } diff --git a/internal/pathutil/pathutil_windows_test.go b/internal/pathutil/pathutil_windows_test.go index 08a8f65..aa628ea 100644 --- a/internal/pathutil/pathutil_windows_test.go +++ b/internal/pathutil/pathutil_windows_test.go @@ -3,6 +3,7 @@ package pathutil_test import ( + "os" "path/filepath" "testing" @@ -11,6 +12,15 @@ import ( "golang.org/x/sys/windows" ) +func TestUserHomeDir(t *testing.T) { + home := pathutil.KnownFolder(windows.FOLDERID_Profile, nil, nil) + if home == "" { + home = os.Getenv("USERPROFILE") + } + + require.Equal(t, home, pathutil.UserHomeDir()) +} + func TestKnownFolder(t *testing.T) { expected := `C:\ProgramData` require.Equal(t, expected, pathutil.KnownFolder(windows.FOLDERID_ProgramData, nil, nil)) @@ -20,35 +30,46 @@ func TestKnownFolder(t *testing.T) { } func TestExpandHome(t *testing.T) { - home := `C:\Users\test` - - require.Equal(t, home, pathutil.ExpandHome(`%USERPROFILE%`, home)) - require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome(`%USERPROFILE%\appname`, home)) + home := pathutil.UserHomeDir() - require.Equal(t, "", pathutil.ExpandHome("", home)) - require.Equal(t, home, pathutil.ExpandHome(home, "")) - require.Equal(t, "", pathutil.ExpandHome("", "")) - - require.Equal(t, home, pathutil.ExpandHome(home, home)) + require.Equal(t, "", pathutil.ExpandHome("")) + require.Equal(t, home, pathutil.ExpandHome(home)) + require.Equal(t, home, pathutil.ExpandHome(`%USERPROFILE%`)) + require.Equal(t, filepath.Join(home, "appname"), pathutil.ExpandHome(`%USERPROFILE%\appname`)) } func TestUnique(t *testing.T) { + home := pathutil.UserHomeDir() + input := []string{ "", - `C:\Users`, - `C:\Users\test`, + home, + filepath.Join(home, "foo"), "a", - `C:\Users\test\appname`, - `%USERPROFILE%/appname`, + `%USERPROFILE%/foo`, + `%USERPROFILE%\foo`, "a", - `C:\Users`, } expected := []string{ - `C:\Users`, - `C:\Users\test`, - `C:\Users\test\appname`, + home, + filepath.Join(home, "foo"), } - require.EqualValues(t, expected, pathutil.Unique(input, `C:\Users\test`)) + require.EqualValues(t, expected, pathutil.Unique(input)) +} + +func TestFirst(t *testing.T) { + home := pathutil.UserHomeDir() + + require.Equal(t, "", pathutil.First([]string{})) + require.Equal(t, home, pathutil.First([]string{home})) + require.Equal(t, home, pathutil.First([]string{"%USERPROFILE%"})) + require.Equal(t, home, pathutil.First([]string{home, ""})) + require.Equal(t, home, pathutil.First([]string{"", home})) + require.Equal(t, home, pathutil.First([]string{"%USERPROFILE%", ""})) + require.Equal(t, home, pathutil.First([]string{"", "%USERPROFILE%"})) + require.Equal(t, `C:\Users\foo`, pathutil.First([]string{`C:\Users\foo`, `C:\Users\bar`})) + require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{`%USERPROFILE%/foo`, `%USERPROFILE%/bar`})) + require.Equal(t, filepath.Join(home, "foo"), pathutil.First([]string{`%USERPROFILE%/foo`, `%USERPROFILE%/bar`})) } diff --git a/internal/userdirs/config_unix.go b/internal/userdirs/config_unix.go new file mode 100644 index 0000000..f6b03b1 --- /dev/null +++ b/internal/userdirs/config_unix.go @@ -0,0 +1,83 @@ +//go:build aix || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris + +package userdirs + +import ( + "bufio" + "io" + "os" + "strings" + + "github.com/adrg/xdg/internal/pathutil" +) + +// ParseConfigFile parses the user directories config file at the specified +// location. The returned map contains pairs consisting of the user directory +// names and their paths. An empty map is returned if an error is encountered. +func ParseConfigFile(name string) map[string]string { + f, err := os.Open(name) + if err != nil { + return map[string]string{} + } + defer f.Close() + + return ParseConfig(f) +} + +// ParseConfig parses the user directories config file contained in the provided +// reader. The returned map contains pairs consisting of the user directory +// names and their paths. An empty map is returned if an error is encountered. +func ParseConfig(r io.Reader) map[string]string { + dirs := map[string]string{} + + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := strings.TrimSpace(scanner.Text()) + if len(line) == 0 || line[0] == '#' { + continue + } + if !strings.HasPrefix(line, "XDG_") { + continue + } + + parts := strings.Split(line, "=") + if len(parts) < 2 { + continue + } + + // Parse key. + key := strings.TrimSpace(parts[0]) + switch key { + case EnvDesktopDir, + EnvDownloadDir, + EnvDocumentsDir, + EnvMusicDir, + EnvPicturesDir, + EnvVideosDir, + EnvTemplatesDir, + EnvPublicShareDir: + default: + continue + } + + // Parse value. + runes := []rune(strings.TrimSpace(parts[1])) + + lenRunes := len(runes) + if lenRunes <= 2 || runes[0] != '"' { + continue + } + + for i := 1; i < lenRunes; i++ { + if runes[i] == '"' { + dirs[key] = pathutil.ExpandHome(string(runes[1:i])) + break + } + } + } + if err := scanner.Err(); err != nil { + return dirs + } + + return dirs +} diff --git a/internal/userdirs/config_unix_test.go b/internal/userdirs/config_unix_test.go new file mode 100644 index 0000000..6f76421 --- /dev/null +++ b/internal/userdirs/config_unix_test.go @@ -0,0 +1,93 @@ +//go:build aix || dragonfly || freebsd || (js && wasm) || nacl || linux || netbsd || openbsd || solaris + +package userdirs_test + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "github.com/adrg/xdg/internal/pathutil" + "github.com/adrg/xdg/internal/userdirs" + "github.com/stretchr/testify/require" +) + +func TestParseConfigFile(t *testing.T) { + // Test parsed values. + f, err := os.CreateTemp("", "test_parse_config_file") + require.NoError(t, err) + + var tmpFileRemoved bool + defer func() { + if !tmpFileRemoved { + os.Remove(f.Name()) + } + }() + + _, err = f.Write([]byte(`XDG_DOWNLOAD_DIR="/home/test/Downloads"`)) + require.NoError(t, err) + + err = f.Close() + require.NoError(t, err) + + dirs := userdirs.ParseConfigFile(f.Name()) + require.NotNil(t, dirs) + require.Equal(t, "/home/test/Downloads", dirs["XDG_DOWNLOAD_DIR"]) + + // Test non-existent file. + err = os.Remove(f.Name()) + require.NoError(t, err) + tmpFileRemoved = true + + dirs = userdirs.ParseConfigFile(f.Name()) + require.NotNil(t, dirs) +} + +func TestParseConfig(t *testing.T) { + // Test parsed values. + home := pathutil.UserHomeDir() + + dirs := userdirs.ParseConfig(strings.NewReader(` + # This file is written by xdg-user-dirs-update + # If you want to change or add directories, just edit the line you're + # interested in. All local changes will be retained on the next run. + # Format is XDG_xxx_DIR="$HOME/yyy", where yyy is a shell-escaped + # homedir-relative path, or XDG_xxx_DIR="/yyy", where /yyy is an + # absolute path. No other format is supported. + # + XDG_DESKTOP_DIR="$HOME/Desktop" + XDG_DOWNLOAD_DIR="$HOME/Downloads" + XDG_TEMPLATES_DIR="/home/test/Templates" + XDG_PUBLICSHARE_DIR="~/Public" + XDG_DOCUMENTS_DIR="$HOME/Documents" + XDG_MUSIC_DIR="$HOME/Music" # Music user directory + # XDG_PICTURES_DIR="$HOME/Pictures" + XDG_VIDEOS_DIR="" + + NON_XDG_DIR="ignore" + XDG_INVALID_DIR="ignore" + XDG_DOWNLOAD_DIR + `)) + + require.NotNil(t, dirs) + require.Equal(t, filepath.Join(home, "Desktop"), dirs["XDG_DESKTOP_DIR"]) + require.Equal(t, filepath.Join(home, "Downloads"), dirs["XDG_DOWNLOAD_DIR"]) + require.Equal(t, "/home/test/Templates", dirs["XDG_TEMPLATES_DIR"]) + require.Equal(t, filepath.Join(home, "Public"), dirs["XDG_PUBLICSHARE_DIR"]) + require.Equal(t, filepath.Join(home, "Documents"), dirs["XDG_DOCUMENTS_DIR"]) + require.Equal(t, filepath.Join(home, "Music"), dirs["XDG_MUSIC_DIR"]) + require.Equal(t, "", dirs["XDG_PICTURES_DIR"]) + require.Equal(t, "", dirs["XDG_VIDEOS_DIR"]) + + // Test reader error. + f, err := os.CreateTemp("", "test_parse_config") + require.NoError(t, err) + defer os.Remove(f.Name()) + + err = f.Close() + require.NoError(t, err) + + dirs = userdirs.ParseConfig(f) + require.NotNil(t, dirs) +} diff --git a/user_dirs.go b/internal/userdirs/userdirs.go similarity index 62% rename from user_dirs.go rename to internal/userdirs/userdirs.go index 7208874..b3c30cf 100644 --- a/user_dirs.go +++ b/internal/userdirs/userdirs.go @@ -1,19 +1,19 @@ -package xdg +package userdirs // XDG user directories environment variables. const ( - envDesktopDir = "XDG_DESKTOP_DIR" - envDownloadDir = "XDG_DOWNLOAD_DIR" - envDocumentsDir = "XDG_DOCUMENTS_DIR" - envMusicDir = "XDG_MUSIC_DIR" - envPicturesDir = "XDG_PICTURES_DIR" - envVideosDir = "XDG_VIDEOS_DIR" - envTemplatesDir = "XDG_TEMPLATES_DIR" - envPublicShareDir = "XDG_PUBLICSHARE_DIR" + EnvDesktopDir = "XDG_DESKTOP_DIR" + EnvDownloadDir = "XDG_DOWNLOAD_DIR" + EnvDocumentsDir = "XDG_DOCUMENTS_DIR" + EnvMusicDir = "XDG_MUSIC_DIR" + EnvPicturesDir = "XDG_PICTURES_DIR" + EnvVideosDir = "XDG_VIDEOS_DIR" + EnvTemplatesDir = "XDG_TEMPLATES_DIR" + EnvPublicShareDir = "XDG_PUBLICSHARE_DIR" ) -// UserDirectories defines the locations of well known user directories. -type UserDirectories struct { +// Directories defines the locations of well known user directories. +type Directories struct { // Desktop defines the location of the user's desktop directory. Desktop string diff --git a/paths_darwin.go b/paths_darwin.go index bfe9ad9..ccf903b 100644 --- a/paths_darwin.go +++ b/paths_darwin.go @@ -1,17 +1,11 @@ package xdg import ( - "os" "path/filepath" -) - -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - return "/" -} + "github.com/adrg/xdg/internal/pathutil" + "github.com/adrg/xdg/internal/userdirs" +) func initDirs(home string) { initBaseDirs(home) @@ -23,17 +17,17 @@ func initBaseDirs(home string) { rootAppSupport := "/Library/Application Support" // Initialize standard directories. - baseDirs.dataHome = xdgPath(envDataHome, homeAppSupport) - baseDirs.data = xdgPaths(envDataDirs, rootAppSupport) - baseDirs.configHome = xdgPath(envConfigHome, homeAppSupport) - baseDirs.config = xdgPaths(envConfigDirs, + baseDirs.dataHome = pathutil.EnvPath(envDataHome, homeAppSupport) + baseDirs.data = pathutil.EnvPathList(envDataDirs, rootAppSupport) + baseDirs.configHome = pathutil.EnvPath(envConfigHome, homeAppSupport) + baseDirs.config = pathutil.EnvPathList(envConfigDirs, filepath.Join(home, "Library", "Preferences"), rootAppSupport, "/Library/Preferences", ) - baseDirs.stateHome = xdgPath(envStateHome, homeAppSupport) - baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, "Library", "Caches")) - baseDirs.runtime = xdgPath(envRuntimeDir, homeAppSupport) + baseDirs.stateHome = pathutil.EnvPath(envStateHome, homeAppSupport) + baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(home, "Library", "Caches")) + baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, homeAppSupport) // Initialize non-standard directories. baseDirs.applications = []string{ @@ -49,12 +43,12 @@ func initBaseDirs(home string) { } func initUserDirs(home string) { - UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) - UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) - UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) - UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) - UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) - UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Movies")) - UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) - UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public")) + UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, filepath.Join(home, "Desktop")) + UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, filepath.Join(home, "Downloads")) + UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, filepath.Join(home, "Documents")) + UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, filepath.Join(home, "Music")) + UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, filepath.Join(home, "Pictures")) + UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, filepath.Join(home, "Movies")) + UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, filepath.Join(home, "Templates")) + UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, filepath.Join(home, "Public")) } diff --git a/paths_plan9.go b/paths_plan9.go index 2882f68..0cab83c 100644 --- a/paths_plan9.go +++ b/paths_plan9.go @@ -1,17 +1,11 @@ package xdg import ( - "os" "path/filepath" -) - -func homeDir() string { - if home := os.Getenv("home"); home != "" { - return home - } - return "/" -} + "github.com/adrg/xdg/internal/pathutil" + "github.com/adrg/xdg/internal/userdirs" +) func initDirs(home string) { initBaseDirs(home) @@ -23,13 +17,13 @@ func initBaseDirs(home string) { rootLibDir := "/lib" // Initialize standard directories. - baseDirs.dataHome = xdgPath(envDataHome, homeLibDir) - baseDirs.data = xdgPaths(envDataDirs, rootLibDir) - baseDirs.configHome = xdgPath(envConfigHome, homeLibDir) - baseDirs.config = xdgPaths(envConfigDirs, rootLibDir) - baseDirs.stateHome = xdgPath(envStateHome, filepath.Join(homeLibDir, "state")) - baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(homeLibDir, "cache")) - baseDirs.runtime = xdgPath(envRuntimeDir, "/tmp") + baseDirs.dataHome = pathutil.EnvPath(envDataHome, homeLibDir) + baseDirs.data = pathutil.EnvPathList(envDataDirs, rootLibDir) + baseDirs.configHome = pathutil.EnvPath(envConfigHome, homeLibDir) + baseDirs.config = pathutil.EnvPathList(envConfigDirs, rootLibDir) + baseDirs.stateHome = pathutil.EnvPath(envStateHome, filepath.Join(homeLibDir, "state")) + baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(homeLibDir, "cache")) + baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, "/tmp") // Initialize non-standard directories. baseDirs.applications = []string{ @@ -44,12 +38,12 @@ func initBaseDirs(home string) { } func initUserDirs(home string) { - UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "desktop")) - UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "downloads")) - UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "documents")) - UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "music")) - UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "pictures")) - UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "videos")) - UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "templates")) - UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "public")) + UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, filepath.Join(home, "desktop")) + UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, filepath.Join(home, "downloads")) + UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, filepath.Join(home, "documents")) + UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, filepath.Join(home, "music")) + UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, filepath.Join(home, "pictures")) + UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, filepath.Join(home, "videos")) + UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, filepath.Join(home, "templates")) + UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, filepath.Join(home, "public")) } diff --git a/paths_unix.go b/paths_unix.go index 8af2705..a87a601 100644 --- a/paths_unix.go +++ b/paths_unix.go @@ -8,30 +8,23 @@ import ( "strconv" "github.com/adrg/xdg/internal/pathutil" + "github.com/adrg/xdg/internal/userdirs" ) -func homeDir() string { - if home := os.Getenv("HOME"); home != "" { - return home - } - - return "/" -} - func initDirs(home string) { initBaseDirs(home) - initUserDirs(home) + initUserDirs(home, baseDirs.configHome) } func initBaseDirs(home string) { // Initialize standard directories. - baseDirs.dataHome = xdgPath(envDataHome, filepath.Join(home, ".local", "share")) - baseDirs.data = xdgPaths(envDataDirs, "/usr/local/share", "/usr/share") - baseDirs.configHome = xdgPath(envConfigHome, filepath.Join(home, ".config")) - baseDirs.config = xdgPaths(envConfigDirs, "/etc/xdg") - baseDirs.stateHome = xdgPath(envStateHome, filepath.Join(home, ".local", "state")) - baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(home, ".cache")) - baseDirs.runtime = xdgPath(envRuntimeDir, filepath.Join("/run/user", strconv.Itoa(os.Getuid()))) + baseDirs.dataHome = pathutil.EnvPath(envDataHome, filepath.Join(home, ".local", "share")) + baseDirs.data = pathutil.EnvPathList(envDataDirs, "/usr/local/share", "/usr/share") + baseDirs.configHome = pathutil.EnvPath(envConfigHome, filepath.Join(home, ".config")) + baseDirs.config = pathutil.EnvPathList(envConfigDirs, "/etc/xdg") + baseDirs.stateHome = pathutil.EnvPath(envStateHome, filepath.Join(home, ".local", "state")) + baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(home, ".cache")) + baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, filepath.Join("/run/user", strconv.Itoa(os.Getuid()))) // Initialize non-standard directories. appDirs := []string{ @@ -54,17 +47,19 @@ func initBaseDirs(home string) { fontDirs = append(fontDirs, filepath.Join(dir, "fonts")) } - baseDirs.applications = pathutil.Unique(appDirs, Home) - baseDirs.fonts = pathutil.Unique(fontDirs, Home) + baseDirs.applications = pathutil.Unique(appDirs) + baseDirs.fonts = pathutil.Unique(fontDirs) } -func initUserDirs(home string) { - UserDirs.Desktop = xdgPath(envDesktopDir, filepath.Join(home, "Desktop")) - UserDirs.Download = xdgPath(envDownloadDir, filepath.Join(home, "Downloads")) - UserDirs.Documents = xdgPath(envDocumentsDir, filepath.Join(home, "Documents")) - UserDirs.Music = xdgPath(envMusicDir, filepath.Join(home, "Music")) - UserDirs.Pictures = xdgPath(envPicturesDir, filepath.Join(home, "Pictures")) - UserDirs.Videos = xdgPath(envVideosDir, filepath.Join(home, "Videos")) - UserDirs.Templates = xdgPath(envTemplatesDir, filepath.Join(home, "Templates")) - UserDirs.PublicShare = xdgPath(envPublicShareDir, filepath.Join(home, "Public")) +func initUserDirs(home, configHome string) { + userDirsMap := userdirs.ParseConfigFile(filepath.Join(configHome, "user-dirs.dirs")) + + UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, userDirsMap[userdirs.EnvDesktopDir], filepath.Join(home, "Desktop")) + UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, userDirsMap[userdirs.EnvDownloadDir], filepath.Join(home, "Downloads")) + UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, userDirsMap[userdirs.EnvDocumentsDir], filepath.Join(home, "Documents")) + UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, userDirsMap[userdirs.EnvMusicDir], filepath.Join(home, "Music")) + UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, userDirsMap[userdirs.EnvPicturesDir], filepath.Join(home, "Pictures")) + UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, userDirsMap[userdirs.EnvVideosDir], filepath.Join(home, "Videos")) + UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, userDirsMap[userdirs.EnvTemplatesDir], filepath.Join(home, "Templates")) + UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, userDirsMap[userdirs.EnvPublicShareDir], filepath.Join(home, "Public")) } diff --git a/paths_windows.go b/paths_windows.go index 722d3e7..eaac2d3 100644 --- a/paths_windows.go +++ b/paths_windows.go @@ -4,17 +4,10 @@ import ( "path/filepath" "github.com/adrg/xdg/internal/pathutil" + "github.com/adrg/xdg/internal/userdirs" "golang.org/x/sys/windows" ) -func homeDir() string { - return pathutil.KnownFolder( - windows.FOLDERID_Profile, - []string{"USERPROFILE"}, - nil, - ) -} - func initDirs(home string) { kf := initKnownFolders(home) initBaseDirs(home, kf) @@ -23,13 +16,13 @@ func initDirs(home string) { func initBaseDirs(home string, kf *knownFolders) { // Initialize standard directories. - baseDirs.dataHome = xdgPath(envDataHome, kf.localAppData) - baseDirs.data = xdgPaths(envDataDirs, kf.roamingAppData, kf.programData) - baseDirs.configHome = xdgPath(envConfigHome, kf.localAppData) - baseDirs.config = xdgPaths(envConfigDirs, kf.programData, kf.roamingAppData) - baseDirs.stateHome = xdgPath(envStateHome, kf.localAppData) - baseDirs.cacheHome = xdgPath(envCacheHome, filepath.Join(kf.localAppData, "cache")) - baseDirs.runtime = xdgPath(envRuntimeDir, kf.localAppData) + baseDirs.dataHome = pathutil.EnvPath(envDataHome, kf.localAppData) + baseDirs.data = pathutil.EnvPathList(envDataDirs, kf.roamingAppData, kf.programData) + baseDirs.configHome = pathutil.EnvPath(envConfigHome, kf.localAppData) + baseDirs.config = pathutil.EnvPathList(envConfigDirs, kf.programData, kf.roamingAppData) + baseDirs.stateHome = pathutil.EnvPath(envStateHome, kf.localAppData) + baseDirs.cacheHome = pathutil.EnvPath(envCacheHome, filepath.Join(kf.localAppData, "cache")) + baseDirs.runtime = pathutil.EnvPath(envRuntimeDir, kf.localAppData) // Initialize non-standard directories. baseDirs.applications = []string{ @@ -43,14 +36,14 @@ func initBaseDirs(home string, kf *knownFolders) { } func initUserDirs(home string, kf *knownFolders) { - UserDirs.Desktop = xdgPath(envDesktopDir, kf.desktop) - UserDirs.Download = xdgPath(envDownloadDir, kf.downloads) - UserDirs.Documents = xdgPath(envDocumentsDir, kf.documents) - UserDirs.Music = xdgPath(envMusicDir, kf.music) - UserDirs.Pictures = xdgPath(envPicturesDir, kf.pictures) - UserDirs.Videos = xdgPath(envVideosDir, kf.videos) - UserDirs.Templates = xdgPath(envTemplatesDir, kf.templates) - UserDirs.PublicShare = xdgPath(envPublicShareDir, kf.public) + UserDirs.Desktop = pathutil.EnvPath(userdirs.EnvDesktopDir, kf.desktop) + UserDirs.Download = pathutil.EnvPath(userdirs.EnvDownloadDir, kf.downloads) + UserDirs.Documents = pathutil.EnvPath(userdirs.EnvDocumentsDir, kf.documents) + UserDirs.Music = pathutil.EnvPath(userdirs.EnvMusicDir, kf.music) + UserDirs.Pictures = pathutil.EnvPath(userdirs.EnvPicturesDir, kf.pictures) + UserDirs.Videos = pathutil.EnvPath(userdirs.EnvVideosDir, kf.videos) + UserDirs.Templates = pathutil.EnvPath(userdirs.EnvTemplatesDir, kf.templates) + UserDirs.PublicShare = pathutil.EnvPath(userdirs.EnvPublicShareDir, kf.public) } type knownFolders struct { diff --git a/xdg.go b/xdg.go index 24683bc..32574ce 100644 --- a/xdg.go +++ b/xdg.go @@ -1,12 +1,13 @@ package xdg import ( - "os" - "path/filepath" - "github.com/adrg/xdg/internal/pathutil" + "github.com/adrg/xdg/internal/userdirs" ) +// UserDirectories defines the locations of well known user directories. +type UserDirectories = userdirs.Directories + var ( // Home contains the path of the user's home directory. Home string @@ -88,7 +89,7 @@ func init() { // in the environment. func Reload() { // Initialize home directory. - Home = homeDir() + Home = pathutil.UserHomeDir() // Initialize base and user directories. initDirs(Home) @@ -198,21 +199,3 @@ func SearchCacheFile(relPath string) (string, error) { func SearchRuntimeFile(relPath string) (string, error) { return baseDirs.searchRuntimeFile(relPath) } - -func xdgPath(name, defaultPath string) string { - dir := pathutil.ExpandHome(os.Getenv(name), Home) - if dir != "" && filepath.IsAbs(dir) { - return dir - } - - return defaultPath -} - -func xdgPaths(name string, defaultPaths ...string) []string { - dirs := pathutil.Unique(filepath.SplitList(os.Getenv(name)), Home) - if len(dirs) != 0 { - return dirs - } - - return pathutil.Unique(defaultPaths, Home) -} diff --git a/xdg_test.go b/xdg_test.go index b29044e..72e1910 100644 --- a/xdg_test.go +++ b/xdg_test.go @@ -3,6 +3,7 @@ package xdg_test import ( "os" "path/filepath" + "strings" "testing" "github.com/stretchr/testify/require" @@ -18,6 +19,21 @@ type envSample struct { } func testDirs(t *testing.T, samples ...*envSample) { + // Reset environment after test execution. + environ := os.Environ() + defer func() { + os.Clearenv() + for _, env := range environ { + parts := strings.SplitN(env, "=", 2) + if len(parts) != 2 { + continue + } + os.Setenv(parts[0], parts[1]) + } + + xdg.Reload() + }() + // Test home directory. require.NotEmpty(t, xdg.Home) t.Logf("Home: %s", xdg.Home)