| name | bin-script-scaffold |
| description | WHAT: Scaffold a new bin script in this dotfiles repo with the required sibling files: the script itself, a bash completion file, and a bats test. WHEN: User asks to add a new git subcommand, utility script, or any other executable under dotenv/bin or a platform-specific bin directory. DO-NOT: Skip the completion file or the bats test stub; invent a new argument-parsing style for flag-taking scripts; or add scripts to external/ or to private `__`-prefixed paths without confirming intent. |
Bin script scaffold
New executables in this repo ship as a trio: the script under a bin/ directory, a sibling bash completion file under
the neighboring completion/ directory, and a bats test mirroring the script path under tests/. All three land in the
same commit.
dotenv/bin/genpasswd is the canonical reference for argument parsing. Use it as the template; do not invent a new
style.
When to use this skill
Apply this skill when the user asks to:
- Add a new utility script under
dotenv/bin/ or a platform bin directory (dotenv/darwin/bin/, dotenv/linux/bin/,
dotenv/wsl/bin/, dotenv/ssh/bin/).
- Add a git subcommand (
dotenv/bin/git-* or platform variant), invoked as git <name> once on $PATH.
- Promote an inline shell snippet or alias into a standalone executable.
Skip this skill when the user wants:
- A private internal helper (
__-prefixed). Those do not need a completion file. Confirm the user actually wants a
private script before scaffolding without one.
- A shell function or alias rather than a standalone executable. Functions and aliases live under
dotenv/functions /
dotenv/aliases, not bin/.
File trio
For a script named foo placed at dotenv/bin/foo:
| File | Path |
|---|
| Script | dotenv/bin/foo |
| Completion | dotenv/completion/foo.bash |
| Test | tests/dotenv/bin/foo.bats |
For a script under a platform bin, the completion and test follow the same parallel layout, e.g. dotenv/wsl/bin/foo
pairs with dotenv/wsl/completion/foo.bash and tests/dotenv/wsl/bin/foo.bats.
Script shape
Every script starts with:
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
Helper function names default to plain identifiers (print_help, parse_args). Use a script-specific prefix only when
it materially improves clarity or avoids collisions during test sourcing.
Argument parsing
Two patterns; pick by what the script accepts.
Help-only (no other flags)
A leading case on $1 short-circuits help, then the rest of the script runs.
case "${1:-}" in
-h | -\? | --help)
print_help
exit 0
;;
esac
Multiple options
A while loop over $@ with explicit cases. Support both --flag value and --flag=value, plus short forms like
-l5 for single-letter flags. Print an error to stderr and exit 1 on unknown args, missing flag values, or
input-validation failures (non-numeric where numeric expected, empty required value, etc.). Always exit 0 after
print_help. Error messages should be prefixed with the script name, e.g. foo: missing value for $1.
while [[ $# -gt 0 ]]; do
case "$1" in
-h | -\? | --help)
print_help
exit 0
;;
-l | --length | --len)
if [[ $# -lt 2 ]]; then
echo "foo: missing value for $1" >&2
exit 1
fi
LENGTH="$2"
shift
;;
-l?*)
LENGTH="${1#-l}"
;;
--length=* | --len=*)
LENGTH="${1#*=}"
;;
*)
echo "foo: unknown argument: $1" >&2
exit 1
;;
esac
shift
done
dotenv/bin/genpasswd shows the full pattern in context.
Completion file
Every public script ships with a sibling completion. Name the file <script>.bash and define a _dot_<name> function
wired up with complete -F. The function name must be a valid bash identifier, so for hyphenated scripts replace the
hyphen with an underscore: git-recent becomes _dot_git_recent. The complete -F invocation still uses the original
script name (complete -F _dot_git_recent git-recent).
For scripts with positional args, flag values, or subcommand verbs, write a real completion function:
_dot_foo() {
local cur prev
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD - 1]}"
case "${prev}" in
-l | --length | --len)
return
;;
esac
if [[ "${cur}" == -* ]]; then
mapfile -t COMPREPLY < <(compgen -W "-l --length --len -h --help" -- "${cur}")
fi
}
complete -F _dot_foo foo
For flag-only or trivial wrappers, the complete -W shortcut is fine. Add -o default if file completion should still
kick in for non-flag words:
complete -W "--help --version" -o default foo
Private __-prefixed scripts do not need completion files.
Test stub
Tests mirror the script path: dotenv/bin/foo lands at tests/dotenv/bin/foo.bats. Test names start with the script
name and a colon. Source helpers from tests/helpers/common.bash. The two helpers you almost always need:
setup_test_bin - creates a temp bin/ prepended to PATH so subsequent stubs are visible.
stub_command <name> - reads a heredoc from stdin and writes a stub of <name> into the test bin. Use this to stub
external commands like git, curl, openssl so tests do not depend on the real binary's behavior or environment.
Both live in tests/helpers/common.bash; check that file for additional helpers (setup_isolated_home, prepend_path,
etc.) before reinventing them.
#!/usr/bin/env bats
setup() {
load '../../helpers/common'
setup_test_bin
SCRIPT="${REPO_ROOT}/dotenv/bin/foo"
}
@test "foo: -h and --help print usage" {
for flag in -h --help; do
run bash "${SCRIPT}" "${flag}"
assert_success
assert_output --partial "Usage: foo"
done
}
@test "foo: unknown argument exits 1" {
run bash "${SCRIPT}" --not-a-flag
assert_failure
assert_output --partial "unknown argument"
}
@test "foo: stubbed external command produces expected output" {
stub_command git <<'EOF'
printf '%s\n' 'branch-a' 'branch-b'
EOF
run bash "${SCRIPT}"
assert_success
assert_output --partial 'branch-a'
}
For internal-function unit tests, use source_without_main (see TESTING.md) instead of invoking the script as a
subprocess.
Workflow
- Confirm the placement:
dotenv/bin/ for cross-platform, dotenv/<platform>/bin/ for platform-specific. Confirm
whether the script should be public or private (__-prefixed).
- Write the script with the standard header,
set -euo pipefail, and the genpasswd-style argparse pattern that matches
its flag surface.
- Write the sibling completion file under the parallel
completion/ directory. Skip this only for __-prefixed
private scripts.
- Write the bats test under the mirrored path with at minimum a help-output test and an unknown-argument test. Add
behavior tests for the script's actual logic.
chmod +x the script.
- Update
REFERENCE.md under ## Commands on PATH > ### Utility commands, or ### Git subcommands for git-*
scripts, per the doc-sync skill.
- Run
./dev/lint-shell.sh and ./dev/test-bats-docker.sh tests/<path>/foo.bats before committing.
Common pitfalls
- Forgetting the completion file. Lint will not catch it; reviewers will.
- Inventing a new argparse style. Stick to the genpasswd pattern; consistency across the repo is the point.
- Missing
chmod +x. Bats tests will pass via bash "${SCRIPT}", but the script will not run from $PATH.
- Putting tests under
tests/bin/ instead of mirroring the source path. Tests must mirror.
- Skipping the
REFERENCE.md update; that step belongs to the same commit.