diff --git a/experiments/experiments.go b/experiments/experiments.go index 9e646c57..f4deeb34 100644 --- a/experiments/experiments.go +++ b/experiments/experiments.go @@ -34,12 +34,7 @@ func Parse(dir string) { // Read any .env files readDotEnv(dir) - // Create a node for the Task config reader - node, _ := taskrc.NewNode("", dir) - - // Read the Task config file - reader := taskrc.NewReader() - config, _ := reader.Read(node) + config, _ := taskrc.GetConfig(dir) // Initialize the experiments GentleForce = New("GENTLE_FORCE", config, 1) diff --git a/internal/fsext/fs.go b/internal/fsext/fs.go index 5a64642e..8c8e3af6 100644 --- a/internal/fsext/fs.go +++ b/internal/fsext/fs.go @@ -37,51 +37,87 @@ func DefaultDir(entrypoint, dir string) string { 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 +// ResolveDir returns an absolute path to the directory that the task should be +// run in. If the entrypoint and dir are BOTH set, then the Taskfile will not +// sit inside the directory specified by dir and we should ensure that the dir +// is absolute. Otherwise, the dir will always be the parent directory of the +// resolved entrypoint, so we should return that parent directory. +func ResolveDir(entrypoint, resolvedEntrypoint, dir string) (string, error) { + if entrypoint != "" && dir != "" { + return filepath.Abs(dir) + } + return filepath.Dir(resolvedEntrypoint), nil +} + +// Search looks for files with the given possible filenames using the given +// entrypoint and directory. If the entrypoint is set, it checks 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 +// possible filenames. Otherwise, it walks up the file tree starting at the +// given directory and performs 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) { +// entrypoint and directory are both empty, it defaults the directory to the +// current working directory and performs a recursive search starting there. If +// a match is found, the absolute path to the file is returned with its +// directory. If no match is found, an error is returned. +func Search(entrypoint, dir string, possibleFilenames []string) (string, error) { var err error if entrypoint != "" { entrypoint, err = SearchPath(entrypoint, possibleFilenames) if err != nil { - return "", "", err + return "", err } - if dir == "" { - dir = filepath.Dir(entrypoint) - } else { - dir, err = filepath.Abs(dir) - if err != nil { - return "", "", err - } - } - return entrypoint, dir, nil + return entrypoint, nil } if dir == "" { dir, err = os.Getwd() if err != nil { - return "", "", err + return "", err } } entrypoint, err = SearchPathRecursively(dir, possibleFilenames) if err != nil { - return "", "", err + return "", err } - dir = filepath.Dir(entrypoint) - return entrypoint, dir, nil + return entrypoint, 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 +// SearchAll looks for files with the given possible filenames using the given +// entrypoint and directory. If the entrypoint is set, it checks if the +// entrypoint matches a file or if it matches a directory containing one of the +// possible filenames and add it to a list of matches. It then walks up the file +// tree starting at the given directory and performs 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 defaults the +// directory to the current working directory and performs a recursive search +// starting there. If matches are found, the absolute path to each file is added +// to the list and returned. +func SearchAll(entrypoint, dir string, possibleFilenames []string) ([]string, error) { + var err error + var entrypoints []string + if entrypoint != "" { + entrypoint, err = SearchPath(entrypoint, possibleFilenames) + if err != nil { + return nil, err + } + entrypoints = append(entrypoints, entrypoint) + } + if dir == "" { + dir, err = os.Getwd() + if err != nil { + return nil, err + } + } + paths, err := SearchNPathRecursively(dir, possibleFilenames, -1) + if err != nil { + return nil, err + } + return append(entrypoints, paths...), nil +} + +// SearchPath 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 @@ -111,36 +147,56 @@ func SearchPath(path string, possibleFilenames []string) (string, error) { 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. +// SearchPathRecursively walks up the directory tree starting at the given +// path, calling the Search function in each directory until it finds a matching +// 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) + paths, err := SearchNPathRecursively(path, possibleFilenames, 1) if err != nil { return "", err } - for { + if len(paths) == 0 { + return "", os.ErrNotExist + } + return paths[0], nil +} + +// SearchNPathRecursively walks up the directory tree starting at the given +// path, calling the Search function in each directory and adding each matching +// file that it finds to a list until it reaches the root directory or the +// length of the list exceeds n. On supported operating systems, it will also +// check if the user ID of the directory changes and abort if it does. +func SearchNPathRecursively(path string, possibleFilenames []string, n int) ([]string, error) { + var paths []string + + owner, err := sysinfo.Owner(path) + if err != nil { + return nil, err + } + + for n == -1 || len(paths) < n { fpath, err := SearchPath(path, possibleFilenames) if err == nil { - return fpath, nil + paths = append(paths, fpath) } // Get the parent path/user id parentPath := filepath.Dir(path) parentOwner, err := sysinfo.Owner(parentPath) if err != nil { - return "", err + return nil, 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 + return paths, nil } owner = parentOwner path = parentPath } + + return paths, nil } diff --git a/internal/fsext/fs_test.go b/internal/fsext/fs_test.go index 1ad5624d..06f59eb8 100644 --- a/internal/fsext/fs_test.go +++ b/internal/fsext/fs_test.go @@ -71,35 +71,30 @@ func TestSearch(t *testing.T) { 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", @@ -107,7 +102,6 @@ func TestSearch(t *testing.T) { 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", @@ -115,7 +109,6 @@ func TestSearch(t *testing.T) { dir: "", possibleFilenames: []string{"fs.go"}, expectedEntrypoint: filepath.Join(wd, "fs.go"), - expectedDir: wd, }, { name: "find ../../Taskfile.yml using no entrypoint or dir by walking", @@ -123,30 +116,109 @@ func TestSearch(t *testing.T) { 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"), + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + t.Parallel() + entrypoint, err := Search(tt.entrypoint, tt.dir, tt.possibleFilenames) + require.NoError(t, err) + require.Equal(t, tt.expectedEntrypoint, entrypoint) + require.NoError(t, err) + }) + } +} + +func TestResolveDir(t *testing.T) { + t.Parallel() + + wd, err := os.Getwd() + require.NoError(t, err) + + tests := []struct { + name string + entrypoint string + resolvedEntrypoint string + dir string + expectedDir string + }{ + { + name: "find foo.txt using relative entrypoint", + entrypoint: "./testdata/foo.txt", + resolvedEntrypoint: 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"), + resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), + expectedDir: filepath.Join(wd, "testdata"), + }, + { + name: "find foo.txt using relative dir", + resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), + dir: "./testdata", + expectedDir: filepath.Join(wd, "testdata"), + }, + { + name: "find foo.txt using absolute dir", + resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), + dir: filepath.Join(wd, "testdata"), + expectedDir: filepath.Join(wd, "testdata"), + }, + { + name: "find foo.txt using relative dir and relative entrypoint", + entrypoint: "./testdata/foo.txt", + resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), + dir: "./testdata/some/other/dir", + expectedDir: filepath.Join(wd, "testdata", "some", "other", "dir"), + }, + { + name: "find fs.go using no entrypoint or dir", + entrypoint: "", + resolvedEntrypoint: filepath.Join(wd, "fs.go"), + dir: "", + expectedDir: wd, + }, + { + name: "find ../../Taskfile.yml using no entrypoint or dir by walking", + entrypoint: "", + resolvedEntrypoint: filepath.Join(wd, "..", "..", "Taskfile.yml"), + dir: "", + expectedDir: filepath.Join(wd, "..", ".."), + }, + { + name: "find foo.txt first if listed first in possible filenames", + entrypoint: "./testdata", + resolvedEntrypoint: filepath.Join(wd, "testdata", "foo.txt"), + expectedDir: filepath.Join(wd, "testdata"), + }, + { + name: "find bar.txt first if listed first in possible filenames", + entrypoint: "./testdata", + resolvedEntrypoint: 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) + dir, err := ResolveDir(tt.entrypoint, tt.resolvedEntrypoint, tt.dir) require.NoError(t, err) - require.Equal(t, tt.expectedEntrypoint, entrypoint) require.Equal(t, tt.expectedDir, dir) + require.NoError(t, err) }) } } diff --git a/taskfile/node_file.go b/taskfile/node_file.go index aa35437c..e2548338 100644 --- a/taskfile/node_file.go +++ b/taskfile/node_file.go @@ -18,15 +18,21 @@ type FileNode struct { } func NewFileNode(entrypoint, dir string, opts ...NodeOption) (*FileNode, error) { - var err error - base := NewBaseNode(dir, opts...) - entrypoint, base.dir, err = fsext.Search(entrypoint, base.dir, defaultTaskfiles) + // Find the entrypoint file + resolvedEntrypoint, err := fsext.Search(entrypoint, dir, defaultTaskfiles) if err != nil { return nil, err } + + // Resolve the directory + resolvedDir, err := fsext.ResolveDir(entrypoint, resolvedEntrypoint, dir) + if err != nil { + return nil, err + } + return &FileNode{ - baseNode: base, - entrypoint: entrypoint, + baseNode: NewBaseNode(resolvedDir, opts...), + entrypoint: resolvedEntrypoint, }, nil } diff --git a/taskrc/ast/taskrc.go b/taskrc/ast/taskrc.go index f82452a9..13972090 100644 --- a/taskrc/ast/taskrc.go +++ b/taskrc/ast/taskrc.go @@ -1,8 +1,27 @@ package ast -import "github.com/Masterminds/semver/v3" +import ( + "maps" + + "github.com/Masterminds/semver/v3" +) type TaskRC struct { Version *semver.Version `yaml:"version"` Experiments map[string]int `yaml:"experiments"` } + +// Merge combines the current TaskRC with another TaskRC, prioritizing non-nil fields from the other TaskRC. +func (t *TaskRC) Merge(other *TaskRC) { + if other == nil { + return + } + if t.Version == nil && other.Version != nil { + t.Version = other.Version + } + if t.Experiments == nil && other.Experiments != nil { + t.Experiments = other.Experiments + } else if t.Experiments != nil && other.Experiments != nil { + maps.Copy(t.Experiments, other.Experiments) + } +} diff --git a/taskrc/node.go b/taskrc/node.go index 4aef0aa8..fa0dfe4f 100644 --- a/taskrc/node.go +++ b/taskrc/node.go @@ -1,10 +1,11 @@ package taskrc -import "github.com/go-task/task/v3/internal/fsext" +import ( + "github.com/go-task/task/v3/internal/fsext" +) type Node struct { entrypoint string - dir string } func NewNode( @@ -12,13 +13,11 @@ func NewNode( dir string, ) (*Node, error) { dir = fsext.DefaultDir(entrypoint, dir) - var err error - entrypoint, dir, err = fsext.Search(entrypoint, dir, defaultTaskRCs) + resolvedEntrypoint, err := fsext.SearchPath(dir, defaultTaskRCs) if err != nil { return nil, err } return &Node{ - entrypoint: entrypoint, - dir: dir, + entrypoint: resolvedEntrypoint, }, nil } diff --git a/taskrc/taskrc.go b/taskrc/taskrc.go index af993055..ae9d7f65 100644 --- a/taskrc/taskrc.go +++ b/taskrc/taskrc.go @@ -1,6 +1,63 @@ package taskrc +import ( + "os" + "path/filepath" + "slices" + + "github.com/go-task/task/v3/internal/fsext" + "github.com/go-task/task/v3/taskrc/ast" +) + var defaultTaskRCs = []string{ ".taskrc.yml", ".taskrc.yaml", } + +// GetConfig loads and merges local and global Task configuration files +func GetConfig(dir string) (*ast.TaskRC, error) { + var config *ast.TaskRC + reader := NewReader() + + // Read the XDG config file + if xdgConfigHome := os.Getenv("XDG_CONFIG_HOME"); xdgConfigHome != "" { + xdgConfigNode, err := NewNode("", filepath.Join(xdgConfigHome, "task")) + if err == nil && xdgConfigNode != nil { + config, err = reader.Read(xdgConfigNode) + if err != nil { + return nil, err + } + } + } + + // Find all the nodes from the given directory up to the users home directory + entrypoints, err := fsext.SearchAll("", dir, defaultTaskRCs) + if err != nil { + return nil, err + } + + // Reverse the entrypoints since we want the child files to override parent ones + slices.Reverse(entrypoints) + + // Loop over the nodes, and merge them into the main config + for _, entrypoint := range entrypoints { + node, err := NewNode("", entrypoint) + if err != nil { + return nil, err + } + localConfig, err := reader.Read(node) + if err != nil { + return nil, err + } + if localConfig == nil { + continue + } + if config == nil { + config = localConfig + continue + } + config.Merge(localConfig) + } + + return config, nil +} diff --git a/taskrc/taskrc_test.go b/taskrc/taskrc_test.go new file mode 100644 index 00000000..84261012 --- /dev/null +++ b/taskrc/taskrc_test.go @@ -0,0 +1,137 @@ +package taskrc + +import ( + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "github.com/go-task/task/v3/taskrc/ast" +) + +const ( + xdgConfigYAML = ` +experiments: + FOO: 1 + BAR: 1 + BAZ: 1 +` + + homeConfigYAML = ` +experiments: + FOO: 2 + BAR: 2 +` + + localConfigYAML = ` +experiments: + FOO: 3 +` +) + +func setupDirs(t *testing.T) (string, string, string) { + t.Helper() + + xdgConfigDir := t.TempDir() + xdgTaskConfigDir := filepath.Join(xdgConfigDir, "task") + require.NoError(t, os.Mkdir(xdgTaskConfigDir, 0o755)) + + homeDir := t.TempDir() + + localDir := filepath.Join(homeDir, "local") + require.NoError(t, os.Mkdir(localDir, 0o755)) + + t.Setenv("XDG_CONFIG_HOME", xdgConfigDir) + t.Setenv("HOME", homeDir) + + return xdgTaskConfigDir, homeDir, localDir +} + +func writeFile(t *testing.T, dir, filename, content string) { + t.Helper() + err := os.WriteFile(filepath.Join(dir, filename), []byte(content), 0o644) + assert.NoError(t, err) +} + +func TestGetConfig_NoConfigFiles(t *testing.T) { //nolint:paralleltest // cannot run in parallel + _, _, localDir := setupDirs(t) + + cfg, err := GetConfig(localDir) + assert.NoError(t, err) + assert.Nil(t, cfg) +} + +func TestGetConfig_OnlyXDG(t *testing.T) { //nolint:paralleltest // cannot run in parallel + xdgDir, _, localDir := setupDirs(t) + + writeFile(t, xdgDir, ".taskrc.yml", xdgConfigYAML) + + cfg, err := GetConfig(localDir) + assert.NoError(t, err) + assert.Equal(t, &ast.TaskRC{ + Version: nil, + Experiments: map[string]int{ + "FOO": 1, + "BAR": 1, + "BAZ": 1, + }, + }, cfg) +} + +func TestGetConfig_OnlyHome(t *testing.T) { //nolint:paralleltest // cannot run in parallel + _, homeDir, localDir := setupDirs(t) + + writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML) + + cfg, err := GetConfig(localDir) + assert.NoError(t, err) + assert.Equal(t, &ast.TaskRC{ + Version: nil, + Experiments: map[string]int{ + "FOO": 2, + "BAR": 2, + }, + }, cfg) +} + +func TestGetConfig_OnlyLocal(t *testing.T) { //nolint:paralleltest // cannot run in parallel + _, _, localDir := setupDirs(t) + + writeFile(t, localDir, ".taskrc.yml", localConfigYAML) + + cfg, err := GetConfig(localDir) + assert.NoError(t, err) + assert.Equal(t, &ast.TaskRC{ + Version: nil, + Experiments: map[string]int{ + "FOO": 3, + }, + }, cfg) +} + +func TestGetConfig_All(t *testing.T) { //nolint:paralleltest // cannot run in parallel + xdgConfigDir, homeDir, localDir := setupDirs(t) + + // Write local config + writeFile(t, localDir, ".taskrc.yml", localConfigYAML) + + // Write home config + writeFile(t, homeDir, ".taskrc.yml", homeConfigYAML) + + // Write XDG config + writeFile(t, xdgConfigDir, ".taskrc.yml", xdgConfigYAML) + + cfg, err := GetConfig(localDir) + assert.NoError(t, err) + assert.NotNil(t, cfg) + assert.Equal(t, &ast.TaskRC{ + Version: nil, + Experiments: map[string]int{ + "FOO": 3, + "BAR": 2, + "BAZ": 1, + }, + }, cfg) +} diff --git a/website/.vitepress/config.ts b/website/.vitepress/config.ts index 5e3bf43a..5386ad49 100644 --- a/website/.vitepress/config.ts +++ b/website/.vitepress/config.ts @@ -204,12 +204,20 @@ export default defineConfig({ collapsed: true, items: [ { - text: 'CLI', - link: '/docs/reference/cli' + text: 'Taskfile Schema', + link: '/docs/reference/schema' }, { - text: 'Schema', - link: '/docs/reference/schema' + text: 'Environment', + link: '/docs/reference/environment' + }, + { + text: 'Configuration', + link: '/docs/reference/config' + }, + { + text: 'CLI', + link: '/docs/reference/cli' }, { text: 'Templating', @@ -218,10 +226,6 @@ export default defineConfig({ { text: 'Package API', link: '/docs/reference/package' - }, - { - text: 'Environment', - link: '/docs/reference/environment' } ] }, diff --git a/website/src/docs/reference/cli.md b/website/src/docs/reference/cli.md index eb1c7019..848f492c 100644 --- a/website/src/docs/reference/cli.md +++ b/website/src/docs/reference/cli.md @@ -1,13 +1,26 @@ --- -title: CLI Reference +title: Command Line Interface Reference description: Complete reference for Task CLI commands, flags, and exit codes permalink: /reference/cli/ outline: deep --- -# Command Line Interface +# Command Line Interface Reference -Task CLI commands have the following syntax: +Task has multiple ways of being configured. These methods are parsed, in +sequence, in the following order with the highest priority last: + +- [Environment variables](./environment.md) +- [Configuration files](./config.md) +- _Command-line flags_ + +In this document, we will look at the last of the three options, command-line +flags. All CLI commands override their configuration file and environment +variable equivalents. + +## Format + +Task commands have the following syntax: ```bash task [options] [tasks...] [-- CLI_ARGS...] @@ -16,7 +29,7 @@ task [options] [tasks...] [-- CLI_ARGS...] ::: tip If `--` is given, all remaining arguments will be assigned to a special -`CLI_ARGS` variable +`CLI_ARGS` variable. ::: diff --git a/website/src/docs/reference/config.md b/website/src/docs/reference/config.md new file mode 100644 index 00000000..ddba3b4c --- /dev/null +++ b/website/src/docs/reference/config.md @@ -0,0 +1,70 @@ +--- +title: Configuration Reference +description: Complete reference for the Task config files and env vars +permalink: /reference/config/ +outline: deep +--- + +# Configuration Reference + +Task has multiple ways of being configured. These methods are parsed, in +sequence, in the following order with the highest priority last: + +- [Environment variables](./environment.md) +- _Configuration files_ +- [Command-line flags](./cli.md) + +In this document, we will look at the second of the three options, configuration +files. + +## File Precedence + +Task's configuration files are named `.taskrc.yml` or `.taskrc.yaml`. Task will +automatically look for directories containing files with these names in the +following order with the highest priority first: + +- Current directory (or the one specified by the `--taskfile`/`--entrypoint` + flags). +- Each directory walking up the file tree from the current directory (or the one + specified by the `--taskfile`/`--entrypoint` flags) until we reach the user's + home directory or the root directory of that drive. +- `$XDG_CONFIG_HOME/task`. + +All config files will be merged together into a unified config, starting with +the lowest priority file in `$XDG_CONFIG_HOME/task` with each subsequent file +overwriting the previous one if values are set. + +For example, given the following files: + +```yaml [$XDG_CONFIG_HOME/task/.taskrc.yml] +# lowest priority global config +option_1: foo +option_2: foo +option_3: foo +``` + +```yaml [$HOME/.taskrc.yml] +option_1: bar +option_2: bar +``` + +```yaml [$HOME/path/to/project/.taskrc.yml] +# highest priority project config +option_1: baz +``` + +You would end up with the following configuration: + +```yaml +option_1: baz # Taken from $HOME/path/to/project/.taskrc.yml +option_2: bar # Taken from $HOME/.taskrc.yml +option_3: foo # Taken from $XDG_CONFIG_HOME/task/.taskrc.yml +``` + +## Configuration Options + +### `experiments` + +The experiments section allows you to enable Task's experimental features. These +options are not enumerated here. Instead, please refer to our +[experiments documentation](../experiments/index.md) for more information. diff --git a/website/src/docs/reference/environment.md b/website/src/docs/reference/environment.md index 0d62ce23..3ff5ac6d 100644 --- a/website/src/docs/reference/environment.md +++ b/website/src/docs/reference/environment.md @@ -6,33 +6,43 @@ outline: deep # Environment Reference -Task allows you to configure some behavior using environment variables. This -page lists all the environment variables that Task supports. +Task has multiple ways of being configured. These methods are parsed, in +sequence, in the following order with the highest priority last: -| ENV | Default | Description | -| ----------------- | --------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- | -| `TASK_TEMP_DIR` | `.task` | Location of the temp dir. Can relative to the project like `tmp/task` or absolute like `/tmp/.task` or `~/.task`. | -| `TASK_REMOTE_DIR` | `TASK_TEMP_DIR` | Location of the remote temp dir (used for caching). Can relative to the project like `tmp/task` or absolute like `/tmp/.task` or `~/.task`. | -| `TASK_OFFLINE` | `false` | Set the `--offline` flag through the environment variable. Only for remote experiment. CLI flag `--offline` takes precedence over the env variable | -| `FORCE_COLOR` | | Force color output usage. | +- _Environment variables_ +- [Configuration files](./config.md) +- [Command-line flags](./cli.md) -## Custom Colors +In this document, we will look at the first of the three options, environment +variables. All Task-specific variables are prefixed with `TASK_` and override +their configuration file equivalents. -| ENV | Default | Description | -| --------------------------- | ------- | ----------------------- | -| `TASK_COLOR_RESET` | `0` | Color used for white. | -| `TASK_COLOR_RED` | `31` | Color used for red. | -| `TASK_COLOR_GREEN` | `32` | Color used for green. | -| `TASK_COLOR_YELLOW` | `33` | Color used for yellow. | -| `TASK_COLOR_BLUE` | `34` | Color used for blue. | -| `TASK_COLOR_MAGENTA` | `35` | Color used for magenta. | -| `TASK_COLOR_CYAN` | `36` | Color used for cyan. | -| `TASK_COLOR_BRIGHT_RED` | `91` | Color used for red. | -| `TASK_COLOR_BRIGHT_GREEN` | `92` | Color used for green. | -| `TASK_COLOR_BRIGHT_YELLOW` | `93` | Color used for yellow. | -| `TASK_COLOR_BRIGHT_BLUE` | `94` | Color used for blue. | -| `TASK_COLOR_BRIGHT_MAGENTA` | `95` | Color used for magenta. | -| `TASK_COLOR_BRIGHT_CYAN` | `96` | Color used for cyan. | +## Variables + +### `TASK_TEMP_DIR` + +Defines the location of Task's temporary directory which is used for storing +checksums and temporary metadata. Can be relative like `tmp/task` or absolute +like `/tmp/.task` or `~/.task`. Relative paths are relative to the root +Taskfile, not the working directory. Defaults to: `./.task`. + +### `TASK_REMOTE_DIR` + +Defines the location of Task's remote temporary directory which is used for +caching remote files. Can be relative like `tmp/task` or absolute like +`/tmp/.task` or `~/.task`. Relative paths are relative to the root Taskfile, not +the working directory. Defaults to: `./.task`. + +### `TASK_OFFLINE` + +Set the `--offline` flag through the environment variable. Only for remote +experiment. CLI flag `--offline` takes precedence over the env variable. + +### `FORCE_COLOR` + +Force color output usage. + +### Custom Colors All color variables are [ANSI color codes][ansi]. You can specify multiple codes separated by a semicolon. For example: `31;1` will make the text bold and red. @@ -45,4 +55,22 @@ For convenience, we allow foreground colors to be specified using shorthand, comma-separated syntax: `R,G,B`. For example, `255,0,0` is equivalent to `38;2;255:0:0`. +A table of variables and their defaults can be found below: + +| ENV | Default | +| --------------------------- | ------- | +| `TASK_COLOR_RESET` | `0` | +| `TASK_COLOR_RED` | `31` | +| `TASK_COLOR_GREEN` | `32` | +| `TASK_COLOR_YELLOW` | `33` | +| `TASK_COLOR_BLUE` | `34` | +| `TASK_COLOR_MAGENTA` | `35` | +| `TASK_COLOR_CYAN` | `36` | +| `TASK_COLOR_BRIGHT_RED` | `91` | +| `TASK_COLOR_BRIGHT_GREEN` | `92` | +| `TASK_COLOR_BRIGHT_YELLOW` | `93` | +| `TASK_COLOR_BRIGHT_BLUE` | `94` | +| `TASK_COLOR_BRIGHT_MAGENTA` | `95` | +| `TASK_COLOR_BRIGHT_CYAN` | `96` | + [ansi]: https://en.wikipedia.org/wiki/ANSI_escape_code diff --git a/website/src/docs/reference/package.md b/website/src/docs/reference/package.md index fe134ee2..7f710124 100644 --- a/website/src/docs/reference/package.md +++ b/website/src/docs/reference/package.md @@ -1,9 +1,9 @@ --- -title: CLI Reference -description: Complete reference for Task CLI commands, flags, and exit codes +title: Package API Reference +description: A reference for Task's Golang package API --- -# Package API +# Package API Reference ::: warning diff --git a/website/src/docs/reference/schema.md b/website/src/docs/reference/schema.md index 8de2398e..a6d81c35 100644 --- a/website/src/docs/reference/schema.md +++ b/website/src/docs/reference/schema.md @@ -1,11 +1,10 @@ --- -title: Schema Reference -description: - Complete reference for the Taskfile schema based on the official JSON schema +title: Taskfile Schema Reference +description: A reference for the Taskfile schema outline: deep --- -# Schema Reference +# Taskfile Schema Reference This page documents all available properties and types for the Taskfile schema version 3, based on the