diff --git a/cmd/xc/interactive.go b/cmd/xc/interactive.go index 2993f6e..2dabac6 100644 --- a/cmd/xc/interactive.go +++ b/cmd/xc/interactive.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "io" + "os" "strings" "github.com/charmbracelet/bubbles/list" @@ -129,7 +130,11 @@ func interactivePicker(ctx context.Context, tasks []models.Task, dir string) err if task == nil { return nil } - runner, err := run.NewRunner(tasks, dir) + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting current directory: %w", err) + } + runner, err := run.NewRunner(tasks, dir, cwd) if err != nil { return fmt.Errorf("xc parse error: %w", err) } diff --git a/cmd/xc/main.go b/cmd/xc/main.go index 2c4f6ba..0c228e2 100644 --- a/cmd/xc/main.go +++ b/cmd/xc/main.go @@ -254,7 +254,11 @@ func runMain() error { return nil } // xc task1 - runner, err := run.NewRunner(tasks, dir) + cwd, err := os.Getwd() + if err != nil { + return fmt.Errorf("error getting current directory: %w", err) + } + runner, err := run.NewRunner(tasks, dir, cwd) if err != nil { return fmt.Errorf("xc parse error: %w", err) } diff --git a/doc/content/task-syntax/directory.md b/doc/content/task-syntax/directory.md index 1b19b9a..189b598 100644 --- a/doc/content/task-syntax/directory.md +++ b/doc/content/task-syntax/directory.md @@ -21,3 +21,16 @@ directory: ./src sh build.sh ``` ```` + +## Using the caller's working directory + +By default, tasks run in the directory where the README file is located, even if `xc` is invoked from a subdirectory. Setting `directory` to `$PWD` will instead run the task in the directory where `xc` was invoked. + +````markdown +## Tasks +### List +directory: $PWD +``` +ls +``` +```` diff --git a/run/run.go b/run/run.go index 81ea34b..17171a9 100644 --- a/run/run.go +++ b/run/run.go @@ -25,6 +25,7 @@ type Runner struct { scriptRunner ScriptRunner tasks models.Tasks dir string + cwd string alreadyRan map[string]bool alreadyRanMu sync.Mutex trace bool @@ -38,7 +39,7 @@ type Runner struct { // // NewRunner will return an error in the case that Dependent tasks are cyclical, // invalid or at a larger depth than 50. -func NewRunner(ts models.Tasks, dir string) (runner Runner, err error) { +func NewRunner(ts models.Tasks, dir, cwd string) (runner Runner, err error) { trace := !slices.Contains( []string{"false", "no", "0"}, strings.ToLower(os.Getenv("XC_TRACE"))) @@ -46,6 +47,7 @@ func NewRunner(ts models.Tasks, dir string) (runner Runner, err error) { scriptRunner: newInterpreter(), tasks: ts, dir: dir, + cwd: cwd, alreadyRan: map[string]bool{}, trace: trace, } @@ -218,6 +220,9 @@ func (r *Runner) getExecutionPath(task models.Task) string { if task.Dir == "" { return r.dir } + if task.Dir == "$PWD" { + return r.cwd + } if filepath.IsAbs(task.Dir) { return task.Dir } diff --git a/run/run_test.go b/run/run_test.go index 702e698..55f67bf 100644 --- a/run/run_test.go +++ b/run/run_test.go @@ -182,7 +182,7 @@ func TestRunAsync(t *testing.T) { for i := range tt.tasks { tt.tasks[i].DepsBehaviour = models.DependencyBehaviourAsync } - runner, err := NewRunner(tt.tasks, "") + runner, err := NewRunner(tt.tasks, "", "") if (err != nil) != tt.expectedParseError { t.Fatalf("expected error %v, got %v", tt.expectedParseError, err) } @@ -206,7 +206,7 @@ func TestRun(t *testing.T) { for _, tt := range testCases() { tt := tt t.Run(tt.name, func(t *testing.T) { - runner, err := NewRunner(tt.tasks, "") + runner, err := NewRunner(tt.tasks, "", "") if (err != nil) != tt.expectedParseError { t.Fatalf("expected error %v, got %v", tt.expectedParseError, err) } @@ -234,7 +234,7 @@ func TestRunWithInputs(t *testing.T) { Script: "somecmd", Inputs: []string{"FOO"}, }, - }, "") + }, "", "") if err != nil { t.Fatal(err) } @@ -250,7 +250,7 @@ func TestRunWithInputs(t *testing.T) { Script: "somecmd", Inputs: []string{"FOO"}, }, - }, "") + }, "", "") if err != nil { t.Fatal(err) } @@ -271,7 +271,7 @@ func TestRunWithInputs(t *testing.T) { Script: "somecmd", Inputs: []string{"FOO"}, }, - }, "") + }, "", "") if err != nil { t.Fatal(err) } @@ -317,7 +317,7 @@ func TestOptionalInputPrecedence(t *testing.T) { return val, found } t.Run("env default is used when no cli arg or os env is set", func(t *testing.T) { - runner, err := NewRunner(makeTask(), "") + runner, err := NewRunner(makeTask(), "", "") if err != nil { t.Fatal(err) } @@ -340,7 +340,7 @@ func TestOptionalInputPrecedence(t *testing.T) { }) t.Run("cli arg overrides env default", func(t *testing.T) { - runner, err := NewRunner(makeTask(), "") + runner, err := NewRunner(makeTask(), "", "") if err != nil { t.Fatal(err) } @@ -364,7 +364,7 @@ func TestOptionalInputPrecedence(t *testing.T) { t.Run("os env overrides env default when input is declared", func(t *testing.T) { t.Setenv("MY_VAR", "from_shell") - runner, err := NewRunner(makeTask(), "") + runner, err := NewRunner(makeTask(), "", "") if err != nil { t.Fatal(err) } @@ -388,7 +388,7 @@ func TestOptionalInputPrecedence(t *testing.T) { t.Run("cli arg overrides os env", func(t *testing.T) { t.Setenv("MY_VAR", "from_shell") - runner, err := NewRunner(makeTask(), "") + runner, err := NewRunner(makeTask(), "", "") if err != nil { t.Fatal(err) } @@ -420,7 +420,7 @@ func TestOptionalInputPrecedence(t *testing.T) { Script: "somecmd", Env: []string{"MY_VAR=forced_value"}, }, - }, "") + }, "", "") if err != nil { t.Fatal(err) } @@ -442,3 +442,54 @@ func TestOptionalInputPrecedence(t *testing.T) { } }) } + +func TestGetExecutionPath(t *testing.T) { + tests := []struct { + name string + dir string + cwd string + taskDir string + expected string + }{ + { + name: "empty dir uses runner dir", + dir: "/repo", + cwd: "/repo/subdir", + taskDir: "", + expected: "/repo", + }, + { + name: "relative dir is joined with runner dir", + dir: "/repo", + cwd: "/repo/subdir", + taskDir: "./src", + expected: "/repo/src", + }, + { + name: "absolute dir is used as-is", + dir: "/repo", + cwd: "/repo/subdir", + taskDir: "/tmp/build", + expected: "/tmp/build", + }, + { + name: "$PWD uses caller working directory", + dir: "/repo", + cwd: "/repo/subdir", + taskDir: "$PWD", + expected: "/repo/subdir", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + r := Runner{ + dir: tt.dir, + cwd: tt.cwd, + } + got := r.getExecutionPath(models.Task{Dir: tt.taskDir}) + if got != tt.expected { + t.Fatalf("expected %q, got %q", tt.expected, got) + } + }) + } +}