mirror of
https://github.com/go-task/task.git
synced 2026-02-24 03:59:52 +01:00
feat(vars): add interactive prompting for required variables (#2579)
This commit is contained in:
@@ -86,6 +86,7 @@ complete -c $GO_TASK_PROGNAME -s j -l json -d 'format task
|
||||
complete -c $GO_TASK_PROGNAME -s l -l list -d 'list tasks with descriptions'
|
||||
complete -c $GO_TASK_PROGNAME -l nested -d 'nest namespaces when listing as JSON'
|
||||
complete -c $GO_TASK_PROGNAME -l no-status -d 'ignore status when listing as JSON'
|
||||
complete -c $GO_TASK_PROGNAME -l interactive -d 'prompt for missing required variables'
|
||||
complete -c $GO_TASK_PROGNAME -s o -l output -d 'set output style' -xa "interleaved group prefixed"
|
||||
complete -c $GO_TASK_PROGNAME -l output-group-begin -d 'message template before grouped output'
|
||||
complete -c $GO_TASK_PROGNAME -l output-group-end -d 'message template after grouped output'
|
||||
|
||||
@@ -40,6 +40,7 @@ Register-ArgumentCompleter -CommandName task -ScriptBlock {
|
||||
[CompletionResult]::new('--list', '--list', [CompletionResultType]::ParameterName, 'list tasks'),
|
||||
[CompletionResult]::new('--nested', '--nested', [CompletionResultType]::ParameterName, 'nest namespaces in JSON'),
|
||||
[CompletionResult]::new('--no-status', '--no-status', [CompletionResultType]::ParameterName, 'ignore status in JSON'),
|
||||
[CompletionResult]::new('--interactive', '--interactive', [CompletionResultType]::ParameterName, 'prompt for missing required variables'),
|
||||
[CompletionResult]::new('-o', '-o', [CompletionResultType]::ParameterName, 'set output style'),
|
||||
[CompletionResult]::new('--output', '--output', [CompletionResultType]::ParameterName, 'set output style'),
|
||||
[CompletionResult]::new('--output-group-begin', '--output-group-begin', [CompletionResultType]::ParameterName, 'template before group'),
|
||||
|
||||
@@ -90,6 +90,7 @@ _task() {
|
||||
'(-j --json)'{-j,--json}'[format task list as JSON]'
|
||||
'(--nested)--nested[nest namespaces when listing as JSON]'
|
||||
'(--no-status)--no-status[ignore status when listing as JSON]'
|
||||
'(--interactive)--interactive[prompt for missing required variables]'
|
||||
'(-o --output)'{-o,--output}'[set output style]:style:(interleaved group prefixed)'
|
||||
'(--output-group-begin)--output-group-begin[message template before grouped output]:template text: '
|
||||
'(--output-group-end)--output-group-end[message template after grouped output]:template text: '
|
||||
|
||||
15
executor.go
15
executor.go
@@ -44,6 +44,7 @@ type (
|
||||
DisableFuzzy bool
|
||||
AssumeYes bool
|
||||
AssumeTerm bool // Used for testing
|
||||
Interactive bool
|
||||
Dry bool
|
||||
Summary bool
|
||||
Parallel bool
|
||||
@@ -70,6 +71,7 @@ type (
|
||||
fuzzyModel *fuzzy.Model
|
||||
fuzzyModelOnce sync.Once
|
||||
|
||||
promptedVars *ast.Vars // vars collected via interactive prompts
|
||||
concurrencySemaphore chan struct{}
|
||||
taskCallCount map[string]*int32
|
||||
mkdirMutexMap map[string]*sync.Mutex
|
||||
@@ -367,6 +369,19 @@ func (o *assumeTermOption) ApplyToExecutor(e *Executor) {
|
||||
e.AssumeTerm = o.assumeTerm
|
||||
}
|
||||
|
||||
// WithInteractive tells the [Executor] to prompt for missing required variables.
|
||||
func WithInteractive(interactive bool) ExecutorOption {
|
||||
return &interactiveOption{interactive}
|
||||
}
|
||||
|
||||
type interactiveOption struct {
|
||||
interactive bool
|
||||
}
|
||||
|
||||
func (o *interactiveOption) ApplyToExecutor(e *Executor) {
|
||||
e.Interactive = o.interactive
|
||||
}
|
||||
|
||||
// WithDry tells the [Executor] to output the commands that would be run without
|
||||
// actually running them.
|
||||
func WithDry(dry bool) ExecutorOption {
|
||||
|
||||
18
go.mod
18
go.mod
@@ -5,6 +5,9 @@ go 1.24.6
|
||||
toolchain go1.25.6
|
||||
|
||||
require (
|
||||
charm.land/bubbles/v2 v2.0.0-rc.1
|
||||
charm.land/bubbletea/v2 v2.0.0-rc.2
|
||||
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7
|
||||
github.com/Ladicle/tabwriter v1.0.0
|
||||
github.com/Masterminds/semver/v3 v3.4.0
|
||||
github.com/alecthomas/chroma/v2 v2.23.0
|
||||
@@ -45,6 +48,7 @@ require (
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.29.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.54.0 // indirect
|
||||
github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.54.0 // indirect
|
||||
github.com/atotto/clipboard v0.1.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 // indirect
|
||||
github.com/aws/aws-sdk-go-v2/config v1.32.6 // indirect
|
||||
@@ -66,6 +70,15 @@ require (
|
||||
github.com/aws/smithy-go v1.24.0 // indirect
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/charmbracelet/colorprofile v0.3.3 // indirect
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 // indirect
|
||||
github.com/charmbracelet/x/ansi v0.11.1 // indirect
|
||||
github.com/charmbracelet/x/term v0.2.2 // indirect
|
||||
github.com/charmbracelet/x/termios v0.1.1 // indirect
|
||||
github.com/charmbracelet/x/windows v0.2.2 // indirect
|
||||
github.com/clipperhouse/displaywidth v0.5.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 // indirect
|
||||
github.com/dlclark/regexp2 v1.11.5 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
@@ -84,18 +97,23 @@ require (
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/pgzip v1.2.6 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/muesli/cancelreader v0.2.2 // indirect
|
||||
github.com/pierrec/lz4/v4 v4.1.22 // indirect
|
||||
github.com/planetscale/vtprotobuf v0.6.1-0.20240319094008-0393e58bdf10 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect
|
||||
github.com/spiffe/go-spiffe/v2 v2.5.0 // indirect
|
||||
github.com/stretchr/objx v0.5.2 // indirect
|
||||
github.com/u-root/u-root v0.15.1-0.20251208185023-2f8c7e763cf8 // indirect
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||
github.com/ulikunitz/xz v0.5.15 // indirect
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
||||
github.com/zeebo/errs v1.4.0 // indirect
|
||||
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
|
||||
go.opentelemetry.io/contrib/detectors/gcp v1.36.0 // indirect
|
||||
|
||||
42
go.sum
42
go.sum
@@ -1,5 +1,11 @@
|
||||
cel.dev/expr v0.24.0 h1:56OvJKSH3hDGL0ml5uSxZmz3/3Pq4tJ+fb1unVLAFcY=
|
||||
cel.dev/expr v0.24.0/go.mod h1:hLPLo1W4QUmuYdA72RBX06QTs6MXw941piREPl3Yfiw=
|
||||
charm.land/bubbles/v2 v2.0.0-rc.1 h1:EiIFVAc3Zi/yY86td+79mPhHR7AqZ1OxF+6ztpOCRaM=
|
||||
charm.land/bubbles/v2 v2.0.0-rc.1/go.mod h1:5AbN6cEd/47gkEf8TgiQ2O3RZ5QxMS14l9W+7F9fPC4=
|
||||
charm.land/bubbletea/v2 v2.0.0-rc.2 h1:TdTbUOFzbufDJmSz/3gomL6q+fR6HwfY+P13hXQzD7k=
|
||||
charm.land/bubbletea/v2 v2.0.0-rc.2/go.mod h1:IXFmnCnMLTWw/KQ9rEatSYqbAPAYi8kA3Yqwa1SFnLk=
|
||||
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7 h1:059k1h5vvZ4ASinki9nmBguxu9Rq0UDDSa6q8LOUphk=
|
||||
charm.land/lipgloss/v2 v2.0.0-beta.3.0.20251106192539-4b304240aab7/go.mod h1:1qZyvvVCenJO2M1ac2mX0yyiIZJoZmDM4DG4s0udJkU=
|
||||
cloud.google.com/go v0.123.0 h1:2NAUJwPR47q+E35uaJeYoNhuNEM9kM8SjgRgdeOJUSE=
|
||||
cloud.google.com/go v0.123.0/go.mod h1:xBoMV08QcqUGuPW65Qfm1o9Y4zKZBpGS+7bImXLTAZU=
|
||||
cloud.google.com/go/auth v0.17.0 h1:74yCm7hCj2rUyyAocqnFzsAYXgJhrG26XCFimrc/Kz4=
|
||||
@@ -38,6 +44,8 @@ github.com/alecthomas/chroma/v2 v2.23.0 h1:u/Orux1J0eLuZDeQ44froV8smumheieI0Eofh
|
||||
github.com/alecthomas/chroma/v2 v2.23.0/go.mod h1:NqVhfBR0lte5Ouh3DcthuUCTUpDC9cxBOfyMbMQPs3o=
|
||||
github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs=
|
||||
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
|
||||
github.com/atotto/clipboard v0.1.4 h1:EH0zSVneZPSuFR11BlR9YppQTVDbh5+16AmcJi4g1z4=
|
||||
github.com/atotto/clipboard v0.1.4/go.mod h1:ZY9tmq7sm5xIbd9bOK4onWV4S6X0u6GY7Vn0Yu86PYI=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0 h1:tNvqh1s+v0vFYdA1xq0aOJH+Y5cRyZ5upu6roPgPKd4=
|
||||
github.com/aws/aws-sdk-go-v2 v1.41.0/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0=
|
||||
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU=
|
||||
@@ -76,12 +84,34 @@ github.com/aws/aws-sdk-go-v2/service/sts v1.41.5 h1:SciGFVNZ4mHdm7gpD1dgZYnCuVdX
|
||||
github.com/aws/aws-sdk-go-v2/service/sts v1.41.5/go.mod h1:iW40X4QBmUxdP+fZNOpfmkdMZqsovezbAeO+Ubiv2pk=
|
||||
github.com/aws/smithy-go v1.24.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk=
|
||||
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1 h1:LV+qyBQ2pqe0u42ZsUEtPiCaUoqgA9gYRDs3vj1nolY=
|
||||
github.com/aymanbagabas/go-udiff v0.3.1/go.mod h1:G0fsKmG+P6ylD0r6N/KgQD/nWzgfnl8ZBcNLgcbrw8E=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d h1:xDfNPAt8lFiC1UJrqV3uuy861HCTo708pDMbjHHdCas=
|
||||
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d/go.mod h1:6QX/PXZ00z/TKoufEY6K/a0k6AhaJrQKdFe6OfVXsa4=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/chainguard-dev/git-urls v1.0.2 h1:pSpT7ifrpc5X55n4aTTm7FFUE+ZQHKiqpiwNkJrVcKQ=
|
||||
github.com/chainguard-dev/git-urls v1.0.2/go.mod h1:rbGgj10OS7UgZlbzdUQIQpT0k/D4+An04HJY7Ol+Y/o=
|
||||
github.com/charmbracelet/colorprofile v0.3.3 h1:DjJzJtLP6/NZ8p7Cgjno0CKGr7wwRJGxWUwh2IyhfAI=
|
||||
github.com/charmbracelet/colorprofile v0.3.3/go.mod h1:nB1FugsAbzq284eJcjfah2nhdSLppN2NqvfotkfRYP4=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38 h1:7Rs87fbKJoIIxsQS8YKJYGYa0tlsDwwb0twQjV1KB+g=
|
||||
github.com/charmbracelet/ultraviolet v0.0.0-20251116181749-377898bcce38/go.mod h1:6lfcr3MNP+kZR25sF1nQwJFuQnNYBlFy3PGX5rvslXc=
|
||||
github.com/charmbracelet/x/ansi v0.11.1 h1:iXAC8SyMQDJgtcz9Jnw+HU8WMEctHzoTAETIeA3JXMk=
|
||||
github.com/charmbracelet/x/ansi v0.11.1/go.mod h1:M49wjzpIujwPceJ+t5w3qh2i87+HRtHohgb5iTyepL0=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f h1:pk6gmGpCE7F3FcjaOEKYriCvpmIN4+6OS/RD0vm4uIA=
|
||||
github.com/charmbracelet/x/exp/golden v0.0.0-20250806222409-83e3a29d542f/go.mod h1:IfZAMTHB6XkZSeXUqriemErjAWCCzT0LwjKFYCZyw0I=
|
||||
github.com/charmbracelet/x/term v0.2.2 h1:xVRT/S2ZcKdhhOuSP4t5cLi5o+JxklsoEObBSgfgZRk=
|
||||
github.com/charmbracelet/x/term v0.2.2/go.mod h1:kF8CY5RddLWrsgVwpw4kAa6TESp6EB5y3uxGLeCqzAI=
|
||||
github.com/charmbracelet/x/termios v0.1.1 h1:o3Q2bT8eqzGnGPOYheoYS8eEleT5ZVNYNy8JawjaNZY=
|
||||
github.com/charmbracelet/x/termios v0.1.1/go.mod h1:rB7fnv1TgOPOyyKRJ9o+AsTU/vK5WHJ2ivHeut/Pcwo=
|
||||
github.com/charmbracelet/x/windows v0.2.2 h1:IofanmuvaxnKHuV04sC0eBy/smG6kIKrWG2/jYn2GuM=
|
||||
github.com/charmbracelet/x/windows v0.2.2/go.mod h1:/8XtdKZzedat74NQFn0NGlGL4soHB0YQZrETF96h75k=
|
||||
github.com/clipperhouse/displaywidth v0.5.0 h1:AIG5vQaSL2EKqzt0M9JMnvNxOCRTKUc4vUnLWGgP89I=
|
||||
github.com/clipperhouse/displaywidth v0.5.0/go.mod h1:R+kHuzaYWFkTm7xoMmK1lFydbci4X2CicfbGstSGg0o=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443 h1:aQ3y1lwWyqYPiWZThqv1aFbZMiM9vblcSArJRf2Irls=
|
||||
github.com/cncf/xds/go v0.0.0-20250501225837-2ac532fd4443/go.mod h1:W+zGtBO5Y1IgJhy4+A9GOqVhqLpfZi+vwmdNXUehLA8=
|
||||
github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
|
||||
@@ -164,14 +194,20 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag=
|
||||
github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2 h1:vGKWl0YJqUNxE8d+h8f6NJLcCJrgbhC4NcD46KavDd4=
|
||||
github.com/mitchellh/hashstructure/v2 v2.0.2/go.mod h1:MG3aRVU/N29oo/V/IhBX8GR/zz4kQkprJgF2EVszyDE=
|
||||
github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA=
|
||||
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
@@ -182,6 +218,8 @@ github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRI
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.3.0 h1:w/bWkEJdYuRNYhHn5eXnIT8LzDM1O629X1I9MJSkD7Q=
|
||||
github.com/puzpuzpuz/xsync/v4 v4.3.0/go.mod h1:VJDmTCJMBt8igNxnkQd86r+8KUeN1quSfNKu5bLYFQo=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sajari/fuzzy v1.0.0 h1:+FmwVvJErsd0d0hAPlj4CxqxUtQY/fOoY0DwX4ykpRY=
|
||||
@@ -208,6 +246,8 @@ github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8
|
||||
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||
github.com/ulikunitz/xz v0.5.15 h1:9DNdB5s+SgV3bQ2ApL10xRc35ck0DuIX/isZvIk+ubY=
|
||||
github.com/ulikunitz/xz v0.5.15/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/errs v1.4.0 h1:XNdoD/RRMKP7HD0UhJnIzUy74ISdGGxURlYG8HSWSfM=
|
||||
@@ -238,6 +278,8 @@ go.yaml.in/yaml/v4 v4.0.0-rc.3 h1:3h1fjsh1CTAPjW7q/EMe+C8shx5d8ctzZTrLcs/j8Go=
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.3/go.mod h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/oauth2 v0.33.0 h1:4Q+qn+E5z8gPRJfmRy7C2gGG3T4jIprK6aSYgTXGRpo=
|
||||
|
||||
@@ -83,6 +83,7 @@ var (
|
||||
Timeout time.Duration
|
||||
CacheExpiryDuration time.Duration
|
||||
RemoteCacheDir string
|
||||
Interactive bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -132,6 +133,7 @@ func init() {
|
||||
pflag.BoolVarP(&Silent, "silent", "s", false, "Disables echoing.")
|
||||
pflag.BoolVar(&DisableFuzzy, "disable-fuzzy", getConfig(config, func() *bool { return config.DisableFuzzy }, false), "Disables fuzzy matching for task names.")
|
||||
pflag.BoolVarP(&AssumeYes, "yes", "y", false, "Assume \"yes\" as answer to all prompts.")
|
||||
pflag.BoolVar(&Interactive, "interactive", getConfig(config, func() *bool { return config.Interactive }, false), "Prompt for missing required variables.")
|
||||
pflag.BoolVarP(&Parallel, "parallel", "p", false, "Executes tasks provided on command line in parallel.")
|
||||
pflag.BoolVarP(&Dry, "dry", "n", false, "Compiles and prints tasks in the order that they would be run, without executing them.")
|
||||
pflag.BoolVar(&Summary, "summary", false, "Show summary about a task.")
|
||||
@@ -281,6 +283,7 @@ func (o *flagsOption) ApplyToExecutor(e *task.Executor) {
|
||||
task.WithSilent(Silent),
|
||||
task.WithDisableFuzzy(DisableFuzzy),
|
||||
task.WithAssumeYes(AssumeYes),
|
||||
task.WithInteractive(Interactive),
|
||||
task.WithDry(Dry || Status),
|
||||
task.WithSummary(Summary),
|
||||
task.WithParallel(Parallel),
|
||||
|
||||
211
internal/input/input.go
Normal file
211
internal/input/input.go
Normal file
@@ -0,0 +1,211 @@
|
||||
package input
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"charm.land/bubbles/v2/textinput"
|
||||
tea "charm.land/bubbletea/v2"
|
||||
"charm.land/lipgloss/v2"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
)
|
||||
|
||||
var ErrCancelled = errors.New("prompt cancelled")
|
||||
|
||||
var (
|
||||
promptStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
|
||||
cursorStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("6")).Bold(true) // cyan bold
|
||||
selectedStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("2")).Bold(true) // green bold
|
||||
dimStyle = lipgloss.NewStyle().Foreground(lipgloss.Color("8")) // gray
|
||||
)
|
||||
|
||||
// Prompter handles interactive variable prompting
|
||||
type Prompter struct {
|
||||
Stdin io.Reader
|
||||
Stdout io.Writer
|
||||
Stderr io.Writer
|
||||
}
|
||||
|
||||
// Text prompts the user for a text value
|
||||
func (p *Prompter) Text(varName string) (string, error) {
|
||||
m := newTextModel(varName)
|
||||
|
||||
prog := tea.NewProgram(m,
|
||||
tea.WithInput(p.Stdin),
|
||||
tea.WithOutput(p.Stderr),
|
||||
)
|
||||
|
||||
result, err := prog.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
model := result.(textModel)
|
||||
if model.cancelled {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
|
||||
return model.value, nil
|
||||
}
|
||||
|
||||
// Select prompts the user to select from a list of options
|
||||
func (p *Prompter) Select(varName string, options []string) (string, error) {
|
||||
if len(options) == 0 {
|
||||
return "", errors.New("no options provided")
|
||||
}
|
||||
|
||||
m := newSelectModel(varName, options)
|
||||
|
||||
prog := tea.NewProgram(m,
|
||||
tea.WithInput(p.Stdin),
|
||||
tea.WithOutput(p.Stderr),
|
||||
)
|
||||
|
||||
result, err := prog.Run()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
model := result.(selectModel)
|
||||
if model.cancelled {
|
||||
return "", ErrCancelled
|
||||
}
|
||||
|
||||
return model.options[model.cursor], nil
|
||||
}
|
||||
|
||||
// Prompt prompts for a variable value, using Select if enum is provided, Text otherwise
|
||||
func (p *Prompter) Prompt(varName string, enum []string) (string, error) {
|
||||
if len(enum) > 0 {
|
||||
return p.Select(varName, enum)
|
||||
}
|
||||
return p.Text(varName)
|
||||
}
|
||||
|
||||
// textModel is the Bubble Tea model for text input
|
||||
type textModel struct {
|
||||
varName string
|
||||
textInput textinput.Model
|
||||
value string
|
||||
cancelled bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newTextModel(varName string) textModel {
|
||||
ti := textinput.New()
|
||||
ti.Placeholder = ""
|
||||
ti.CharLimit = 256
|
||||
ti.SetWidth(40)
|
||||
ti.Focus()
|
||||
|
||||
return textModel{
|
||||
varName: varName,
|
||||
textInput: ti,
|
||||
}
|
||||
}
|
||||
|
||||
func (m textModel) Init() tea.Cmd {
|
||||
return tea.Batch(m.textInput.Focus(), textinput.Blink)
|
||||
}
|
||||
|
||||
func (m textModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.Keystroke() {
|
||||
case "ctrl+c", "escape":
|
||||
m.cancelled = true
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
case "enter":
|
||||
m.value = m.textInput.Value()
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
var cmd tea.Cmd
|
||||
m.textInput, cmd = m.textInput.Update(msg)
|
||||
return m, cmd
|
||||
}
|
||||
|
||||
func (m textModel) View() tea.View {
|
||||
if m.done {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
prompt := promptStyle.Render(fmt.Sprintf("? Enter value for %s: ", m.varName))
|
||||
return tea.NewView(prompt + m.textInput.View() + "\n")
|
||||
}
|
||||
|
||||
// selectModel is the Bubble Tea model for selection
|
||||
type selectModel struct {
|
||||
varName string
|
||||
options []string
|
||||
cursor int
|
||||
cancelled bool
|
||||
done bool
|
||||
}
|
||||
|
||||
func newSelectModel(varName string, options []string) selectModel {
|
||||
return selectModel{
|
||||
varName: varName,
|
||||
options: options,
|
||||
cursor: 0,
|
||||
}
|
||||
}
|
||||
|
||||
func (m selectModel) Init() tea.Cmd {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m selectModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
|
||||
switch msg := msg.(type) {
|
||||
case tea.KeyPressMsg:
|
||||
switch msg.Keystroke() {
|
||||
case "ctrl+c", "escape":
|
||||
m.cancelled = true
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
case "up", "shift+tab", "k":
|
||||
if m.cursor > 0 {
|
||||
m.cursor--
|
||||
}
|
||||
case "down", "tab", "j":
|
||||
if m.cursor < len(m.options)-1 {
|
||||
m.cursor++
|
||||
}
|
||||
case "enter":
|
||||
m.done = true
|
||||
return m, tea.Quit
|
||||
}
|
||||
}
|
||||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (m selectModel) View() tea.View {
|
||||
if m.done {
|
||||
return tea.NewView("")
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
|
||||
b.WriteString(promptStyle.Render(fmt.Sprintf("? Select value for %s:", m.varName)))
|
||||
b.WriteString("\n")
|
||||
|
||||
for i, opt := range m.options {
|
||||
if i == m.cursor {
|
||||
b.WriteString(cursorStyle.Render("❯ "))
|
||||
b.WriteString(selectedStyle.Render(opt))
|
||||
} else {
|
||||
b.WriteString(" " + opt)
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
b.WriteString(dimStyle.Render(" (↑/↓ to move, enter to select, esc to cancel)"))
|
||||
|
||||
return tea.NewView(b.String())
|
||||
}
|
||||
174
requires.go
174
requires.go
@@ -4,35 +4,180 @@ import (
|
||||
"slices"
|
||||
|
||||
"github.com/go-task/task/v3/errors"
|
||||
"github.com/go-task/task/v3/internal/input"
|
||||
"github.com/go-task/task/v3/internal/term"
|
||||
"github.com/go-task/task/v3/taskfile/ast"
|
||||
)
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
|
||||
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
func (e *Executor) canPrompt() bool {
|
||||
return e.Interactive && (e.AssumeTerm || term.IsTerminal())
|
||||
}
|
||||
|
||||
func (e *Executor) newPrompter() *input.Prompter {
|
||||
return &input.Prompter{
|
||||
Stdin: e.Stdin,
|
||||
Stdout: e.Stdout,
|
||||
Stderr: e.Stderr,
|
||||
}
|
||||
}
|
||||
|
||||
// promptDepsVars traverses the dependency tree, collects all missing required
|
||||
// variables, and prompts for them upfront. This is used for deps which execute
|
||||
// in parallel, so all prompts must happen before execution to avoid interleaving.
|
||||
// Prompted values are stored in e.promptedVars for injection into task calls.
|
||||
func (e *Executor) promptDepsVars(calls []*Call) error {
|
||||
if !e.canPrompt() {
|
||||
return nil
|
||||
}
|
||||
|
||||
var missingVars []errors.MissingVar
|
||||
for _, requiredVar := range t.Requires.Vars {
|
||||
_, ok := t.Vars.Get(requiredVar.Name)
|
||||
if !ok {
|
||||
missingVars = append(missingVars, errors.MissingVar{
|
||||
Name: requiredVar.Name,
|
||||
AllowedValues: requiredVar.Enum,
|
||||
})
|
||||
// Collect all missing vars from the dependency tree
|
||||
visited := make(map[string]bool)
|
||||
varsMap := make(map[string]*ast.VarsWithValidation)
|
||||
|
||||
var collect func(call *Call) error
|
||||
collect = func(call *Call) error {
|
||||
compiledTask, err := e.FastCompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, v := range getMissingRequiredVars(compiledTask) {
|
||||
if _, exists := varsMap[v.Name]; !exists {
|
||||
varsMap[v.Name] = v
|
||||
}
|
||||
}
|
||||
|
||||
// Check visited AFTER collecting vars to handle duplicate task calls with different vars
|
||||
if visited[call.Task] {
|
||||
return nil
|
||||
}
|
||||
visited[call.Task] = true
|
||||
|
||||
for _, dep := range compiledTask.Deps {
|
||||
depCall := &Call{
|
||||
Task: dep.Task,
|
||||
Vars: dep.Vars,
|
||||
Silent: dep.Silent,
|
||||
}
|
||||
if err := collect(depCall); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, call := range calls {
|
||||
if err := collect(call); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if len(missingVars) > 0 {
|
||||
return &errors.TaskMissingRequiredVarsError{
|
||||
TaskName: t.Name(),
|
||||
MissingVars: missingVars,
|
||||
if len(varsMap) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
prompter := e.newPrompter()
|
||||
e.promptedVars = ast.NewVars()
|
||||
|
||||
for _, v := range varsMap {
|
||||
value, err := prompter.Prompt(v.Name, v.Enum)
|
||||
if err != nil {
|
||||
if errors.Is(err, input.ErrCancelled) {
|
||||
return &errors.TaskCancelledByUserError{TaskName: "interactive prompt"}
|
||||
}
|
||||
return err
|
||||
}
|
||||
e.promptedVars.Set(v.Name, ast.Var{Value: value})
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// promptTaskVars prompts for any missing required vars from a single task.
|
||||
// Used for sequential task calls (cmds) where we can prompt just-in-time.
|
||||
// Returns true if any vars were prompted (caller should recompile the task).
|
||||
func (e *Executor) promptTaskVars(t *ast.Task, call *Call) (bool, error) {
|
||||
if !e.canPrompt() || t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// Find missing vars, excluding already prompted ones
|
||||
var missing []*ast.VarsWithValidation
|
||||
for _, v := range getMissingRequiredVars(t) {
|
||||
if e.promptedVars != nil {
|
||||
if _, ok := e.promptedVars.Get(v.Name); ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
missing = append(missing, v)
|
||||
}
|
||||
|
||||
if len(missing) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
prompter := e.newPrompter()
|
||||
|
||||
for _, v := range missing {
|
||||
value, err := prompter.Prompt(v.Name, v.Enum)
|
||||
if err != nil {
|
||||
if errors.Is(err, input.ErrCancelled) {
|
||||
return false, &errors.TaskCancelledByUserError{TaskName: t.Name()}
|
||||
}
|
||||
return false, err
|
||||
}
|
||||
|
||||
// Add to call.Vars for recompilation
|
||||
if call.Vars == nil {
|
||||
call.Vars = ast.NewVars()
|
||||
}
|
||||
call.Vars.Set(v.Name, ast.Var{Value: value})
|
||||
|
||||
// Cache for reuse by other tasks
|
||||
if e.promptedVars == nil {
|
||||
e.promptedVars = ast.NewVars()
|
||||
}
|
||||
e.promptedVars.Set(v.Name, ast.Var{Value: value})
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// getMissingRequiredVars returns required vars that are not set in the task's vars.
|
||||
func getMissingRequiredVars(t *ast.Task) []*ast.VarsWithValidation {
|
||||
if t.Requires == nil {
|
||||
return nil
|
||||
}
|
||||
var missing []*ast.VarsWithValidation
|
||||
for _, v := range t.Requires.Vars {
|
||||
if _, ok := t.Vars.Get(v.Name); !ok {
|
||||
missing = append(missing, v)
|
||||
}
|
||||
}
|
||||
return missing
|
||||
}
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsSet(t *ast.Task) error {
|
||||
missing := getMissingRequiredVars(t)
|
||||
if len(missing) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
missingVars := make([]errors.MissingVar, len(missing))
|
||||
for i, v := range missing {
|
||||
missingVars[i] = errors.MissingVar{
|
||||
Name: v.Name,
|
||||
AllowedValues: v.Enum,
|
||||
}
|
||||
}
|
||||
|
||||
return &errors.TaskMissingRequiredVarsError{
|
||||
TaskName: t.Name(),
|
||||
MissingVars: missingVars,
|
||||
}
|
||||
}
|
||||
|
||||
func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
||||
if t.Requires == nil || len(t.Requires.Vars) == 0 {
|
||||
return nil
|
||||
@@ -50,7 +195,6 @@ func (e *Executor) areTaskRequiredVarsAllowedValuesSet(t *ast.Task) error {
|
||||
Name: requiredVar.Name,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if len(notAllowedValuesVars) > 0 {
|
||||
|
||||
31
task.go
31
task.go
@@ -74,6 +74,11 @@ func (e *Executor) Run(ctx context.Context, calls ...*Call) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Prompt for all required vars from deps upfront (parallel execution)
|
||||
if err := e.promptDepsVars(calls); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
regularCalls, watchCalls, err := e.splitRegularAndWatchCalls(calls...)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -121,6 +126,19 @@ func (e *Executor) splitRegularAndWatchCalls(calls ...*Call) (regularCalls []*Ca
|
||||
|
||||
// RunTask runs a task by its name
|
||||
func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
// Inject prompted vars into call if available
|
||||
if e.promptedVars != nil {
|
||||
if call.Vars == nil {
|
||||
call.Vars = ast.NewVars()
|
||||
}
|
||||
for name, v := range e.promptedVars.All() {
|
||||
// Only inject if not already set in call
|
||||
if _, ok := call.Vars.Get(name); !ok {
|
||||
call.Vars.Set(name, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
t, err := e.FastCompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -141,6 +159,19 @@ func (e *Executor) RunTask(ctx context.Context, call *Call) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Prompt for missing required vars (just-in-time for sequential task calls)
|
||||
prompted, err := e.promptTaskVars(t, call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if prompted {
|
||||
// Recompile with the new vars
|
||||
t, err = e.FastCompiledTask(call)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
if err := e.areTaskRequiredVarsSet(t); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -15,6 +15,7 @@ type TaskRC struct {
|
||||
Color *bool `yaml:"color"`
|
||||
DisableFuzzy *bool `yaml:"disable-fuzzy"`
|
||||
Concurrency *int `yaml:"concurrency"`
|
||||
Interactive *bool `yaml:"interactive"`
|
||||
Remote Remote `yaml:"remote"`
|
||||
Failfast bool `yaml:"failfast"`
|
||||
Experiments map[string]int `yaml:"experiments"`
|
||||
@@ -59,5 +60,6 @@ func (t *TaskRC) Merge(other *TaskRC) {
|
||||
t.Color = cmp.Or(other.Color, t.Color)
|
||||
t.DisableFuzzy = cmp.Or(other.DisableFuzzy, t.DisableFuzzy)
|
||||
t.Concurrency = cmp.Or(other.Concurrency, t.Concurrency)
|
||||
t.Interactive = cmp.Or(other.Interactive, t.Interactive)
|
||||
t.Failfast = cmp.Or(other.Failfast, t.Failfast)
|
||||
}
|
||||
|
||||
1
testdata/interactive_vars/.taskrc.yml
vendored
Normal file
1
testdata/interactive_vars/.taskrc.yml
vendored
Normal file
@@ -0,0 +1 @@
|
||||
interactive: true
|
||||
108
testdata/interactive_vars/Taskfile.yml
vendored
Normal file
108
testdata/interactive_vars/Taskfile.yml
vendored
Normal file
@@ -0,0 +1,108 @@
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
# Simple text input prompt
|
||||
greet:
|
||||
desc: Greet someone by name
|
||||
requires:
|
||||
vars:
|
||||
- NAME
|
||||
cmds:
|
||||
- echo "Hello, {{.NAME}}!"
|
||||
|
||||
# Enum selection (dropdown menu)
|
||||
deploy:
|
||||
desc: Deploy to an environment
|
||||
requires:
|
||||
vars:
|
||||
- name: ENVIRONMENT
|
||||
enum: [dev, staging, prod]
|
||||
cmds:
|
||||
- echo "Deploying to {{.ENVIRONMENT}}..."
|
||||
|
||||
# Multiple variables at once
|
||||
release:
|
||||
desc: Create a release with version and environment
|
||||
requires:
|
||||
vars:
|
||||
- VERSION
|
||||
- name: ENVIRONMENT
|
||||
enum: [dev, staging, prod]
|
||||
cmds:
|
||||
- echo "Releasing {{.VERSION}} to {{.ENVIRONMENT}}"
|
||||
|
||||
# Nested dependencies - all prompts happen upfront
|
||||
full-deploy:
|
||||
desc: Full deployment pipeline with nested deps
|
||||
deps:
|
||||
- task: build
|
||||
- task: test
|
||||
cmds:
|
||||
- task: deploy
|
||||
|
||||
build:
|
||||
requires:
|
||||
vars:
|
||||
- name: BUILD_MODE
|
||||
enum: [debug, release]
|
||||
cmds:
|
||||
- echo "Building in {{.BUILD_MODE}} mode..."
|
||||
|
||||
test:
|
||||
requires:
|
||||
vars:
|
||||
- name: TEST_SUITE
|
||||
enum: [unit, integration, e2e, all]
|
||||
cmds:
|
||||
- echo "Running {{.TEST_SUITE}} tests..."
|
||||
|
||||
# Variable already set - no prompt shown
|
||||
greet-world:
|
||||
desc: Greet the world (no prompt needed)
|
||||
vars:
|
||||
NAME: World
|
||||
requires:
|
||||
vars:
|
||||
- NAME
|
||||
cmds:
|
||||
- echo "Hello, {{.NAME}}!"
|
||||
|
||||
# Complex scenario with multiple levels
|
||||
pipeline:
|
||||
desc: Run the full CI/CD pipeline
|
||||
cmds:
|
||||
- task: setup
|
||||
- task: build
|
||||
- task: test
|
||||
- task: deploy
|
||||
|
||||
setup:
|
||||
requires:
|
||||
vars:
|
||||
- PROJECT_NAME
|
||||
cmds:
|
||||
- echo "Setting up project {{.PROJECT_NAME}}..."
|
||||
|
||||
# Docker example with multiple selections
|
||||
docker-build:
|
||||
desc: Build a Docker image
|
||||
requires:
|
||||
vars:
|
||||
- IMAGE_NAME
|
||||
- IMAGE_TAG
|
||||
- name: PLATFORM
|
||||
enum: [linux/amd64, linux/arm64, linux/arm/v7]
|
||||
cmds:
|
||||
- echo "Building {{.IMAGE_NAME}}:{{.IMAGE_TAG}} for {{.PLATFORM}}"
|
||||
|
||||
# Database migration example
|
||||
db-migrate:
|
||||
desc: Run database migrations
|
||||
requires:
|
||||
vars:
|
||||
- name: DIRECTION
|
||||
enum: [up, down]
|
||||
- name: DATABASE
|
||||
enum: [postgres, mysql, sqlite]
|
||||
cmds:
|
||||
- echo "Running {{.DIRECTION}} migrations on {{.DATABASE}}"
|
||||
@@ -1233,6 +1233,65 @@ This is supported only for string variables.
|
||||
|
||||
:::
|
||||
|
||||
### Prompting for missing variables interactively
|
||||
|
||||
If you want Task to prompt users for missing required variables instead of
|
||||
failing, you can enable interactive mode in your `.taskrc.yml`:
|
||||
|
||||
```yaml
|
||||
# ~/.taskrc.yml
|
||||
interactive: true
|
||||
```
|
||||
|
||||
When enabled, Task will display an interactive prompt for any missing required
|
||||
variable. For variables with an `enum`, a selection menu is shown. For variables
|
||||
without an enum, a text input is displayed.
|
||||
|
||||
```yaml
|
||||
# Taskfile.yml
|
||||
version: '3'
|
||||
|
||||
tasks:
|
||||
deploy:
|
||||
requires:
|
||||
vars:
|
||||
- name: ENVIRONMENT
|
||||
enum: [dev, staging, prod]
|
||||
- VERSION
|
||||
cmds:
|
||||
- echo "Deploying {{.VERSION}} to {{.ENVIRONMENT}}"
|
||||
```
|
||||
|
||||
```shell
|
||||
$ task deploy
|
||||
? Select value for ENVIRONMENT:
|
||||
❯ dev
|
||||
staging
|
||||
prod
|
||||
? Enter value for VERSION: 1.0.0
|
||||
Deploying 1.0.0 to prod
|
||||
```
|
||||
|
||||
If the variable is already set (via CLI, environment, or Taskfile), no prompt
|
||||
is shown:
|
||||
|
||||
```shell
|
||||
$ task deploy ENVIRONMENT=prod VERSION=1.0.0
|
||||
Deploying 1.0.0 to prod
|
||||
```
|
||||
|
||||
::: info
|
||||
|
||||
Interactive prompts require a TTY (terminal). Task automatically detects
|
||||
non-interactive environments like GitHub Actions, GitLab CI, and other CI
|
||||
pipelines where stdin/stdout are not connected to a terminal. In these cases,
|
||||
prompts are skipped and missing variables will cause an error as usual.
|
||||
|
||||
You can enable prompts from the command line with `--interactive` or by setting
|
||||
`interactive: true` in your `.taskrc.yml`.
|
||||
|
||||
:::
|
||||
|
||||
## Variables
|
||||
|
||||
Task allows you to set variables using the `vars` keyword. The following
|
||||
|
||||
@@ -301,6 +301,19 @@ Automatically answer "yes" to all prompts.
|
||||
task deploy --yes
|
||||
```
|
||||
|
||||
#### `--interactive`
|
||||
|
||||
Enable interactive prompts for missing required variables. When a required
|
||||
variable is not provided, Task will prompt for input instead of failing.
|
||||
|
||||
Task automatically detects non-TTY environments (like CI pipelines) and
|
||||
skips prompts. This flag can also be set in `.taskrc.yml` to enable prompts
|
||||
by default.
|
||||
|
||||
```bash
|
||||
task deploy --interactive
|
||||
```
|
||||
|
||||
## Exit Codes
|
||||
|
||||
Task uses specific exit codes to indicate different types of errors:
|
||||
|
||||
@@ -135,6 +135,20 @@ concurrency: 4
|
||||
failfast: true
|
||||
```
|
||||
|
||||
### `interactive`
|
||||
|
||||
- **Type**: `boolean`
|
||||
- **Default**: `false`
|
||||
- **Description**: Prompt for missing required variables instead of failing.
|
||||
When enabled, Task will display an interactive prompt for any missing required
|
||||
variable. Requires a TTY. Task automatically detects non-TTY environments
|
||||
(CI pipelines, etc.) and skips prompts.
|
||||
- **CLI equivalent**: [`--interactive`](./cli.md#--interactive)
|
||||
|
||||
```yaml
|
||||
interactive: true
|
||||
```
|
||||
|
||||
## Example Configuration
|
||||
|
||||
Here's a complete example of a `.taskrc.yml` file with all available options:
|
||||
|
||||
@@ -655,7 +655,7 @@ tasks:
|
||||
#### `requires`
|
||||
|
||||
- **Type**: `Requires`
|
||||
- **Description**: Required variables with optional enums
|
||||
- **Description**: Required variables with optional enum validation
|
||||
|
||||
```yaml
|
||||
tasks:
|
||||
@@ -680,6 +680,9 @@ tasks:
|
||||
- ./deploy.sh
|
||||
```
|
||||
|
||||
See [Prompting for missing variables interactively](/docs/guide#prompting-for-missing-variables-interactively)
|
||||
for information on enabling interactive prompts for missing required variables.
|
||||
|
||||
#### `watch`
|
||||
|
||||
- **Type**: `bool`
|
||||
|
||||
@@ -78,6 +78,11 @@
|
||||
"description": "When running tasks in parallel, stop all tasks if one fails.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"interactive": {
|
||||
"description": "Prompt for missing required variables instead of failing. Requires a TTY.",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
||||
@@ -635,7 +635,7 @@
|
||||
"name": { "type": "string" },
|
||||
"enum": { "type": "array", "items": { "type": "string" } }
|
||||
},
|
||||
"required": ["name", "enum"],
|
||||
"required": ["name"],
|
||||
"additionalProperties": false
|
||||
}
|
||||
]
|
||||
|
||||
Reference in New Issue
Block a user