mirror of
https://github.com/go-task/task.git
synced 2025-08-08 04:22:08 +03:00
feat: recursive config search (#2166)
* refactor: experiments flags * refactor: args.Parse * feat: recursive search for taskrc files * feat: consolidate some code into new fsext package * feat: add tests for search and default dir * fix: linting issues
This commit is contained in:
4
.taskrc.yml
Normal file
4
.taskrc.yml
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
experiments:
|
||||||
|
GENTLE_FORCE: 0
|
||||||
|
REMOTE_TASKFILES: 0
|
||||||
|
ENV_PRECEDENCE: 0
|
26
args/args.go
26
args/args.go
@@ -3,10 +3,36 @@ package args
|
|||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/pflag"
|
||||||
|
"mvdan.cc/sh/v3/syntax"
|
||||||
|
|
||||||
"github.com/go-task/task/v3"
|
"github.com/go-task/task/v3"
|
||||||
"github.com/go-task/task/v3/taskfile/ast"
|
"github.com/go-task/task/v3/taskfile/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// Get fetches the remaining arguments after CLI parsing and splits them into
|
||||||
|
// two groups: the arguments before the double dash (--) and the arguments after
|
||||||
|
// the double dash.
|
||||||
|
func Get() ([]string, []string, error) {
|
||||||
|
args := pflag.Args()
|
||||||
|
doubleDashPos := pflag.CommandLine.ArgsLenAtDash()
|
||||||
|
|
||||||
|
if doubleDashPos == -1 {
|
||||||
|
return args, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var quotedCliArgs []string
|
||||||
|
for _, arg := range args[doubleDashPos:] {
|
||||||
|
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return args[:doubleDashPos], quotedCliArgs, nil
|
||||||
|
}
|
||||||
|
|
||||||
// Parse parses command line argument: tasks and global variables
|
// Parse parses command line argument: tasks and global variables
|
||||||
func Parse(args ...string) ([]*task.Call, *ast.Vars) {
|
func Parse(args ...string) ([]*task.Call, *ast.Vars) {
|
||||||
calls := []*task.Call{}
|
calls := []*task.Call{}
|
||||||
|
@@ -5,10 +5,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/pflag"
|
"github.com/spf13/pflag"
|
||||||
"mvdan.cc/sh/v3/syntax"
|
|
||||||
|
|
||||||
"github.com/go-task/task/v3"
|
"github.com/go-task/task/v3"
|
||||||
"github.com/go-task/task/v3/args"
|
"github.com/go-task/task/v3/args"
|
||||||
@@ -78,7 +76,7 @@ func run() error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
args, _, err := getArgs()
|
_, args, err := args.Get()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -145,17 +143,12 @@ func run() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
// Parse the remaining arguments
|
||||||
calls []*task.Call
|
argv, cliArgs, err := args.Get()
|
||||||
globals *ast.Vars
|
|
||||||
)
|
|
||||||
|
|
||||||
tasksAndVars, cliArgs, err := getArgs()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
calls, globals := args.Parse(argv...)
|
||||||
calls, globals = args.Parse(tasksAndVars...)
|
|
||||||
|
|
||||||
// If there are no calls, run the default task instead
|
// If there are no calls, run the default task instead
|
||||||
if len(calls) == 0 {
|
if len(calls) == 0 {
|
||||||
@@ -181,24 +174,3 @@ func run() error {
|
|||||||
|
|
||||||
return e.Run(ctx, calls...)
|
return e.Run(ctx, calls...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getArgs() ([]string, string, error) {
|
|
||||||
var (
|
|
||||||
args = pflag.Args()
|
|
||||||
doubleDashPos = pflag.CommandLine.ArgsLenAtDash()
|
|
||||||
)
|
|
||||||
|
|
||||||
if doubleDashPos == -1 {
|
|
||||||
return args, "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var quotedCliArgs []string
|
|
||||||
for _, arg := range args[doubleDashPos:] {
|
|
||||||
quotedCliArg, err := syntax.Quote(arg, syntax.LangBash)
|
|
||||||
if err != nil {
|
|
||||||
return nil, "", err
|
|
||||||
}
|
|
||||||
quotedCliArgs = append(quotedCliArgs, quotedCliArg)
|
|
||||||
}
|
|
||||||
return args[:doubleDashPos], strings.Join(quotedCliArgs, " "), nil
|
|
||||||
}
|
|
||||||
|
@@ -8,6 +8,11 @@ const (
|
|||||||
CodeUnknown // Used when no other exit code is appropriate
|
CodeUnknown // Used when no other exit code is appropriate
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TaskRC related exit codes
|
||||||
|
const (
|
||||||
|
CodeTaskRCNotFoundError int = iota + 50
|
||||||
|
)
|
||||||
|
|
||||||
// Taskfile related exit codes
|
// Taskfile related exit codes
|
||||||
const (
|
const (
|
||||||
CodeTaskfileNotFound int = iota + 100
|
CodeTaskfileNotFound int = iota + 100
|
||||||
|
20
errors/errors_taskrc.go
Normal file
20
errors/errors_taskrc.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package errors
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type TaskRCNotFoundError struct {
|
||||||
|
URI string
|
||||||
|
Walk bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err TaskRCNotFoundError) Error() string {
|
||||||
|
var walkText string
|
||||||
|
if err.Walk {
|
||||||
|
walkText = " (or any of the parent directories)"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf(`task: No Task config file found at %q%s`, err.URI, walkText)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (err TaskRCNotFoundError) Code() int {
|
||||||
|
return CodeTaskRCNotFoundError
|
||||||
|
}
|
@@ -4,6 +4,8 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/taskrc/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Experiment struct {
|
type Experiment struct {
|
||||||
@@ -14,8 +16,11 @@ type Experiment struct {
|
|||||||
|
|
||||||
// New creates a new experiment with the given name and sets the values that can
|
// New creates a new experiment with the given name and sets the values that can
|
||||||
// enable it.
|
// enable it.
|
||||||
func New(xName string, allowedValues ...int) Experiment {
|
func New(xName string, config *ast.TaskRC, allowedValues ...int) Experiment {
|
||||||
value := experimentConfig.Experiments[xName]
|
var value int
|
||||||
|
if config != nil {
|
||||||
|
value = config.Experiments[xName]
|
||||||
|
}
|
||||||
|
|
||||||
if value == 0 {
|
if value == 0 {
|
||||||
value, _ = strconv.Atoi(getEnv(xName))
|
value, _ = strconv.Atoi(getEnv(xName))
|
||||||
|
@@ -7,6 +7,7 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/internal/experiments"
|
"github.com/go-task/task/v3/internal/experiments"
|
||||||
|
"github.com/go-task/task/v3/taskrc/ast"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestNew(t *testing.T) {
|
func TestNew(t *testing.T) {
|
||||||
@@ -16,43 +17,47 @@ func TestNew(t *testing.T) {
|
|||||||
)
|
)
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
config *ast.TaskRC
|
||||||
allowedValues []int
|
allowedValues []int
|
||||||
value int
|
env int
|
||||||
wantEnabled bool
|
wantEnabled bool
|
||||||
wantActive bool
|
wantActive bool
|
||||||
wantValid error
|
wantValid error
|
||||||
|
wantValue int
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: `[] allowed, value=""`,
|
name: `[] allowed, env=""`,
|
||||||
wantEnabled: false,
|
wantEnabled: false,
|
||||||
wantActive: false,
|
wantActive: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `[] allowed, value="1"`,
|
name: `[] allowed, env="1"`,
|
||||||
value: 1,
|
env: 1,
|
||||||
wantEnabled: false,
|
wantEnabled: false,
|
||||||
wantActive: false,
|
wantActive: false,
|
||||||
wantValid: &experiments.InactiveError{
|
wantValid: &experiments.InactiveError{
|
||||||
Name: exampleExperiment,
|
Name: exampleExperiment,
|
||||||
},
|
},
|
||||||
|
wantValue: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `[1] allowed, value=""`,
|
name: `[1] allowed, env=""`,
|
||||||
allowedValues: []int{1},
|
allowedValues: []int{1},
|
||||||
wantEnabled: false,
|
wantEnabled: false,
|
||||||
wantActive: true,
|
wantActive: true,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `[1] allowed, value="1"`,
|
name: `[1] allowed, env="1"`,
|
||||||
allowedValues: []int{1},
|
allowedValues: []int{1},
|
||||||
value: 1,
|
env: 1,
|
||||||
wantEnabled: true,
|
wantEnabled: true,
|
||||||
wantActive: true,
|
wantActive: true,
|
||||||
|
wantValue: 1,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: `[1] allowed, value="2"`,
|
name: `[1] allowed, env="2"`,
|
||||||
allowedValues: []int{1},
|
allowedValues: []int{1},
|
||||||
value: 2,
|
env: 2,
|
||||||
wantEnabled: false,
|
wantEnabled: false,
|
||||||
wantActive: true,
|
wantActive: true,
|
||||||
wantValid: &experiments.InvalidValueError{
|
wantValid: &experiments.InvalidValueError{
|
||||||
@@ -60,16 +65,76 @@ func TestNew(t *testing.T) {
|
|||||||
AllowedValues: []int{1},
|
AllowedValues: []int{1},
|
||||||
Value: 2,
|
Value: 2,
|
||||||
},
|
},
|
||||||
|
wantValue: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `[1, 2] allowed, env="1"`,
|
||||||
|
allowedValues: []int{1, 2},
|
||||||
|
env: 1,
|
||||||
|
wantEnabled: true,
|
||||||
|
wantActive: true,
|
||||||
|
wantValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `[1, 2] allowed, env="1"`,
|
||||||
|
allowedValues: []int{1, 2},
|
||||||
|
env: 2,
|
||||||
|
wantEnabled: true,
|
||||||
|
wantActive: true,
|
||||||
|
wantValue: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `[1] allowed, config="1"`,
|
||||||
|
config: &ast.TaskRC{
|
||||||
|
Experiments: map[string]int{
|
||||||
|
exampleExperiment: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowedValues: []int{1},
|
||||||
|
wantEnabled: true,
|
||||||
|
wantActive: true,
|
||||||
|
wantValue: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `[1] allowed, config="2"`,
|
||||||
|
config: &ast.TaskRC{
|
||||||
|
Experiments: map[string]int{
|
||||||
|
exampleExperiment: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowedValues: []int{1},
|
||||||
|
wantEnabled: false,
|
||||||
|
wantActive: true,
|
||||||
|
wantValid: &experiments.InvalidValueError{
|
||||||
|
Name: exampleExperiment,
|
||||||
|
AllowedValues: []int{1},
|
||||||
|
Value: 2,
|
||||||
|
},
|
||||||
|
wantValue: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: `[1, 2] allowed, env="1", config="2"`,
|
||||||
|
config: &ast.TaskRC{
|
||||||
|
Experiments: map[string]int{
|
||||||
|
exampleExperiment: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
allowedValues: []int{1, 2},
|
||||||
|
env: 1,
|
||||||
|
wantEnabled: true,
|
||||||
|
wantActive: true,
|
||||||
|
wantValue: 2,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.value))
|
t.Setenv(exampleExperimentEnv, strconv.Itoa(tt.env))
|
||||||
x := experiments.New(exampleExperiment, tt.allowedValues...)
|
x := experiments.New(exampleExperiment, tt.config, tt.allowedValues...)
|
||||||
assert.Equal(t, exampleExperiment, x.Name)
|
assert.Equal(t, exampleExperiment, x.Name)
|
||||||
assert.Equal(t, tt.wantEnabled, x.Enabled())
|
assert.Equal(t, tt.wantEnabled, x.Enabled())
|
||||||
assert.Equal(t, tt.wantActive, x.Active())
|
assert.Equal(t, tt.wantActive, x.Active())
|
||||||
assert.Equal(t, tt.wantValid, x.Valid())
|
assert.Equal(t, tt.wantValid, x.Valid())
|
||||||
|
assert.Equal(t, tt.wantValue, x.Value)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -6,46 +6,47 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/Masterminds/semver/v3"
|
|
||||||
"github.com/joho/godotenv"
|
"github.com/joho/godotenv"
|
||||||
"github.com/spf13/pflag"
|
|
||||||
"gopkg.in/yaml.v3"
|
"github.com/go-task/task/v3/taskrc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const envPrefix = "TASK_X_"
|
const envPrefix = "TASK_X_"
|
||||||
|
|
||||||
var defaultConfigFilenames = []string{
|
// Active experiments.
|
||||||
".taskrc.yml",
|
|
||||||
".taskrc.yaml",
|
|
||||||
}
|
|
||||||
|
|
||||||
type experimentConfigFile struct {
|
|
||||||
Experiments map[string]int `yaml:"experiments"`
|
|
||||||
Version *semver.Version
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
GentleForce Experiment
|
GentleForce Experiment
|
||||||
RemoteTaskfiles Experiment
|
RemoteTaskfiles Experiment
|
||||||
AnyVariables Experiment
|
|
||||||
MapVariables Experiment
|
|
||||||
EnvPrecedence Experiment
|
EnvPrecedence Experiment
|
||||||
)
|
)
|
||||||
|
|
||||||
// An internal list of all the initialized experiments used for iterating.
|
// Inactive experiments. These are experiments that cannot be enabled, but are
|
||||||
|
// preserved for error handling.
|
||||||
var (
|
var (
|
||||||
xList []Experiment
|
AnyVariables Experiment
|
||||||
experimentConfig experimentConfigFile
|
MapVariables Experiment
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
// An internal list of all the initialized experiments used for iterating.
|
||||||
readDotEnv()
|
var xList []Experiment
|
||||||
experimentConfig = readConfig()
|
|
||||||
GentleForce = New("GENTLE_FORCE", 1)
|
func Parse(dir string) {
|
||||||
RemoteTaskfiles = New("REMOTE_TASKFILES", 1)
|
// Read any .env files
|
||||||
AnyVariables = New("ANY_VARIABLES")
|
readDotEnv(dir)
|
||||||
MapVariables = New("MAP_VARIABLES")
|
|
||||||
EnvPrecedence = New("ENV_PRECEDENCE", 1)
|
// Create a node for the Task config reader
|
||||||
|
node, _ := taskrc.NewNode("", dir)
|
||||||
|
|
||||||
|
// Read the Task config file
|
||||||
|
reader := taskrc.NewReader()
|
||||||
|
config, _ := reader.Read(node)
|
||||||
|
|
||||||
|
// Initialize the experiments
|
||||||
|
GentleForce = New("GENTLE_FORCE", config, 1)
|
||||||
|
RemoteTaskfiles = New("REMOTE_TASKFILES", config, 1)
|
||||||
|
EnvPrecedence = New("ENV_PRECEDENCE", config, 1)
|
||||||
|
AnyVariables = New("ANY_VARIABLES", config)
|
||||||
|
MapVariables = New("MAP_VARIABLES", config)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate checks if any experiments have been enabled while being inactive.
|
// Validate checks if any experiments have been enabled while being inactive.
|
||||||
@@ -68,29 +69,19 @@ func getEnv(xName string) string {
|
|||||||
return os.Getenv(envName)
|
return os.Getenv(envName)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getFilePath(filename string) string {
|
func getFilePath(filename, dir string) string {
|
||||||
// Parse the CLI flags again to get the directory/taskfile being run
|
|
||||||
// We use a flagset here so that we can parse a subset of flags without exiting on error.
|
|
||||||
var dir, taskfile string
|
|
||||||
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
|
|
||||||
fs.StringVarP(&dir, "dir", "d", "", "Sets directory of execution.")
|
|
||||||
fs.StringVarP(&taskfile, "taskfile", "t", "", `Choose which Taskfile to run. Defaults to "Taskfile.yml".`)
|
|
||||||
fs.Usage = func() {}
|
|
||||||
_ = fs.Parse(os.Args[1:])
|
|
||||||
// If the directory is set, find a .env file in that directory.
|
|
||||||
if dir != "" {
|
if dir != "" {
|
||||||
return filepath.Join(dir, filename)
|
return filepath.Join(dir, filename)
|
||||||
}
|
}
|
||||||
// If the taskfile is set, find a .env file in the directory containing the Taskfile.
|
|
||||||
if taskfile != "" {
|
|
||||||
return filepath.Join(filepath.Dir(taskfile), filename)
|
|
||||||
}
|
|
||||||
// Otherwise just use the current working directory.
|
|
||||||
return filename
|
return filename
|
||||||
}
|
}
|
||||||
|
|
||||||
func readDotEnv() {
|
func readDotEnv(dir string) {
|
||||||
env, _ := godotenv.Read(getFilePath(".env"))
|
env, err := godotenv.Read(getFilePath(".env", dir))
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// If the env var is an experiment, set it.
|
// If the env var is an experiment, set it.
|
||||||
for key, value := range env {
|
for key, value := range env {
|
||||||
if strings.HasPrefix(key, envPrefix) {
|
if strings.HasPrefix(key, envPrefix) {
|
||||||
@@ -98,27 +89,3 @@ func readDotEnv() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func readConfig() experimentConfigFile {
|
|
||||||
var cfg experimentConfigFile
|
|
||||||
|
|
||||||
var content []byte
|
|
||||||
var err error
|
|
||||||
for _, filename := range defaultConfigFilenames {
|
|
||||||
path := getFilePath(filename)
|
|
||||||
content, err = os.ReadFile(path)
|
|
||||||
if err == nil {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return experimentConfigFile{}
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := yaml.Unmarshal(content, &cfg); err != nil {
|
|
||||||
return experimentConfigFile{}
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
@@ -4,6 +4,7 @@ import (
|
|||||||
"cmp"
|
"cmp"
|
||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -77,6 +78,26 @@ var (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
|
// Config files can enable experiments which alter the availability and/or
|
||||||
|
// behavior of some flags, so we need to parse the experiments before the
|
||||||
|
// flags. However, we need the --taskfile and --dir flags before we can
|
||||||
|
// parse the experiments as they can alter the location of the config files.
|
||||||
|
// Because of this circular dependency, we parse the flags twice. First, we
|
||||||
|
// get the --taskfile and --dir flags, then we parse the experiments, then
|
||||||
|
// we parse the flags again to get the full set. We use a flagset here so
|
||||||
|
// that we can parse a subset of flags without exiting on error.
|
||||||
|
var dir, entrypoint string
|
||||||
|
fs := pflag.NewFlagSet("experiments", pflag.ContinueOnError)
|
||||||
|
fs.StringVarP(&dir, "dir", "d", "", "")
|
||||||
|
fs.StringVarP(&entrypoint, "taskfile", "t", "", "")
|
||||||
|
fs.Usage = func() {}
|
||||||
|
_ = fs.Parse(os.Args[1:])
|
||||||
|
|
||||||
|
// Parse the experiments
|
||||||
|
dir = cmp.Or(dir, filepath.Dir(entrypoint))
|
||||||
|
experiments.Parse(dir)
|
||||||
|
|
||||||
|
// Parse the rest of the flags
|
||||||
log.SetFlags(0)
|
log.SetFlags(0)
|
||||||
log.SetOutput(os.Stderr)
|
log.SetOutput(os.Stderr)
|
||||||
pflag.Usage = func() {
|
pflag.Usage = func() {
|
||||||
|
146
internal/fsext/fs.go
Normal file
146
internal/fsext/fs.go
Normal file
@@ -0,0 +1,146 @@
|
|||||||
|
package fsext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/internal/filepathext"
|
||||||
|
"github.com/go-task/task/v3/internal/sysinfo"
|
||||||
|
)
|
||||||
|
|
||||||
|
// DefaultDir will return the default directory given an entrypoint or
|
||||||
|
// directory. If the directory is set, it will ensure it is an absolute path and
|
||||||
|
// return it. If the entrypoint is set, but the directory is not, it will leave
|
||||||
|
// the directory blank. If both are empty, it will default the directory to the
|
||||||
|
// current working directory.
|
||||||
|
func DefaultDir(entrypoint, dir string) string {
|
||||||
|
// If the directory is set, ensure it is an absolute path
|
||||||
|
if dir != "" {
|
||||||
|
var err error
|
||||||
|
dir, err = filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return dir
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entrypoint and dir are empty, we default the directory to the current working directory
|
||||||
|
if entrypoint == "" {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return wd
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the entrypoint is set, but the directory is not, we leave the directory blank
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search will look for files with the given possible filenames using the given
|
||||||
|
// entrypoint and directory. If the entrypoint is set, it will check if the
|
||||||
|
// entrypoint matches a file or if it matches a directory containing one of the
|
||||||
|
// possible filenames. Otherwise, it will walk up the file tree starting at the
|
||||||
|
// given directory and perform a search in each directory for the possible
|
||||||
|
// filenames until it finds a match or reaches the root directory. If the
|
||||||
|
// entrypoint and directory are both empty, it will default the directory to the
|
||||||
|
// current working directory and perform a recursive search starting there. If a
|
||||||
|
// match is found, the absolute path to the file will be returned with its
|
||||||
|
// directory. If no match is found, an error will be returned.
|
||||||
|
func Search(entrypoint, dir string, possibleFilenames []string) (string, string, error) {
|
||||||
|
var err error
|
||||||
|
if entrypoint != "" {
|
||||||
|
entrypoint, err = SearchPath(entrypoint, possibleFilenames)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
if dir == "" {
|
||||||
|
dir = filepath.Dir(entrypoint)
|
||||||
|
} else {
|
||||||
|
dir, err = filepath.Abs(dir)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return entrypoint, dir, nil
|
||||||
|
}
|
||||||
|
if dir == "" {
|
||||||
|
dir, err = os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entrypoint, err = SearchPathRecursively(dir, possibleFilenames)
|
||||||
|
if err != nil {
|
||||||
|
return "", "", err
|
||||||
|
}
|
||||||
|
dir = filepath.Dir(entrypoint)
|
||||||
|
return entrypoint, dir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search will check if a file at the given path exists or not. If it does, it
|
||||||
|
// will return the path to it. If it does not, it will search for any files at
|
||||||
|
// the given path with any of the given possible names. If any of these match a
|
||||||
|
// file, the first matching path will be returned. If no files are found, an
|
||||||
|
// error will be returned.
|
||||||
|
func SearchPath(path string, possibleFilenames []string) (string, error) {
|
||||||
|
// Get file info about the path
|
||||||
|
fi, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path exists and is a regular file, device, symlink, or named pipe,
|
||||||
|
// return the absolute path to it
|
||||||
|
if fi.Mode().IsRegular() ||
|
||||||
|
fi.Mode()&os.ModeDevice != 0 ||
|
||||||
|
fi.Mode()&os.ModeSymlink != 0 ||
|
||||||
|
fi.Mode()&os.ModeNamedPipe != 0 {
|
||||||
|
return filepath.Abs(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the path is a directory, check if any of the possible names exist
|
||||||
|
// in that directory
|
||||||
|
for _, filename := range possibleFilenames {
|
||||||
|
alt := filepathext.SmartJoin(path, filename)
|
||||||
|
if _, err := os.Stat(alt); err == nil {
|
||||||
|
return filepath.Abs(alt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
// SearchRecursively will check if a file at the given path exists by calling
|
||||||
|
// the exists function. If a file is not found, it will walk up the directory
|
||||||
|
// tree calling the Search function until it finds a file or reaches the root
|
||||||
|
// directory. On supported operating systems, it will also check if the user ID
|
||||||
|
// of the directory changes and abort if it does.
|
||||||
|
func SearchPathRecursively(path string, possibleFilenames []string) (string, error) {
|
||||||
|
owner, err := sysinfo.Owner(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
fpath, err := SearchPath(path, possibleFilenames)
|
||||||
|
if err == nil {
|
||||||
|
return fpath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the parent path/user id
|
||||||
|
parentPath := filepath.Dir(path)
|
||||||
|
parentOwner, err := sysinfo.Owner(parentPath)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Error if we reached the root directory and still haven't found a file
|
||||||
|
// OR if the user id of the directory changes
|
||||||
|
if path == parentPath || (parentOwner != owner) {
|
||||||
|
return "", os.ErrNotExist
|
||||||
|
}
|
||||||
|
|
||||||
|
owner = parentOwner
|
||||||
|
path = parentPath
|
||||||
|
}
|
||||||
|
}
|
152
internal/fsext/fs_test.go
Normal file
152
internal/fsext/fs_test.go
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
package fsext
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDefaultDir(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entrypoint string
|
||||||
|
dir string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "default to current working directory",
|
||||||
|
entrypoint: "",
|
||||||
|
dir: "",
|
||||||
|
expected: wd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resolves relative dir path",
|
||||||
|
entrypoint: "",
|
||||||
|
dir: "./dir",
|
||||||
|
expected: filepath.Join(wd, "dir"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "return entrypoint if set",
|
||||||
|
entrypoint: filepath.Join(wd, "entrypoint"),
|
||||||
|
dir: "",
|
||||||
|
expected: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "if entrypoint and dir are set",
|
||||||
|
entrypoint: filepath.Join(wd, "entrypoint"),
|
||||||
|
dir: filepath.Join(wd, "dir"),
|
||||||
|
expected: filepath.Join(wd, "dir"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "if entrypoint and dir are set and dir is relative",
|
||||||
|
entrypoint: filepath.Join(wd, "entrypoint"),
|
||||||
|
dir: "./dir",
|
||||||
|
expected: filepath.Join(wd, "dir"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
require.Equal(t, tt.expected, DefaultDir(tt.entrypoint, tt.dir))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSearch(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
entrypoint string
|
||||||
|
dir string
|
||||||
|
possibleFilenames []string
|
||||||
|
expectedEntrypoint string
|
||||||
|
expectedDir string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "find foo.txt using relative entrypoint",
|
||||||
|
entrypoint: "./testdata/foo.txt",
|
||||||
|
possibleFilenames: []string{"foo.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find foo.txt using absolute entrypoint",
|
||||||
|
entrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
possibleFilenames: []string{"foo.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find foo.txt using relative dir",
|
||||||
|
dir: "./testdata",
|
||||||
|
possibleFilenames: []string{"foo.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find foo.txt using absolute dir",
|
||||||
|
dir: filepath.Join(wd, "testdata"),
|
||||||
|
possibleFilenames: []string{"foo.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find foo.txt using relative dir and relative entrypoint",
|
||||||
|
entrypoint: "./testdata/foo.txt",
|
||||||
|
dir: "./testdata/some/other/dir",
|
||||||
|
possibleFilenames: []string{"foo.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find fs.go using no entrypoint or dir",
|
||||||
|
entrypoint: "",
|
||||||
|
dir: "",
|
||||||
|
possibleFilenames: []string{"fs.go"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "fs.go"),
|
||||||
|
expectedDir: wd,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find ../../Taskfile.yml using no entrypoint or dir by walking",
|
||||||
|
entrypoint: "",
|
||||||
|
dir: "",
|
||||||
|
possibleFilenames: []string{"Taskfile.yml"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"),
|
||||||
|
expectedDir: filepath.Join(wd, "..", ".."),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find foo.txt first if listed first in possible filenames",
|
||||||
|
entrypoint: "./testdata",
|
||||||
|
possibleFilenames: []string{"foo.txt", "bar.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "find bar.txt first if listed first in possible filenames",
|
||||||
|
entrypoint: "./testdata",
|
||||||
|
possibleFilenames: []string{"bar.txt", "foo.txt"},
|
||||||
|
expectedEntrypoint: filepath.Join(wd, "testdata", "bar.txt"),
|
||||||
|
expectedDir: filepath.Join(wd, "testdata"),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
entrypoint, dir, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectedEntrypoint, entrypoint)
|
||||||
|
require.Equal(t, tt.expectedDir, dir)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
0
internal/fsext/testdata/bar.txt
vendored
Normal file
0
internal/fsext/testdata/bar.txt
vendored
Normal file
0
internal/fsext/testdata/foo.txt
vendored
Normal file
0
internal/fsext/testdata/foo.txt
vendored
Normal file
@@ -2,8 +2,6 @@ package taskfile
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -11,6 +9,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
"github.com/go-task/task/v3/internal/experiments"
|
"github.com/go-task/task/v3/internal/experiments"
|
||||||
|
"github.com/go-task/task/v3/internal/fsext"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Node interface {
|
type Node interface {
|
||||||
@@ -34,7 +33,7 @@ func NewRootNode(
|
|||||||
insecure bool,
|
insecure bool,
|
||||||
timeout time.Duration,
|
timeout time.Duration,
|
||||||
) (Node, error) {
|
) (Node, error) {
|
||||||
dir = getDefaultDir(entrypoint, dir)
|
dir = fsext.DefaultDir(entrypoint, dir)
|
||||||
// If the entrypoint is "-", we read from stdin
|
// If the entrypoint is "-", we read from stdin
|
||||||
if entrypoint == "-" {
|
if entrypoint == "-" {
|
||||||
return NewStdinNode(dir)
|
return NewStdinNode(dir)
|
||||||
@@ -87,26 +86,3 @@ func getScheme(uri string) (string, error) {
|
|||||||
|
|
||||||
return "", nil
|
return "", nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func getDefaultDir(entrypoint, dir string) string {
|
|
||||||
// If the entrypoint and dir are empty, we default the directory to the current working directory
|
|
||||||
if dir == "" {
|
|
||||||
if entrypoint == "" {
|
|
||||||
wd, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
dir = wd
|
|
||||||
}
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the directory is set, ensure it is an absolute path
|
|
||||||
var err error
|
|
||||||
dir, err = filepath.Abs(dir)
|
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return dir
|
|
||||||
}
|
|
||||||
|
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"github.com/go-task/task/v3/internal/execext"
|
"github.com/go-task/task/v3/internal/execext"
|
||||||
"github.com/go-task/task/v3/internal/filepathext"
|
"github.com/go-task/task/v3/internal/filepathext"
|
||||||
|
"github.com/go-task/task/v3/internal/fsext"
|
||||||
)
|
)
|
||||||
|
|
||||||
// A FileNode is a node that reads a taskfile from the local filesystem.
|
// A FileNode is a node that reads a taskfile from the local filesystem.
|
||||||
@@ -19,7 +20,7 @@ type FileNode struct {
|
|||||||
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
|
func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) {
|
||||||
var err error
|
var err error
|
||||||
base := NewBaseNode(dir, opts...)
|
base := NewBaseNode(dir, opts...)
|
||||||
entrypoint, base.dir, err = resolveFileNodeEntrypointAndDir(entrypoint, base.dir)
|
entrypoint, base.dir, err = fsext.Search(entrypoint, base.dir, defaultTaskfiles)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -42,34 +43,6 @@ func (node *FileNode) Read() ([]byte, error) {
|
|||||||
return io.ReadAll(f)
|
return io.ReadAll(f)
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolveFileNodeEntrypointAndDir resolves checks the values of entrypoint and dir and
|
|
||||||
// populates them with default values if necessary.
|
|
||||||
func resolveFileNodeEntrypointAndDir(entrypoint, dir string) (string, string, error) {
|
|
||||||
var err error
|
|
||||||
if entrypoint != "" {
|
|
||||||
entrypoint, err = Exists(entrypoint)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
if dir == "" {
|
|
||||||
dir = filepath.Dir(entrypoint)
|
|
||||||
}
|
|
||||||
return entrypoint, dir, nil
|
|
||||||
}
|
|
||||||
if dir == "" {
|
|
||||||
dir, err = os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entrypoint, err = ExistsWalk(dir)
|
|
||||||
if err != nil {
|
|
||||||
return "", "", err
|
|
||||||
}
|
|
||||||
dir = filepath.Dir(entrypoint)
|
|
||||||
return entrypoint, dir, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
func (node *FileNode) ResolveEntrypoint(entrypoint string) (string, error) {
|
||||||
// If the file is remote, we don't need to resolve the path
|
// If the file is remote, we don't need to resolve the path
|
||||||
if strings.Contains(entrypoint, "://") {
|
if strings.Contains(entrypoint, "://") {
|
||||||
|
@@ -5,14 +5,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/go-task/task/v3/errors"
|
"github.com/go-task/task/v3/errors"
|
||||||
"github.com/go-task/task/v3/internal/filepathext"
|
|
||||||
"github.com/go-task/task/v3/internal/sysinfo"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@@ -94,65 +90,3 @@ func RemoteExists(ctx context.Context, u *url.URL) (*url.URL, error) {
|
|||||||
|
|
||||||
return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
|
return nil, errors.TaskfileNotFoundError{URI: u.String(), Walk: false}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Exists will check if a file at the given path Exists. If it does, it will
|
|
||||||
// return the path to it. If it does not, it will search for any files at the
|
|
||||||
// given path with any of the default Taskfile files names. If any of these
|
|
||||||
// match a file, the first matching path will be returned. If no files are
|
|
||||||
// found, an error will be returned.
|
|
||||||
func Exists(path string) (string, error) {
|
|
||||||
fi, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
if fi.Mode().IsRegular() ||
|
|
||||||
fi.Mode()&os.ModeDevice != 0 ||
|
|
||||||
fi.Mode()&os.ModeSymlink != 0 ||
|
|
||||||
fi.Mode()&os.ModeNamedPipe != 0 {
|
|
||||||
return filepath.Abs(path)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, taskfile := range defaultTaskfiles {
|
|
||||||
alt := filepathext.SmartJoin(path, taskfile)
|
|
||||||
if _, err := os.Stat(alt); err == nil {
|
|
||||||
return filepath.Abs(alt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", errors.TaskfileNotFoundError{URI: path, Walk: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ExistsWalk will check if a file at the given path exists by calling the
|
|
||||||
// exists function. If a file is not found, it will walk up the directory tree
|
|
||||||
// calling the exists function until it finds a file or reaches the root
|
|
||||||
// directory. On supported operating systems, it will also check if the user ID
|
|
||||||
// of the directory changes and abort if it does.
|
|
||||||
func ExistsWalk(path string) (string, error) {
|
|
||||||
origPath := path
|
|
||||||
owner, err := sysinfo.Owner(path)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
for {
|
|
||||||
fpath, err := Exists(path)
|
|
||||||
if err == nil {
|
|
||||||
return fpath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the parent path/user id
|
|
||||||
parentPath := filepath.Dir(path)
|
|
||||||
parentOwner, err := sysinfo.Owner(parentPath)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Error if we reached the root directory and still haven't found a file
|
|
||||||
// OR if the user id of the directory changes
|
|
||||||
if path == parentPath || (parentOwner != owner) {
|
|
||||||
return "", errors.TaskfileNotFoundError{URI: origPath, Walk: false}
|
|
||||||
}
|
|
||||||
|
|
||||||
owner = parentOwner
|
|
||||||
path = parentPath
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
8
taskrc/ast/taskrc.go
Normal file
8
taskrc/ast/taskrc.go
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
package ast
|
||||||
|
|
||||||
|
import "github.com/Masterminds/semver/v3"
|
||||||
|
|
||||||
|
type TaskRC struct {
|
||||||
|
Version *semver.Version `yaml:"version"`
|
||||||
|
Experiments map[string]int `yaml:"experiments"`
|
||||||
|
}
|
24
taskrc/node.go
Normal file
24
taskrc/node.go
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
package taskrc
|
||||||
|
|
||||||
|
import "github.com/go-task/task/v3/internal/fsext"
|
||||||
|
|
||||||
|
type Node struct {
|
||||||
|
entrypoint string
|
||||||
|
dir string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNode(
|
||||||
|
entrypoint string,
|
||||||
|
dir string,
|
||||||
|
) (*Node, error) {
|
||||||
|
dir = fsext.DefaultDir(entrypoint, dir)
|
||||||
|
var err error
|
||||||
|
entrypoint, dir, err = fsext.Search(entrypoint, dir, defaultTaskRCs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &Node{
|
||||||
|
entrypoint: entrypoint,
|
||||||
|
dir: dir,
|
||||||
|
}, nil
|
||||||
|
}
|
79
taskrc/reader.go
Normal file
79
taskrc/reader.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package taskrc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
|
||||||
|
"github.com/go-task/task/v3/taskrc/ast"
|
||||||
|
)
|
||||||
|
|
||||||
|
type (
|
||||||
|
// DebugFunc is a function that can be called to log debug messages.
|
||||||
|
DebugFunc func(string)
|
||||||
|
// A ReaderOption is any type that can apply a configuration to a [Reader].
|
||||||
|
ReaderOption interface {
|
||||||
|
ApplyToReader(*Reader)
|
||||||
|
}
|
||||||
|
// A Reader will recursively read Taskfiles from a given [Node] and build a
|
||||||
|
// [ast.TaskRC] from them.
|
||||||
|
Reader struct {
|
||||||
|
debugFunc DebugFunc
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewReader constructs a new Taskfile [Reader] using the given Node and
|
||||||
|
// options.
|
||||||
|
func NewReader(opts ...ReaderOption) *Reader {
|
||||||
|
r := &Reader{
|
||||||
|
debugFunc: nil,
|
||||||
|
}
|
||||||
|
r.Options(opts...)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Options loops through the given [ReaderOption] functions and applies them to
|
||||||
|
// the [Reader].
|
||||||
|
func (r *Reader) Options(opts ...ReaderOption) {
|
||||||
|
for _, opt := range opts {
|
||||||
|
opt.ApplyToReader(r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithDebugFunc sets the debug function to be used by the [Reader]. If set,
|
||||||
|
// this function will be called with debug messages. This can be useful if the
|
||||||
|
// caller wants to log debug messages from the [Reader]. By default, no debug
|
||||||
|
// function is set and the logs are not written.
|
||||||
|
func WithDebugFunc(debugFunc DebugFunc) ReaderOption {
|
||||||
|
return &debugFuncOption{debugFunc: debugFunc}
|
||||||
|
}
|
||||||
|
|
||||||
|
type debugFuncOption struct {
|
||||||
|
debugFunc DebugFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *debugFuncOption) ApplyToReader(r *Reader) {
|
||||||
|
r.debugFunc = o.debugFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read will read the Task config defined by the [Reader]'s [Node].
|
||||||
|
func (r *Reader) Read(node *Node) (*ast.TaskRC, error) {
|
||||||
|
var config ast.TaskRC
|
||||||
|
|
||||||
|
if node == nil {
|
||||||
|
return nil, os.ErrInvalid
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read the file
|
||||||
|
b, err := os.ReadFile(node.entrypoint)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the content
|
||||||
|
if err := yaml.Unmarshal(b, &config); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &config, nil
|
||||||
|
}
|
6
taskrc/taskrc.go
Normal file
6
taskrc/taskrc.go
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
package taskrc
|
||||||
|
|
||||||
|
var defaultTaskRCs = []string{
|
||||||
|
".taskrc.yml",
|
||||||
|
".taskrc.yaml",
|
||||||
|
}
|
Reference in New Issue
Block a user