Skip to content

Task panics (index out of range) when pipefail is placed in shopt instead of set #2429

Description

@posidoni

Description

Thanks for Task — I've introduced it to teams at several companies as a make replacement for Go projects.

Placing pipefail into the shopt: list (instead of set:, where it belongs) makes task crash with panic: runtime error: index out of range [-1] instead of reporting a helpful error.

Minimal reproduction — both of these Taskfiles panic:

set:
  - pipefail
  - nounset
  - errexit
shopt:
  - pipefail
# set: may be empty or absent
shopt:
  - pipefail

Actual behavior: panic with a Go runtime stack trace (full output below).

Expected behavior: a clear, friendly error explaining what is wrong — ideally suggesting that pipefail belongs in set:, or at least the same graceful failure that other invalid shopt values already produce:

task: [default] echo 'I will not work'
shopt: invalid option name "abcd"
task: Failed to run task "default": exit status 1

pipefail is special because it is a valid set -o option, which hits a different code path in the interpreter: https://github.com/mvdan/sh/blob/b717ad599e17a6975e15a67ed8937bd8d4909146/interp/builtin.go#L793. The root cause therefore appears to be in the mvdan/sh library, but I think Task should handle it on its side as well — for example by validating set:/shopt: values when the Taskfile is read.

A related question: would it make sense to recover around the interpreter call, so that a library panic surfaces as a regular task error instead of a crash? I have seen panics from the interpreter in several versions. (Discussed below — validation was preferred over recover.)

Full panic output (task 3.45.4, macOS, Apple Silicon)
> task -v
task: "default" started
task: [default] nginx -p "/Users/posidoni/powx/cicd/nginx" -c ./nginx.conf -T
panic: runtime error: index out of range [-1]

goroutine 1 gp=0x140000021c0 m=10 mp=0x14000386008 [running]:
panic({0x101100d60?, 0x1400051a150?})
        /opt/homebrew/opt/go/libexec/src/runtime/panic.go:802 +0x150 fp=0x14000413a50 sp=0x140004139a0 pc=0x10068c4b0
runtime.goPanicIndex(0xffffffffffffffff, 0x36)
        /opt/homebrew/opt/go/libexec/src/runtime/panic.go:115 +0x70 fp=0x14000413a90 sp=0x14000413a50 pc=0x100654910
mvdan.cc/sh/v3/interp.(*Runner).builtin(0x14000295b88, {0x10115bdb0, 0x1400017c780}, {0x0, 0x4001}, {0x140004bc620, 0x5}, {0x140003d2550, 0x2, 0x3})
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/builtin.go:729 +0x4d70 fp=0x14000414bb0 sp=0x14000413a90 pc=0x1008369e0
mvdan.cc/sh/v3/interp.(*Runner).call(0x14000295b88, {0x10115bdb0, 0x1400017c780}, {0x3d2550?, 0x140?}, {0x140003d2540, 0x3, 0x4})
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/runner.go:1031 +0x5a4 fp=0x14000414ca0 sp=0x14000414bb0 pc=0x100841414
mvdan.cc/sh/v3/interp.(*Runner).cmd(0x14000295b88, {0x10115bdb0, 0x1400017c780}, {0x10115ab90, 0x14000312870})
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/runner.go:458 +0x1c5c fp=0x140004151f0 sp=0x14000414ca0 pc=0x10083ebcc
mvdan.cc/sh/v3/interp.(*Runner).stmtSync(0x14000295b88, {0x10115bdb0, 0x1400017c780}, 0x14000280180)
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/runner.go:340 +0x158 fp=0x14000415290 sp=0x140004151f0 pc=0x10083cdc8
mvdan.cc/sh/v3/interp.(*Runner).stmt(0x14000295b88, {0x10115bdb0, 0x1400017c780}, 0x14000280180)
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/runner.go:322 +0x22c fp=0x14000415310 sp=0x14000415290 pc=0x10083cb5c
mvdan.cc/sh/v3/interp.(*Runner).stmts(...)
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/runner.go:829
mvdan.cc/sh/v3/interp.(*Runner).Run(0x14000295b88, {0x10115bdb0, 0x1400017c780}, {0x1011583f8, 0x140003d24c0})
        /Users/posidoni/.cache/go/modcache/mvdan.cc/sh/v3@v3.12.0/interp/api.go:901 +0x32c fp=0x140004153b0 sp=0x14000415310 pc=0x10083129c
github.com/go-task/task/v3/internal/execext.RunCommand({0x10115bdb0, 0x1400017c780}, 0x140004157b8)
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/internal/execext/exec.go:80 +0x5a4 fp=0x140004155b0 sp=0x140004153b0 pc=0x1009dd024
github.com/go-task/task/v3.(*Executor).runCommand(0x140001ca420, {0x10115bdb0, 0x1400017c780}, 0x14000332248, 0x140002b4f40?, 0x0?)
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/task.go:345 +0x578 fp=0x14000415860 sp=0x140004155b0 pc=0x100c1faa8
github.com/go-task/task/v3.(*Executor).RunTask.func1({0x10115bdb0, 0x1400017c780})
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/task.go:217 +0x680 fp=0x14000415a20 sp=0x14000415860 pc=0x100c1e770
github.com/go-task/task/v3.(*Executor).startExecution(0x140001ca420, {0x10115bdb0, 0x1400017c780}, 0x1006302bc?, 0x14000415b80)
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/task.go:376 +0x348 fp=0x14000415af0 sp=0x14000415a20 pc=0x100c201e8
github.com/go-task/task/v3.(*Executor).RunTask(0x140001ca420, {0x10115bdb0, 0x1400017c780}, 0x140002b4f40)
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/task.go:153 +0x188 fp=0x14000415bb0 sp=0x14000415af0 pc=0x100c1dee8
github.com/go-task/task/v3.(*Executor).Run(0x140001ca420, {0x10115bd40, 0x1016b40a0}, {0x14000325d88, 0x1, 0x4})
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/task.go:87 +0x314 fp=0x14000415c70 sp=0x14000415bb0 pc=0x100c1d9f4
main.run()
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/cmd/task/task.go:181 +0x740 fp=0x14000415e90 sp=0x14000415c70 pc=0x100c2cb30
main.main()
        /Users/posidoni/.cache/go/modcache/github.com/go-task/task/v3@v3.45.4/cmd/task/task.go:23 +0x20 fp=0x14000415f40 sp=0x14000415e90 pc=0x100c2c110
runtime.main()
        /opt/homebrew/opt/go/libexec/src/runtime/proc.go:285 +0x278 fp=0x14000415fd0 sp=0x14000415f40 pc=0x100659918
runtime.goexit({})

Resolution: the panic itself was a bug in mvdan/sh, confirmed and fixed the same day in mvdan/sh@6bd6eb6, and the fix ships with current Task releases — a misplaced pipefail now fails with a regular shopt: invalid option name "pipefail" error at runtime. Validating set:/shopt: values when the Taskfile is read (with a hint to move the option to the right list) was favored by the maintainers over recover — implemented in the follow-up PR #2911.

Version

3.45.4

Operating system

macOS (Apple Silicon)

Experiments Enabled

No response

Example Taskfile

---
version: '3.40'

set:
  - e
  - u
shopt:
  - pipefail # <- the misplaced option; it belongs in `set:`

tasks:
    default:
      cmds:
        - echo 123

Metadata

Metadata

Assignees

No one assigned

    Labels

    dep: mvdan/shIssues related to the upstream interpreter used by Task.

    Type

    Fields

    No fields configured for bug.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions