diff --git a/internal/filepathext/filepathext.go b/internal/filepathext/filepathext.go index f2a1ba15a0..615969bf22 100644 --- a/internal/filepathext/filepathext.go +++ b/internal/filepathext/filepathext.go @@ -3,7 +3,7 @@ package filepathext import ( "os" "path/filepath" - "strings" + "regexp" ) // SmartJoin joins two paths, but only if the second is not already an @@ -25,19 +25,12 @@ func IsAbs(path string) bool { return filepath.IsAbs(path) } -var knownAbsDirs = []string{ - ".ROOT_DIR", - ".TASKFILE_DIR", - ".USER_WORKING_DIR", -} +// Heuristically match special directory names within template actions, without +// matching prefixes of longer identifiers. This does not parse template syntax. +var specialDirRE = regexp.MustCompile(`\{\{[^{}]*\.(?:ROOT_DIR|TASKFILE_DIR|USER_WORKING_DIR)(?:[^\p{L}\p{Nd}_{}][^{}]*)?\}\}`) func isSpecialDir(dir string) bool { - for _, d := range knownAbsDirs { - if strings.Contains(dir, d) { - return true - } - } - return false + return specialDirRE.MatchString(dir) } // TryAbsToRel tries to convert an absolute path to relative based on the diff --git a/internal/filepathext/filepathext_test.go b/internal/filepathext/filepathext_test.go new file mode 100644 index 0000000000..cf4f014a80 --- /dev/null +++ b/internal/filepathext/filepathext_test.go @@ -0,0 +1,83 @@ +package filepathext + +import ( + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestSmartJoinRelativePathContainingSpecialVariableName(t *testing.T) { + t.Parallel() + + base := t.TempDir() + for _, variable := range []string{".ROOT_DIR", ".TASKFILE_DIR", ".USER_WORKING_DIR"} { + t.Run(variable, func(t *testing.T) { + t.Parallel() + for _, relative := range []string{ + filepath.Join("project"+variable, "file.txt"), + filepath.Join(variable, "file.txt"), + filepath.Join("{{.PROJECT}}"+variable, "file.txt"), + filepath.Join("{{.PROJECT}}"+variable+"{{.SUFFIX}}", "file.txt"), + filepath.Join("{{ "+variable, "file.txt"), + } { + require.False(t, IsAbs(relative), relative) + require.Equal(t, filepath.Join(base, relative), SmartJoin(base, relative)) + } + }) + } +} + +func TestSmartJoinAbsoluteAndTemplatePaths(t *testing.T) { + t.Parallel() + + base := t.TempDir() + absolute := filepath.Join(t.TempDir(), "file.txt") + for _, path := range []string{ + absolute, + "{{.ROOT_DIR}}/file.txt", + "{{ .TASKFILE_DIR }}/file.txt", + "{{- .USER_WORKING_DIR -}}/file.txt", + "{{.ROOT_DIR | toSlash}}/file.txt", + "{{.ROOT_DIR|toSlash}}/file.txt", + "{{(.ROOT_DIR)}}/file.txt", + "{{.PROJECT}}/{{.ROOT_DIR}}/file.txt", + "{{\n.TASKFILE_DIR\n}}/file.txt", + `{{joinPath .ROOT_DIR "src"}}/file.txt`, + } { + require.True(t, IsAbs(path), path) + require.Equal(t, path, SmartJoin(base, path)) + } +} + +func TestSmartJoinUnrelatedTemplatePaths(t *testing.T) { + t.Parallel() + + base := t.TempDir() + for _, path := range []string{ + "{{.PROJECT}}/file.txt", + "{{XROOT_DIR}}/file.txt", + "{{XTASKFILE_DIR}}/file.txt", + "{{XUSER_WORKING_DIR}}/file.txt", + } { + require.False(t, IsAbs(path), path) + require.Equal(t, filepath.Join(base, path), SmartJoin(base, path)) + } +} + +func TestSmartJoinTemplateVariableNameSuffixes(t *testing.T) { + t.Parallel() + + base := t.TempDir() + for _, variable := range []string{".ROOT_DIR", ".TASKFILE_DIR", ".USER_WORKING_DIR"} { + for _, suffix := range []string{"_SUFFIX", "_EXTRA", "2", "suffix", "变量"} { + t.Run(variable+suffix, func(t *testing.T) { + t.Parallel() + + path := "{{" + variable + suffix + "}}/file.txt" + require.False(t, IsAbs(path), path) + require.Equal(t, filepath.Join(base, path), SmartJoin(base, path)) + }) + } + } +} diff --git a/taskfile/node_file_test.go b/taskfile/node_file_test.go new file mode 100644 index 0000000000..8f8552d550 --- /dev/null +++ b/taskfile/node_file_test.go @@ -0,0 +1,33 @@ +package taskfile + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFileNodeResolveLiteralSpecialDir(t *testing.T) { + t.Parallel() + + dir := t.TempDir() + entrypoint := filepath.Join(dir, "Taskfile.yml") + require.NoError(t, os.WriteFile(entrypoint, []byte("version: '3'\n"), 0o600)) + node, err := NewFileNode(entrypoint, "") + require.NoError(t, err) + + for _, name := range []string{"project.ROOT_DIR", "project.TASKFILE_DIR", "project.USER_WORKING_DIR"} { + t.Run(name, func(t *testing.T) { + t.Parallel() + + resolvedDir, err := node.ResolveDir(name) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, name), resolvedDir) + + resolvedEntrypoint, err := node.ResolveEntrypoint(filepath.Join(name, "Taskfile.yml")) + require.NoError(t, err) + require.Equal(t, filepath.Join(dir, name, "Taskfile.yml"), resolvedEntrypoint) + }) + } +}