diff --git a/internal/config/expand.go b/internal/config/expand.go index a268da3..c1d3702 100644 --- a/internal/config/expand.go +++ b/internal/config/expand.go @@ -2,12 +2,15 @@ package config import ( "os" + "path/filepath" + "strings" ) // ExpandEnv expands environment variables in config values. // It processes: // - env map values: expands $VAR and ${VAR} using provided environment // - volumes paths: expands variables in volume mount strings +// - file paths: expands ~/ prefix to user's home directory in WorkingDir, Dockerfile, and Volumes // // Uses os.ExpandEnv behavior: undefined variables expand to empty string. // Returns a new Config with expanded values. @@ -33,9 +36,30 @@ func ExpandEnv(cfg Config, environment []string) Config { if cfg.Volumes != nil { result.Volumes = make([]string, len(cfg.Volumes)) for i, volume := range cfg.Volumes { - result.Volumes[i] = os.Expand(volume, mapper) + expanded := os.Expand(volume, mapper) + result.Volumes[i] = expandHome(expanded) } } + // Expand home directory in file path fields + result.WorkingDir = expandHome(cfg.WorkingDir) + result.Dockerfile = expandHome(cfg.Dockerfile) + return result } + +// expandHome expands the tilde (~) prefix in a path to the user's home directory. +// Only paths that start with ~/ are expanded. Other instances of ~ are left as literals. +// If the path starts with ~/ but the home directory cannot be determined, the path is returned unchanged. +func expandHome(path string) string { + if !strings.HasPrefix(path, "~/") { + return path + } + + home, err := os.UserHomeDir() + if err != nil || home == "" { + return path + } + + return filepath.Join(home, path[2:]) +} diff --git a/internal/config/expand_test.go b/internal/config/expand_test.go index 3f0686b..4884721 100644 --- a/internal/config/expand_test.go +++ b/internal/config/expand_test.go @@ -1,6 +1,8 @@ package config import ( + "os" + "path/filepath" "testing" "time" @@ -183,3 +185,134 @@ func TestExpandEnv(t *testing.T) { require.Equal(t, "/home/user/data:/data", result.Volumes[0]) }) } + +func TestExpandHome(t *testing.T) { + t.Run("ExpandsHomeDirectory", func(t *testing.T) { + result := expandHome("~/config") + + // Should expand to actual home directory + home, err := os.UserHomeDir() + require.NoError(t, err) + require.Equal(t, filepath.Join(home, "config"), result) + }) + + t.Run("ExpandsHomeDirWithSubpath", func(t *testing.T) { + result := expandHome("~/projects/myapp") + + home, err := os.UserHomeDir() + require.NoError(t, err) + require.Equal(t, filepath.Join(home, "projects/myapp"), result) + }) + + t.Run("DoesNotExpandTildeNotAtStart", func(t *testing.T) { + result := expandHome("/path/to/~/file") + + require.Equal(t, "/path/to/~/file", result) + }) + + t.Run("DoesNotExpandTildeWithoutSlash", func(t *testing.T) { + result := expandHome("~file") + + require.Equal(t, "~file", result) + }) + + t.Run("HandlesRootPath", func(t *testing.T) { + result := expandHome("/absolute/path") + + require.Equal(t, "/absolute/path", result) + }) + + t.Run("HandlesEmptyPath", func(t *testing.T) { + result := expandHome("") + + require.Equal(t, "", result) + }) +} + +func TestExpandEnvWithHomeDirectory(t *testing.T) { + // Get actual home directory for test assertions + home, err := os.UserHomeDir() + require.NoError(t, err) + + t.Run("ExpandsWorkingDir", func(t *testing.T) { + cfg := Config{ + WorkingDir: "~/workspace", + } + environment := []string{} + + result := ExpandEnv(cfg, environment) + + require.Equal(t, filepath.Join(home, "workspace"), result.WorkingDir) + }) + + t.Run("ExpandsDockerfile", func(t *testing.T) { + cfg := Config{ + Dockerfile: "~/dockerfiles/Dockerfile.dev", + } + environment := []string{} + + result := ExpandEnv(cfg, environment) + + require.Equal(t, filepath.Join(home, "dockerfiles/Dockerfile.dev"), result.Dockerfile) + }) + + t.Run("ExpandsHomeInVolumes", func(t *testing.T) { + cfg := Config{ + Volumes: []string{ + "~/data:/data", + "~/cache:/cache", + }, + } + environment := []string{} + + result := ExpandEnv(cfg, environment) + + require.Equal(t, filepath.Join(home, "data")+":/data", result.Volumes[0]) + require.Equal(t, filepath.Join(home, "cache")+":/cache", result.Volumes[1]) + }) + + t.Run("ExpandsHomeAfterEnvVar", func(t *testing.T) { + cfg := Config{ + Volumes: []string{ + "$HOME/data:/data", + }, + } + environment := []string{"HOME=" + home} + + result := ExpandEnv(cfg, environment) + + require.Equal(t, filepath.Join(home, "data")+":/data", result.Volumes[0]) + }) + + t.Run("DoesNotExpandNonHomePrefix", func(t *testing.T) { + cfg := Config{ + WorkingDir: "/workspace", + Dockerfile: "./Dockerfile", + } + environment := []string{} + + result := ExpandEnv(cfg, environment) + + require.Equal(t, "/workspace", result.WorkingDir) + require.Equal(t, "./Dockerfile", result.Dockerfile) + }) + + t.Run("ExpandsMultiplePathsWithHome", func(t *testing.T) { + cfg := Config{ + WorkingDir: "~/app", + Dockerfile: "~/docker/Dockerfile", + Volumes: []string{ + "~/data:/data", + "/absolute:/absolute", + }, + } + environment := []string{} + + result := ExpandEnv(cfg, environment) + + require.Equal(t, filepath.Join(home, "app"), result.WorkingDir) + require.Equal(t, filepath.Join(home, "docker/Dockerfile"), result.Dockerfile) + require.Equal(t, filepath.Join(home, "data")+":/data", result.Volumes[0]) + require.Equal(t, "/absolute:/absolute", result.Volumes[1]) + }) +}