| name | shell |
| description | 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. |
| disable-model-invocation | false |
Invowk 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/virtual.go - Virtual shell runtime
cmd/invowk/internal_exec_virtual.go - Virtual execution command
- Shell script execution logic
Virtual Shell (mvdan/sh)
The virtual 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:
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:
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 -:
set -- -v --env=staging
Affected Locations
When working with mvdan/sh in this codebase, ensure "--" is prepended in:
internal/runtime/virtual.go - Execute() method
internal/runtime/virtual.go - ExecuteCapture() method
cmd/invowk/internal_exec_virtual.go - runInternalExecVirtual() function
Testing
This issue manifests on Windows CI because the virtual runtime is the only bash-compatible option there. When adding new mvdan/sh integration points:
- Test with arguments starting with
- (e.g., -v, --flag=value)
- Run
make test-cli to verify flag handling works
- Ensure Windows CI passes
Bash Script Execution
Strict Mode (set -euo pipefail)
All bash scripts in this project use strict mode for safety:
set -euo pipefail
This enables:
-e (errexit): Exit on any command failure
-u (nounset): Error on undefined variables
-o pipefail: Propagate errors through pipes
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:
COUNTER=0
((COUNTER++))
Safe Arithmetic Patterns
Use assignment syntax instead of increment operators:
COUNTER=$((COUNTER + 1))
let COUNTER++ || true
: $((COUNTER++))
Anti-Patterns to Avoid
((COUNTER++))
((FAILED++))
((SKIPPED++))
Real-World Example
The VHS test scripts use counters for PASSED, FAILED, and SKIPPED tests:
SKIPPED=0
if [[ ! -f "$golden_file" ]]; then
((SKIPPED++))
continue
fi
SKIPPED=0
if [[ ! -f "$golden_file" ]]; then
SKIPPED=$((SKIPPED + 1))
continue
fi
Common Pitfalls
- Missing
"--" delimiter - Always use append([]string{"--"}, args...) when calling interp.Params() with user-provided positional arguments.
- Arithmetic with
set -e - Always use VAR=$((VAR + 1)) instead of ((VAR++)) when VAR might be 0.