From 59d599d6efcab94e30ff86f54fc59213e780025c Mon Sep 17 00:00:00 2001 From: tensorworker Date: Tue, 13 Jan 2026 21:46:06 -0500 Subject: [PATCH] fix: expand tilde in --env-file paths to user home directory When using --env-file=~/.env, the tilde was not expanded to the user's home directory. Instead, it was treated as a literal character and resolved relative to the current working directory, resulting in errors like "couldn't find env file: /current/dir/~/.env". This adds an ExpandUser function that expands ~ to the home directory before converting relative paths to absolute paths. Fixes #13508 Signed-off-by: tensorworker --- cmd/compose/compose.go | 4 ++ internal/paths/paths.go | 24 +++++++++++ internal/paths/paths_test.go | 84 ++++++++++++++++++++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 internal/paths/paths_test.go diff --git a/cmd/compose/compose.go b/cmd/compose/compose.go index 7796a1a6c2d..5a3860bde18 100644 --- a/cmd/compose/compose.go +++ b/cmd/compose/compose.go @@ -46,6 +46,7 @@ import ( "github.com/docker/compose/v5/cmd/display" "github.com/docker/compose/v5/cmd/formatter" + "github.com/docker/compose/v5/internal/paths" "github.com/docker/compose/v5/internal/tracing" "github.com/docker/compose/v5/pkg/api" "github.com/docker/compose/v5/pkg/compose" @@ -550,12 +551,15 @@ func RootCommand(dockerCli command.Cli, backendOptions *BackendOptions) *cobra.C fmt.Fprint(os.Stderr, aec.Apply("option '--workdir' is DEPRECATED at root level! Please use '--project-directory' instead.\n", aec.RedF)) } for i, file := range opts.EnvFiles { + file = paths.ExpandUser(file) if !filepath.IsAbs(file) { file, err := filepath.Abs(file) if err != nil { return err } opts.EnvFiles[i] = file + } else { + opts.EnvFiles[i] = file } } diff --git a/internal/paths/paths.go b/internal/paths/paths.go index 4e4c01b8cc4..e247f68b289 100644 --- a/internal/paths/paths.go +++ b/internal/paths/paths.go @@ -22,6 +22,30 @@ import ( "strings" ) +// ExpandUser expands a leading tilde (~) in a path to the user's home directory. +// If the path doesn't start with ~, it is returned unchanged. +// If the home directory cannot be determined, the original path is returned. +func ExpandUser(path string) string { + if path == "" { + return path + } + if path[0] != '~' { + return path + } + if len(path) > 1 && path[1] != '/' && path[1] != filepath.Separator { + // ~otheruser/... syntax is not supported + return path + } + home, err := os.UserHomeDir() + if err != nil { + return path + } + if len(path) == 1 { + return home + } + return filepath.Join(home, path[2:]) +} + func IsChild(dir string, file string) bool { if dir == "" { return false diff --git a/internal/paths/paths_test.go b/internal/paths/paths_test.go new file mode 100644 index 00000000000..8488ea8e21f --- /dev/null +++ b/internal/paths/paths_test.go @@ -0,0 +1,84 @@ +/* + Copyright 2020 Docker Compose CLI authors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package paths + +import ( + "os" + "path/filepath" + "testing" + + "gotest.tools/v3/assert" +) + +func TestExpandUser(t *testing.T) { + home, err := os.UserHomeDir() + assert.NilError(t, err) + + tests := []struct { + name string + input string + expected string + }{ + { + name: "empty string", + input: "", + expected: "", + }, + { + name: "tilde only", + input: "~", + expected: home, + }, + { + name: "tilde with slash", + input: "~/.env", + expected: filepath.Join(home, ".env"), + }, + { + name: "tilde with subdir", + input: "~/subdir/.env", + expected: filepath.Join(home, "subdir", ".env"), + }, + { + name: "absolute path unchanged", + input: "/absolute/path/.env", + expected: "/absolute/path/.env", + }, + { + name: "relative path unchanged", + input: "relative/path/.env", + expected: "relative/path/.env", + }, + { + name: "tilde in middle unchanged", + input: "/path/~/file", + expected: "/path/~/file", + }, + { + name: "tilde other user unchanged", + input: "~otheruser/.env", + expected: "~otheruser/.env", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := ExpandUser(tt.input) + assert.Equal(t, result, tt.expected) + }) + } +}