Skip to content

Commit 30fe5a1

Browse files
authored
fix: expand leading ~ in path flags and profiles (#48)
1 parent 5576172 commit 30fe5a1

6 files changed

Lines changed: 159 additions & 0 deletions

File tree

cmd/root.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,11 @@ var rootCmd = &cobra.Command{
7575
}
7676
}
7777

78+
// Expand a leading "~" in path flags; a quoted "~/..." argument is
79+
// never expanded by the shell and would otherwise be treated as a
80+
// literal directory name relative to the working directory.
81+
expandFlagPaths()
82+
7883
if logFile == "" {
7984
if logsDir, err := paths.LogsDir(); err == nil {
8085
logFile = filepath.Join(logsDir, time.Now().Format("2006-01-02_15-04-05")+".log")
@@ -155,6 +160,16 @@ func init() {
155160
rootCmd.PersistentFlags().StringVar(&logFile, "log-file", "", "Write command output to a log file")
156161
}
157162

163+
// expandFlagPaths expands a leading "~" in the path-valued global flags to the
164+
// user's home directory. Shells only expand an unquoted "~", so a quoted
165+
// argument like "~/.local/share" reaches the program literally.
166+
func expandFlagPaths() {
167+
instanceDir = paths.ExpandTilde(instanceDir)
168+
logFile = paths.ExpandTilde(logFile)
169+
cacheDir = paths.ExpandTilde(cacheDir)
170+
cacheDirAll = paths.ExpandTilde(cacheDirAll)
171+
}
172+
158173
func getGithubToken() string {
159174
if githubToken != "" {
160175
return githubToken

cmd/root_test.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,40 @@ package cmd
22

33
import (
44
"errors"
5+
"path/filepath"
56
"testing"
67

78
"github.com/spf13/cobra"
89
)
910

11+
func TestExpandFlagPaths(t *testing.T) {
12+
home := t.TempDir()
13+
t.Setenv("HOME", home)
14+
15+
instanceDir = "~/.local/share/instances/foo"
16+
logFile = "~/logs/run.log"
17+
cacheDir = "~/cache"
18+
cacheDirAll = ".local/relative"
19+
t.Cleanup(func() {
20+
instanceDir, logFile, cacheDir, cacheDirAll = ".", "", "", ""
21+
})
22+
23+
expandFlagPaths()
24+
25+
if want := filepath.Join(home, ".local", "share", "instances", "foo"); instanceDir != want {
26+
t.Errorf("instanceDir = %q, want %q", instanceDir, want)
27+
}
28+
if want := filepath.Join(home, "logs", "run.log"); logFile != want {
29+
t.Errorf("logFile = %q, want %q", logFile, want)
30+
}
31+
if want := filepath.Join(home, "cache"); cacheDir != want {
32+
t.Errorf("cacheDir = %q, want %q", cacheDir, want)
33+
}
34+
if cacheDirAll != ".local/relative" {
35+
t.Errorf("cacheDirAll = %q, want unchanged relative path", cacheDirAll)
36+
}
37+
}
38+
1039
func TestUsageArgsWrapsValidationErrors(t *testing.T) {
1140
wrapped := usageArgs(cobra.ExactArgs(1))
1241
cmd := &cobra.Command{Use: "test"}

internal/paths/expand.go

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package paths
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
)
7+
8+
// ExpandTilde expands a leading "~" (alone or followed by a path separator)
9+
// to the current user's home directory. Shells only expand an unquoted "~",
10+
// so a quoted argument like "~/.local/share" reaches the program literally;
11+
// this restores the expected behavior.
12+
//
13+
// "~user" syntax is not supported and is returned unchanged, as is any path
14+
// where "~" is not the leading element.
15+
func ExpandTilde(p string) string {
16+
if p == "" || p[0] != '~' {
17+
return p
18+
}
19+
if p == "~" {
20+
if home, err := os.UserHomeDir(); err == nil {
21+
return home
22+
}
23+
return p
24+
}
25+
if p[1] == '/' || p[1] == os.PathSeparator {
26+
if home, err := os.UserHomeDir(); err == nil {
27+
return filepath.Join(home, p[2:])
28+
}
29+
}
30+
return p
31+
}

internal/paths/expand_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package paths
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"testing"
7+
)
8+
9+
func TestExpandTilde(t *testing.T) {
10+
home, err := os.UserHomeDir()
11+
if err != nil {
12+
t.Fatalf("UserHomeDir: %v", err)
13+
}
14+
15+
tests := []struct {
16+
name string
17+
in string
18+
want string
19+
}{
20+
{"bare tilde", "~", home},
21+
{"tilde slash", "~/", home},
22+
{"tilde path", "~/.local/share", filepath.Join(home, ".local", "share")},
23+
{"empty", "", ""},
24+
{"absolute unchanged", "/home/ethan/.local", "/home/ethan/.local"},
25+
{"relative unchanged", ".local/share", ".local/share"},
26+
{"tilde mid-path unchanged", "foo/~/bar", "foo/~/bar"},
27+
{"tilde user unchanged", "~ethan/.local", "~ethan/.local"},
28+
}
29+
30+
for _, tt := range tests {
31+
t.Run(tt.name, func(t *testing.T) {
32+
got := ExpandTilde(tt.in)
33+
if got != tt.want {
34+
t.Errorf("ExpandTilde(%q) = %q, want %q", tt.in, got, tt.want)
35+
}
36+
})
37+
}
38+
}

internal/profile/profile.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,14 @@ func Load(name string) (*Profile, error) {
5353
}
5454
}
5555

56+
// Expand a leading "~" in stored path values; profiles are hand-editable
57+
// and a quoted "~" in the source is never expanded by the shell.
58+
for _, ptr := range []*string{p.InstanceDir, p.CacheDir, p.LogFile} {
59+
if ptr != nil {
60+
*ptr = paths.ExpandTilde(*ptr)
61+
}
62+
}
63+
5664
return &p, nil
5765
}
5866

internal/profile/profile_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package profile
2+
3+
import (
4+
"path/filepath"
5+
"testing"
6+
)
7+
8+
func TestLoadExpandsTildePaths(t *testing.T) {
9+
home := t.TempDir()
10+
t.Setenv("HOME", home)
11+
t.Setenv("XDG_CONFIG_HOME", t.TempDir())
12+
13+
inst := "~/instances/foo"
14+
cache := "~/cache/mods"
15+
logf := "~/logs/run.log"
16+
p := &Profile{InstanceDir: &inst, CacheDir: &cache, LogFile: &logf}
17+
if err := Save("tilde", p); err != nil {
18+
t.Fatalf("Save: %v", err)
19+
}
20+
21+
got, err := Load("tilde")
22+
if err != nil {
23+
t.Fatalf("Load: %v", err)
24+
}
25+
26+
wantInst := filepath.Join(home, "instances", "foo")
27+
if got.InstanceDir == nil || *got.InstanceDir != wantInst {
28+
t.Errorf("InstanceDir = %v, want %q", got.InstanceDir, wantInst)
29+
}
30+
wantCache := filepath.Join(home, "cache", "mods")
31+
if got.CacheDir == nil || *got.CacheDir != wantCache {
32+
t.Errorf("CacheDir = %v, want %q", got.CacheDir, wantCache)
33+
}
34+
wantLog := filepath.Join(home, "logs", "run.log")
35+
if got.LogFile == nil || *got.LogFile != wantLog {
36+
t.Errorf("LogFile = %v, want %q", got.LogFile, wantLog)
37+
}
38+
}

0 commit comments

Comments
 (0)