diff --git a/pkg/utils/path.go b/pkg/utils/path.go index f132f45..a7b893a 100644 --- a/pkg/utils/path.go +++ b/pkg/utils/path.go @@ -1,20 +1,67 @@ package utils import ( + "fmt" "os" + "os/user" "path/filepath" "strings" ) func ExpandPath(path string) (string, error) { - if strings.HasPrefix(path, "~") { + if len(path) == 0 { + return path, nil + } + + if !strings.HasPrefix(path, "~") { + return filepath.Abs(os.ExpandEnv(path)) + } + + // [...] the characters in the tilde-prefix following the are treated + // as a possible login name from the user database. [...]. + + parts := pathSegments(path) + if len(parts[0]) == 1 { + // If the login name is null (that is, the tilde-prefix contains only the tilde), + // the tilde-prefix is replaced by the value of the variable HOME. If HOME is + // unset, the results are unspecified. [continue] home, err := os.UserHomeDir() if err != nil { - return "", err + return path, err } + path = strings.Replace(path, "~", home, 1) + + return filepath.Abs(os.ExpandEnv(path)) + } + + // Otherwise, the tilde-prefix shall be replaced + // by a pathname of the initial working directory associated with the login name + // obtained using the getpwnam() function as defined in the System Interfaces volume + // of POSIX.1-2017. If the system does not recognize the login name, the results are + // undefined. + + // treat what follows the tilde as a potentially valid username + + usr, err := user.Lookup(strings.TrimPrefix(parts[0], "~")) + if err == nil { + // replace the tilde-prefix with the user's home directory + return filepath.Abs(os.ExpandEnv(strings.Replace(path, parts[0], usr.HomeDir, 1))) + } + + switch terr := err.(type) { + case user.UnknownUserError: // non existing user, move on + default: // unexpected error + return path, fmt.Errorf("ExpandPath: got unexpected error %v", terr) } - path = os.ExpandEnv(path) - return filepath.Abs(path) + return os.ExpandEnv(path), nil +} + +func pathSegments(path string) []string { + dir, last := filepath.Split(path) + if dir == "" { + return []string{last} + } + return append(pathSegments(filepath.Clean(dir)), last) } diff --git a/pkg/utils/path_test.go b/pkg/utils/path_test.go new file mode 100644 index 0000000..74dd9d7 --- /dev/null +++ b/pkg/utils/path_test.go @@ -0,0 +1,63 @@ +package utils + +import ( + "fmt" + "log" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestExpandPath(t *testing.T) { + tests := []struct { + name string + path string + want string + wantErr bool + }{ + {"empty", "", "", false}, + {"non existing user", "~NONEXISTING/", "~NONEXISTING/", false}, + {"any path", "/patgh/to/file", "/patgh/to/file", false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ExpandPath(tt.path) + if tt.wantErr { + assert.NotNil(t, err) + return + } + + assert.Nil(t, err) + assert.Equal(t, tt.want, got) + }) + } +} + +func ExampleExpandPath() { + p, err := ExpandPath("~/test") + if err != nil { + log.Fatal(err) + } + + _, last := filepath.Split(p) + fmt.Println(last) + + p, _ = ExpandPath("~NONEXISTINGUSER/path/to/file") + fmt.Println(p) + + p, _ = ExpandPath("/path/to/file/tilde/~") + fmt.Println(p) + + p, _ = ExpandPath("") + fmt.Println(p) + + p, _ = ExpandPath("") + fmt.Println(p) + + // Output: + // test + // ~NONEXISTINGUSER/path/to/file + // /path/to/file/tilde/~ + // +}