Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Parse user directories config file #87

Merged
merged 15 commits into from
Jul 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<details open>
<summary><strong>Unix-like operating systems</strong></summary>
<br/>
Expand Down Expand Up @@ -156,7 +167,7 @@ Sensible fallback locations are used for the folders which are not set.
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------: | :-------------------------------------------------------------------------------------------------------------------------------------: |
| <kbd><b>Home</b></kbd> | <kbd>Profile</kbd> | <kbd>%USERPROFILE%</kbd> |
| <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> |
| <kbd><b>Fonts</b></kbd> | <kbd>Fonts</kbd><br/><kbd>-</kbd> | <kbd>%SystemRoot%\Fonts</kbd><br/><kbd>%LOCALAPPDATA%\Microsoft\Windows\Fonts</kbd> |
| <kbd><b>Fonts</b></kbd> | <kbd>Fonts</kbd> | <kbd>%SystemRoot%\Fonts</kbd><br/><kbd>%LOCALAPPDATA%\Microsoft\Windows\Fonts</kbd> |

</details>

Expand Down
49 changes: 43 additions & 6 deletions internal/pathutil/pathutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -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
Expand Down Expand Up @@ -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)
}
15 changes: 12 additions & 3 deletions internal/pathutil/pathutil_plan9.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
76 changes: 50 additions & 26 deletions internal/pathutil/pathutil_plan9_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"}))
}
34 changes: 34 additions & 0 deletions internal/pathutil/pathutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package pathutil_test
import (
"os"
"path/filepath"
"strings"
"testing"

"github.com/stretchr/testify/require"
Expand Down Expand Up @@ -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)))
}
15 changes: 12 additions & 3 deletions internal/pathutil/pathutil_unix.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down
73 changes: 48 additions & 25 deletions internal/pathutil/pathutil_unix_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
package pathutil_test

import (
"os"
"path/filepath"
"testing"

Expand All @@ -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"}))
}
Loading
Loading