shell
Shell runtime rules for mvdan/sh virtual shell in internal/runtime/virtual.go. Covers positional arguments gotcha (prepend "--"), bash strict mode (set -euo pipefail), arithmetic increment pitfalls.
SKILL.md.claude/skills/shell
GitHubInvowk Shell Runtime Rules
This skill covers how Invowk handles shell interpreters and script execution internally. NOT general bash scripting guidance.
Use this skill when working on:
internal/runtime/sh.go- virtual-sh runtimecmd/invowk/internal_exec_sh.go- virtual-sh execution commandinternal/app/commandadapters/sh_interactive.goand shared virtual interactive helpers- Repository bash script execution logic
Virtual Shell (mvdan/sh)
The virtual-sh runtime uses mvdan/sh, a pure Go POSIX shell interpreter. This provides cross-platform bash-like script execution without requiring an external shell binary.
Positional Arguments Gotcha
CRITICAL: Always prepend "--" when passing positional arguments to interp.Params().
The interp.Params() function configures shell parameters, but it follows POSIX shell conventions where arguments starting with - are interpreted as shell options (like -e, -u, -x).
The Problem
Without "--", positional arguments like -v or --env=staging are interpreted as shell options:
// WRONG: Will fail with "invalid option: -v"
if len(args) > 0 {
opts = append(opts, interp.Params(args...))
}
Error messages you might see:
failed to create interpreter: invalid option: "-v"failed to create interpreter: invalid option: "--"failed to create interpreter: invalid option: "--env=staging"
The Solution
Prepend "--" to signal the end of options:
// CORRECT: "--" signals end of options, remaining args become $1, $2, etc.
if len(args) > 0 {
params := append([]string{"--"}, args...)
opts = append(opts, interp.Params(params...))
}
Why This Works
In POSIX shells, "--" is the standard delimiter that terminates option parsing. Everything after "--" is treated as a positional parameter, regardless of whether it starts with -:
# Shell equivalent:
set -- -v --env=staging # Sets $1="-v", $2="--env=staging"
Affected Locations
When working with mvdan/sh in this codebase, ensure "--" is prepended in:
internal/runtime/sh.go-prepareShExec()for normal virtual-sh executioninternal/runtime/sh.go-RunShScript()for the internal exec-sh path
internal/app/commandadapters/sh_interactive.go transports arguments through
--args; verify that transport stays aligned with the runtime-side
interp.Params delimiter, but do not copy the delimiter pattern there unless it
is directly calling mvdan/sh.
Testing
This issue manifests on Windows CI because virtual-sh is the bash-compatible embedded shell option there. When adding new mvdan/sh integration points:
- Test with arguments starting with
-(e.g.,-v,--flag=value) - Run
go test ./internal/runtime -run TestShRuntime_PositionalArgs_DashPrefix -count=1 - Run relevant interactive adapter tests when
internal/app/commandadapters/*sh*changes - Run
make test-cliwhen CLI behavior or txtar fixtures changed
Bash Script Execution
Strict Mode (set -euo pipefail)
Project-level bash entrypoints should use strict mode for safety. Short sourced
helpers that only emit shell fragments, such as Bencher helper scripts, may omit
it deliberately. Note: This applies to repository bash scripts, not to CUE
command scripts executed via container runtimes (/bin/sh -c). For container
script behavior, see the testing skill's "Shell Script Behavior in Containers"
section.
set -euo pipefail
This exits on command failures, undefined variables, and failed pipeline
segments. Verify changed repo bash scripts with bash -n <script> and
make lint-scripts when applicable.
Arithmetic Increment Gotcha
CRITICAL: Never use ((var++)) with set -e when var might be 0.
In bash, arithmetic expressions return exit status based on the expression's value:
((0))returns exit status 1 (false)((1))returns exit status 0 (true)
The post-increment ((x++)) evaluates to the original value of x:
# DANGEROUS with set -e:
COUNTER=0
((COUNTER++)) # Evaluates to 0 (the original value), exits with status 1!
# Script terminates here due to set -e
Safe Arithmetic Patterns
Use assignment syntax instead of increment operators:
# CORRECT: Assignment always succeeds
COUNTER=$((COUNTER + 1))
Anti-Patterns to Avoid
# WRONG: Will fail if COUNTER is 0
((COUNTER++))
# WRONG: Will fail if FAILED is 0
((FAILED++))
# WRONG: Will fail if SKIPPED is 0
((SKIPPED++))
Real-World Example
Shell validation scripts commonly use counters for PASSED, FAILED, and SKIPPED checks:
# WRONG - Script exits on first skip when SKIPPED is 0:
SKIPPED=0
if [[ ! -f "$golden_file" ]]; then
((SKIPPED++)) # Exit status 1, script terminates!
continue
fi
# CORRECT - Assignment syntax always works:
SKIPPED=0
if [[ ! -f "$golden_file" ]]; then
SKIPPED=$((SKIPPED + 1)) # Always succeeds
continue
fi
Common Pitfalls
- Missing
"--"delimiter - Always useappend([]string{"--"}, args...)when callinginterp.Params()with user-provided positional arguments. - Arithmetic with
set -e- Always useVAR=$((VAR + 1))instead of((VAR++))whenVARmight be 0.