diff --git a/internal/templater/funcs.go b/internal/templater/funcs.go index 7bffae31a6..4fdd395d22 100644 --- a/internal/templater/funcs.go +++ b/internal/templater/funcs.go @@ -34,6 +34,7 @@ func init() { "IsSH": IsSH, // Deprecated "joinPath": filepath.Join, "relPath": filepath.Rel, + "resolvePath": resolvePath, "merge": merge, "spew": spew.Sdump, "fromYaml": fromYaml, @@ -89,6 +90,20 @@ func splitArgs(s string) ([]string, error) { return shell.Fields(s, nil) } +func resolvePath(path string) (string, error) { + absPath, err := filepath.Abs(path) + if err != nil { + return "", err + } + + resolvedPath, err := filepath.EvalSymlinks(absPath) + if err != nil { + return absPath, nil + } + + return resolvedPath, nil +} + // Deprecated: now always returns true func IsSH() bool { return true diff --git a/internal/templater/funcs_test.go b/internal/templater/funcs_test.go new file mode 100644 index 0000000000..aa5efd030d --- /dev/null +++ b/internal/templater/funcs_test.go @@ -0,0 +1,57 @@ +package templater + +import ( + "os" + "path/filepath" + "testing" +) + +func TestResolvePathCleansRelativeSegments(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + input := filepath.Join(tmpDir, "foo", "..", "bar") + + resolved, err := resolvePath(input) + if err != nil { + t.Fatalf("resolvePath returned error: %v", err) + } + + expected, err := filepath.Abs(filepath.Join(tmpDir, "bar")) + if err != nil { + t.Fatalf("filepath.Abs returned error: %v", err) + } + + if resolved != expected { + t.Fatalf("expected %q, got %q", expected, resolved) + } +} + +func TestResolvePathResolvesSymlink(t *testing.T) { + t.Parallel() + + tmpDir := t.TempDir() + targetDir := filepath.Join(tmpDir, "target") + if err := os.Mkdir(targetDir, 0o755); err != nil { + t.Fatalf("mkdir target: %v", err) + } + + link := filepath.Join(tmpDir, "link") + if err := os.Symlink(targetDir, link); err != nil { + t.Skipf("symlinks not supported in this environment: %v", err) + } + + resolved, err := resolvePath(filepath.Join(link, "..", "link")) + if err != nil { + t.Fatalf("resolvePath returned error: %v", err) + } + + expected, err := filepath.Abs(targetDir) + if err != nil { + t.Fatalf("filepath.Abs returned error: %v", err) + } + + if resolved != expected { + t.Fatalf("expected symlink to resolve to %q, got %q", expected, resolved) + } +} diff --git a/website/src/docs/reference/templating.md b/website/src/docs/reference/templating.md index c08fc00740..372d557980 100644 --- a/website/src/docs/reference/templating.md +++ b/website/src/docs/reference/templating.md @@ -617,6 +617,7 @@ tasks: - echo "{{.WIN_PATH | fromSlash}}" # Convert to OS-specific slashes - echo "{{joinPath .OUTPUT_DIR .BINARY_NAME}}" # Join path elements - echo "Relative {{relPath .ROOT_DIR .TASKFILE_DIR}}" # Get relative path + - echo "{{resolvePath "../dist/../build"}}" # Resolve to an absolute path (follows symlinks when available) ``` ### Data Structure Functions