Skip to content

Commit

Permalink
Merge pull request #87 from adrg/parse-user-dirs
Browse files Browse the repository at this point in the history
Parse user directories config file
  • Loading branch information
adrg authored Jul 8, 2024
2 parents df5884b + ed625ee commit fe619a1
Show file tree
Hide file tree
Showing 18 changed files with 540 additions and 216 deletions.
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

0 comments on commit fe619a1

Please sign in to comment.