diff --git a/init.go b/init.go index 2bddb210..0521af6b 100644 --- a/init.go +++ b/init.go @@ -4,7 +4,8 @@ import ( "fmt" "io" "os" - "path/filepath" + + "github.com/go-task/task/v3/internal/filepathext" ) const defaultTaskfile = `# https://taskfile.dev @@ -23,7 +24,7 @@ tasks: // InitTaskfile Taskfile creates a new Taskfile func InitTaskfile(w io.Writer, dir string) error { - f := filepath.Join(dir, "Taskfile.yaml") + f := filepathext.SmartJoin(dir, "Taskfile.yaml") if _, err := os.Stat(f); err == nil { return ErrTaskfileAlreadyExists diff --git a/internal/compiler/v3/compiler_v3.go b/internal/compiler/v3/compiler_v3.go index 0d129b51..a1be19e1 100644 --- a/internal/compiler/v3/compiler_v3.go +++ b/internal/compiler/v3/compiler_v3.go @@ -4,12 +4,12 @@ import ( "bytes" "context" "fmt" - "path/filepath" "strings" "sync" "github.com/go-task/task/v3/internal/compiler" "github.com/go-task/task/v3/internal/execext" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" @@ -83,9 +83,7 @@ func (c *CompilerV3) getVariables(t *taskfile.Task, call *taskfile.Call, evaluat if err := tr.Err(); err != nil { return nil, err } - if !filepath.IsAbs(dir) { - dir = filepath.Join(c.Dir, dir) - } + dir = filepathext.SmartJoin(c.Dir, dir) taskRangeFunc = getRangeFunc(dir) } diff --git a/internal/filepathext/filepathext.go b/internal/filepathext/filepathext.go new file mode 100644 index 00000000..6146d88e --- /dev/null +++ b/internal/filepathext/filepathext.go @@ -0,0 +1,14 @@ +package filepathext + +import ( + "path/filepath" +) + +// SmartJoin joins two paths, but only if the second is not already an +// absolute path. +func SmartJoin(a, b string) string { + if filepath.IsAbs(b) { + return b + } + return filepath.Join(a, b) +} diff --git a/internal/status/checksum.go b/internal/status/checksum.go index fc59abbe..17163a99 100644 --- a/internal/status/checksum.go +++ b/internal/status/checksum.go @@ -8,6 +8,8 @@ import ( "path/filepath" "regexp" "strings" + + "github.com/go-task/task/v3/internal/filepathext" ) // Checksum validades if a task is up to date by calculating its source @@ -43,7 +45,7 @@ func (c *Checksum) IsUpToDate() (bool, error) { } if !c.Dry { - _ = os.MkdirAll(filepath.Join(c.TempDir, "checksum"), 0755) + _ = os.MkdirAll(filepathext.SmartJoin(c.TempDir, "checksum"), 0755) if err = os.WriteFile(checksumFile, []byte(newMd5+"\n"), 0644); err != nil { return false, err } diff --git a/internal/status/glob.go b/internal/status/glob.go index 6ecaa749..7d1ca1f0 100644 --- a/internal/status/glob.go +++ b/internal/status/glob.go @@ -2,12 +2,12 @@ package status import ( "os" - "path/filepath" "sort" "github.com/mattn/go-zglob" "github.com/go-task/task/v3/internal/execext" + "github.com/go-task/task/v3/internal/filepathext" ) func globs(dir string, globs []string) ([]string, error) { @@ -25,17 +25,18 @@ func globs(dir string, globs []string) ([]string, error) { func Glob(dir string, g string) ([]string, error) { files := make([]string, 0) - if !filepath.IsAbs(g) { - g = filepath.Join(dir, g) - } + g = filepathext.SmartJoin(dir, g) + g, err := execext.Expand(g) if err != nil { return nil, err } + fs, err := zglob.Glob(g) if err != nil { return nil, err } + for _, f := range fs { info, err := os.Stat(f) if err != nil { diff --git a/setup.go b/setup.go index a7f6d509..1b2e141a 100644 --- a/setup.go +++ b/setup.go @@ -12,6 +12,7 @@ import ( compilerv2 "github.com/go-task/task/v3/internal/compiler/v2" compilerv3 "github.com/go-task/task/v3/internal/compiler/v3" "github.com/go-task/task/v3/internal/execext" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/logger" "github.com/go-task/task/v3/internal/output" "github.com/go-task/task/v3/taskfile" @@ -69,7 +70,7 @@ func (e *Executor) setupTempDir() error { } if os.Getenv("TASK_TEMP_DIR") == "" { - e.TempDir = filepath.Join(e.Dir, ".task") + e.TempDir = filepathext.SmartJoin(e.Dir, ".task") } else if filepath.IsAbs(os.Getenv("TASK_TEMP_DIR")) || strings.HasPrefix(os.Getenv("TASK_TEMP_DIR"), "~") { tempDir, err := execext.Expand(os.Getenv("TASK_TEMP_DIR")) if err != nil { @@ -77,9 +78,9 @@ func (e *Executor) setupTempDir() error { } projectDir, _ := filepath.Abs(e.Dir) projectName := filepath.Base(projectDir) - e.TempDir = filepath.Join(tempDir, projectName) + e.TempDir = filepathext.SmartJoin(tempDir, projectName) } else { - e.TempDir = filepath.Join(e.Dir, os.Getenv("TASK_TEMP_DIR")) + e.TempDir = filepathext.SmartJoin(e.Dir, os.Getenv("TASK_TEMP_DIR")) } return nil diff --git a/task_test.go b/task_test.go index 137544ad..216e3968 100644 --- a/task_test.go +++ b/task_test.go @@ -14,6 +14,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/go-task/task/v3" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile" ) @@ -37,12 +38,12 @@ func (fct fileContentTest) name(file string) string { func (fct fileContentTest) Run(t *testing.T) { for f := range fct.Files { - _ = os.Remove(filepath.Join(fct.Dir, f)) + _ = os.Remove(filepathext.SmartJoin(fct.Dir, f)) } e := &task.Executor{ Dir: fct.Dir, - TempDir: filepath.Join(fct.Dir, ".task"), + TempDir: filepathext.SmartJoin(fct.Dir, ".task"), Entrypoint: fct.Entrypoint, Stdout: io.Discard, Stderr: io.Discard, @@ -52,7 +53,7 @@ func (fct fileContentTest) Run(t *testing.T) { for name, expectContent := range fct.Files { t.Run(fct.name(name), func(t *testing.T) { - path := filepath.Join(fct.Dir, name) + path := filepathext.SmartJoin(fct.Dir, name) b, err := os.ReadFile(path) assert.NoError(t, err, "Error reading file") s := string(b) @@ -235,7 +236,7 @@ func TestDeps(t *testing.T) { } for _, f := range files { - _ = os.Remove(filepath.Join(dir, f)) + _ = os.Remove(filepathext.SmartJoin(dir, f)) } e := &task.Executor{ @@ -247,7 +248,7 @@ func TestDeps(t *testing.T) { assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "default"})) for _, f := range files { - f = filepath.Join(dir, f) + f = filepathext.SmartJoin(dir, f) if _, err := os.Stat(f); err != nil { t.Errorf("File %s should exist", f) } @@ -263,7 +264,7 @@ func TestStatus(t *testing.T) { } for _, f := range files { - path := filepath.Join(dir, f) + path := filepathext.SmartJoin(dir, f) _ = os.Remove(path) if _, err := os.Stat(path); err == nil { t.Errorf("File should not exist: %v", err) @@ -273,7 +274,7 @@ func TestStatus(t *testing.T) { var buff bytes.Buffer e := &task.Executor{ Dir: dir, - TempDir: filepath.Join(dir, ".task"), + TempDir: filepathext.SmartJoin(dir, ".task"), Stdout: &buff, Stderr: &buff, Silent: true, @@ -283,7 +284,7 @@ func TestStatus(t *testing.T) { assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "gen-bar"})) for _, f := range files { - if _, err := os.Stat(filepath.Join(dir, f)); err != nil { + if _, err := os.Stat(filepathext.SmartJoin(dir, f)); err != nil { t.Errorf("File should exist: %v", err) } } @@ -360,10 +361,10 @@ func TestGenerates(t *testing.T) { fileWithSpaces = "my text file.txt" ) - var srcFile = filepath.Join(dir, srcTask) + var srcFile = filepathext.SmartJoin(dir, srcTask) for _, task := range []string{srcTask, relTask, absTask, fileWithSpaces} { - path := filepath.Join(dir, task) + path := filepathext.SmartJoin(dir, task) _ = os.Remove(path) if _, err := os.Stat(path); err == nil { t.Errorf("File should not exist: %v", err) @@ -379,7 +380,7 @@ func TestGenerates(t *testing.T) { assert.NoError(t, e.Setup()) for _, theTask := range []string{relTask, absTask, fileWithSpaces} { - var destFile = filepath.Join(dir, theTask) + var destFile = filepathext.SmartJoin(dir, theTask) var upToDate = fmt.Sprintf("task: Task \"%s\" is up to date\n", srcTask) + fmt.Sprintf("task: Task \"%s\" is up to date\n", theTask) @@ -416,16 +417,16 @@ func TestStatusChecksum(t *testing.T) { } for _, f := range files { - _ = os.Remove(filepath.Join(dir, f)) + _ = os.Remove(filepathext.SmartJoin(dir, f)) - _, err := os.Stat(filepath.Join(dir, f)) + _, err := os.Stat(filepathext.SmartJoin(dir, f)) assert.Error(t, err) } var buff bytes.Buffer e := task.Executor{ Dir: dir, - TempDir: filepath.Join(dir, ".task"), + TempDir: filepathext.SmartJoin(dir, ".task"), Stdout: &buff, Stderr: &buff, } @@ -433,7 +434,7 @@ func TestStatusChecksum(t *testing.T) { assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "build"})) for _, f := range files { - _, err := os.Stat(filepath.Join(dir, f)) + _, err := os.Stat(filepathext.SmartJoin(dir, f)) assert.NoError(t, err) } @@ -577,13 +578,13 @@ func TestListCanListDescOnly(t *testing.T) { func TestStatusVariables(t *testing.T) { const dir = "testdata/status_vars" - _ = os.RemoveAll(filepath.Join(dir, ".task")) - _ = os.Remove(filepath.Join(dir, "generated.txt")) + _ = os.RemoveAll(filepathext.SmartJoin(dir, ".task")) + _ = os.Remove(filepathext.SmartJoin(dir, "generated.txt")) var buff bytes.Buffer e := task.Executor{ Dir: dir, - TempDir: filepath.Join(dir, ".task"), + TempDir: filepathext.SmartJoin(dir, ".task"), Stdout: &buff, Stderr: &buff, Silent: false, @@ -594,7 +595,7 @@ func TestStatusVariables(t *testing.T) { assert.Contains(t, buff.String(), "d41d8cd98f00b204e9800998ecf8427e") - inf, err := os.Stat(filepath.Join(dir, "source.txt")) + inf, err := os.Stat(filepathext.SmartJoin(dir, "source.txt")) assert.NoError(t, err) ts := fmt.Sprintf("%d", inf.ModTime().Unix()) tf := inf.ModTime().String() @@ -605,7 +606,7 @@ func TestStatusVariables(t *testing.T) { func TestInit(t *testing.T) { const dir = "testdata/init" - var file = filepath.Join(dir, "Taskfile.yaml") + var file = filepathext.SmartJoin(dir, "Taskfile.yaml") _ = os.Remove(file) if _, err := os.Stat(file); err == nil { @@ -694,7 +695,7 @@ func TestExpand(t *testing.T) { func TestDry(t *testing.T) { const dir = "testdata/dry" - file := filepath.Join(dir, "file.txt") + file := filepathext.SmartJoin(dir, "file.txt") _ = os.Remove(file) var buff bytes.Buffer @@ -719,12 +720,12 @@ func TestDry(t *testing.T) { func TestDryChecksum(t *testing.T) { const dir = "testdata/dry_checksum" - checksumFile := filepath.Join(dir, ".task/checksum/default") + checksumFile := filepathext.SmartJoin(dir, ".task/checksum/default") _ = os.Remove(checksumFile) e := task.Executor{ Dir: dir, - TempDir: filepath.Join(dir, ".task"), + TempDir: filepathext.SmartJoin(dir, ".task"), Stdout: io.Discard, Stderr: io.Discard, Dry: true, @@ -964,7 +965,7 @@ func TestSummary(t *testing.T) { assert.NoError(t, e.Setup()) assert.NoError(t, e.Run(context.Background(), taskfile.Call{Task: "task-with-summary"}, taskfile.Call{Task: "other-task-with-summary"})) - data, err := os.ReadFile(filepath.Join(dir, "task-with-summary.txt")) + data, err := os.ReadFile(filepathext.SmartJoin(dir, "task-with-summary.txt")) assert.NoError(t, err) expectedOutput := string(data) diff --git a/taskfile/included_taskfile.go b/taskfile/included_taskfile.go index 2a4c8d53..ec4ade08 100644 --- a/taskfile/included_taskfile.go +++ b/taskfile/included_taskfile.go @@ -6,6 +6,7 @@ import ( "path/filepath" "github.com/go-task/task/v3/internal/execext" + "github.com/go-task/task/v3/internal/filepathext" "gopkg.in/yaml.v3" ) @@ -133,7 +134,7 @@ func (it *IncludedTaskfile) resolvePath(path string) (string, error) { return path, nil } - result, err := filepath.Abs(filepath.Join(it.BaseDir, path)) + result, err := filepath.Abs(filepathext.SmartJoin(it.BaseDir, path)) if err != nil { return "", fmt.Errorf("task: error resolving path %s relative to %s: %w", path, it.BaseDir, err) } diff --git a/taskfile/read/dotenv.go b/taskfile/read/dotenv.go index 0ce6fed4..70b9dc1d 100644 --- a/taskfile/read/dotenv.go +++ b/taskfile/read/dotenv.go @@ -2,11 +2,11 @@ package read import ( "os" - "path/filepath" "github.com/joho/godotenv" "github.com/go-task/task/v3/internal/compiler" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" ) @@ -27,10 +27,8 @@ func Dotenv(c compiler.Compiler, tf *taskfile.Taskfile, dir string) (*taskfile.V for _, dotEnvPath := range tf.Dotenv { dotEnvPath = tr.Replace(dotEnvPath) + dotEnvPath = filepathext.SmartJoin(dir, dotEnvPath) - if !filepath.IsAbs(dotEnvPath) { - dotEnvPath = filepath.Join(dir, dotEnvPath) - } if _, err := os.Stat(dotEnvPath); os.IsNotExist(err) { continue } diff --git a/taskfile/read/taskfile.go b/taskfile/read/taskfile.go index 95348ee8..285df4b7 100644 --- a/taskfile/read/taskfile.go +++ b/taskfile/read/taskfile.go @@ -9,6 +9,7 @@ import ( "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" ) @@ -44,7 +45,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { readerNode.Dir = d } - path, err := exists(filepath.Join(readerNode.Dir, readerNode.Entrypoint)) + path, err := exists(filepathext.SmartJoin(readerNode.Dir, readerNode.Entrypoint)) if err != nil { return nil, err } @@ -140,10 +141,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { } for _, task := range includedTaskfile.Tasks { - if !filepath.IsAbs(task.Dir) { - task.Dir = filepath.Join(dir, task.Dir) - } - + task.Dir = filepathext.SmartJoin(dir, task.Dir) task.IncludeVars = includedTask.Vars task.IncludedTaskfileVars = includedTaskfile.Vars } @@ -159,7 +157,7 @@ func Taskfile(readerNode *ReaderNode) (*taskfile.Taskfile, error) { } if v < 3.0 { - path = filepath.Join(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS)) + path = filepathext.SmartJoin(readerNode.Dir, fmt.Sprintf("Taskfile_%s.yml", runtime.GOOS)) if _, err = os.Stat(path); err == nil { osTaskfile, err := readTaskfile(path) if err != nil { @@ -191,29 +189,19 @@ func readTaskfile(file string) (*taskfile.Taskfile, error) { return &t, yaml.NewDecoder(f).Decode(&t) } -// exists finds a Taskfile at the stated location, returning a fully qualified path to the file func exists(path string) (string, error) { fi, err := os.Stat(path) if err != nil { return "", err } if fi.Mode().IsRegular() { - // File exists, return a fully qualified path - result, err := filepath.Abs(path) - if err != nil { - return "", err - } - return result, nil + return path, nil } for _, n := range defaultTaskfiles { - fpath := filepath.Join(path, n) + fpath := filepathext.SmartJoin(path, n) if _, err := os.Stat(fpath); err == nil { - result, err := filepath.Abs(fpath) - if err != nil { - return "", err - } - return result, nil + return fpath, nil } } @@ -228,14 +216,14 @@ func checkCircularIncludes(node *ReaderNode) error { return errors.New("task: failed to check for include cycle: node.Parent was nil") } var curNode = node - var basePath = filepath.Join(node.Dir, node.Entrypoint) + var basePath = filepathext.SmartJoin(node.Dir, node.Entrypoint) for curNode.Parent != nil { curNode = curNode.Parent - curPath := filepath.Join(curNode.Dir, curNode.Entrypoint) + curPath := filepathext.SmartJoin(curNode.Dir, curNode.Entrypoint) if curPath == basePath { return fmt.Errorf("task: include cycle detected between %s <--> %s", curPath, - filepath.Join(node.Parent.Dir, node.Parent.Entrypoint), + filepathext.SmartJoin(node.Parent.Dir, node.Parent.Entrypoint), ) } } diff --git a/taskfile/read/taskvars.go b/taskfile/read/taskvars.go index 84285327..f920384e 100644 --- a/taskfile/read/taskvars.go +++ b/taskfile/read/taskvars.go @@ -3,11 +3,11 @@ package read import ( "fmt" "os" - "path/filepath" "runtime" "gopkg.in/yaml.v3" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/taskfile" ) @@ -15,7 +15,7 @@ import ( func Taskvars(dir string) (*taskfile.Vars, error) { vars := &taskfile.Vars{} - path := filepath.Join(dir, "Taskvars.yml") + path := filepathext.SmartJoin(dir, "Taskvars.yml") if _, err := os.Stat(path); err == nil { vars, err = readTaskvars(path) if err != nil { @@ -23,7 +23,7 @@ func Taskvars(dir string) (*taskfile.Vars, error) { } } - path = filepath.Join(dir, fmt.Sprintf("Taskvars_%s.yml", runtime.GOOS)) + path = filepathext.SmartJoin(dir, fmt.Sprintf("Taskvars_%s.yml", runtime.GOOS)) if _, err := os.Stat(path); err == nil { osVars, err := readTaskvars(path) if err != nil { diff --git a/variables.go b/variables.go index b9faa720..80a232a3 100644 --- a/variables.go +++ b/variables.go @@ -1,10 +1,10 @@ package task import ( - "path/filepath" "strings" "github.com/go-task/task/v3/internal/execext" + "github.com/go-task/task/v3/internal/filepathext" "github.com/go-task/task/v3/internal/status" "github.com/go-task/task/v3/internal/templater" "github.com/go-task/task/v3/taskfile" @@ -68,8 +68,8 @@ func (e *Executor) compiledTask(call taskfile.Call, evaluateShVars bool) (*taskf if err != nil { return nil, err } - if e.Dir != "" && !filepath.IsAbs(new.Dir) { - new.Dir = filepath.Join(e.Dir, new.Dir) + if e.Dir != "" { + new.Dir = filepathext.SmartJoin(e.Dir, new.Dir) } if new.Prefix == "" { new.Prefix = new.Task