1 package pathutil
2 3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7 )
8 9 // Unique eliminates the duplicate paths from the provided slice and returns
10 // the result. The paths are expanded using the `ExpandHome` function and only
11 // absolute paths are kept. The items in the output slice are in the order in
12 // which they occur in the input slice.
13 func Unique(paths []string) []string {
14 var (
15 uniq []string
16 registry = map[string]struct{}{}
17 )
18 19 for _, p := range paths {
20 if p = ExpandHome(p); p != "" && filepath.IsAbs(p) {
21 if _, ok := registry[p]; ok {
22 continue
23 }
24 25 registry[p] = struct{}{}
26 uniq = append(uniq, p)
27 }
28 }
29 30 return uniq
31 }
32 33 // First returns the first absolute path from the provided slice.
34 // The paths in the input slice are expanded using the `ExpandHome` function.
35 func First(paths []string) string {
36 for _, p := range paths {
37 if p = ExpandHome(p); p != "" && filepath.IsAbs(p) {
38 return p
39 }
40 }
41 42 return ""
43 }
44 45 // Create returns a suitable location relative to which the file with the
46 // specified `name` can be written. The first path from the provided `paths`
47 // slice which is successfully created (or already exists) is used as a base
48 // path for the file. The `name` parameter should contain the name of the file
49 // which is going to be written in the location returned by this function, but
50 // it can also contain a set of parent directories, which will be created
51 // relative to the selected parent path.
52 func Create(name string, paths []string) (string, error) {
53 searchedPaths := make([]string, 0, len(paths))
54 for _, p := range paths {
55 p = filepath.Join(p, name)
56 57 dir := filepath.Dir(p)
58 if Exists(dir) {
59 return p, nil
60 }
61 if err := os.MkdirAll(dir, os.ModeDir|0o700); err == nil {
62 return p, nil
63 }
64 65 searchedPaths = append(searchedPaths, dir)
66 }
67 68 return "", fmt.Errorf("could not create any of the following paths: %v",
69 searchedPaths)
70 }
71 72 // Search searches for the file with the specified `name` in the provided
73 // slice of `paths`. The `name` parameter must contain the name of the file,
74 // but it can also contain a set of parent directories.
75 func Search(name string, paths []string) (string, error) {
76 searchedPaths := make([]string, 0, len(paths))
77 for _, p := range paths {
78 p = filepath.Join(p, name)
79 if Exists(p) {
80 return p, nil
81 }
82 83 searchedPaths = append(searchedPaths, filepath.Dir(p))
84 }
85 86 return "", fmt.Errorf("could not locate `%s` in any of the following paths: %v",
87 filepath.Base(name), searchedPaths)
88 }
89 90 // EnvPath returns the value of the environment variable with the specified
91 // `name` if it is an absolute path, or the first absolute fallback path.
92 // All paths are expanded using the `ExpandHome` function.
93 func EnvPath(name string, fallbackPaths ...string) string {
94 dir := ExpandHome(os.Getenv(name))
95 if dir != "" && filepath.IsAbs(dir) {
96 return dir
97 }
98 99 return First(fallbackPaths)
100 }
101 102 // EnvPathList reads the value of the environment variable with the specified
103 // `name` and attempts to extract a list of absolute paths from it. If there
104 // are none, a list of absolute fallback paths is returned instead. Duplicate
105 // paths are removed from the returned slice. All paths are expanded using the
106 // `ExpandHome` function.
107 func EnvPathList(name string, fallbackPaths ...string) []string {
108 dirs := Unique(filepath.SplitList(os.Getenv(name)))
109 if len(dirs) != 0 {
110 return dirs
111 }
112 113 return Unique(fallbackPaths)
114 }
115