bin-script-scaffold

star 21

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.

weikinhuang By weikinhuang schedule Updated 6/1/2026

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
# One-line purpose.
# SPDX-License-Identifier: MIT

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:

# shellcheck shell=bash
# Bash completion for foo.
# SPDX-License-Identifier: MIT

_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
# Tests for dotenv/bin/foo.
# SPDX-License-Identifier: MIT

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'
#!/usr/bin/env bash
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

  1. Confirm the placement: dotenv/bin/ for cross-platform, dotenv/<platform>/bin/ for platform-specific. Confirm whether the script should be public or private (__-prefixed).
  2. Write the script with the standard header, set -euo pipefail, and the genpasswd-style argparse pattern that matches its flag surface.
  3. Write the sibling completion file under the parallel completion/ directory. Skip this only for __-prefixed private scripts.
  4. 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.
  5. chmod +x the script.
  6. Update REFERENCE.md under ## Commands on PATH > ### Utility commands, or ### Git subcommands for git-* scripts, per the doc-sync skill.
  7. 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.
Install via CLI
npx skills add https://github.com/weikinhuang/dotfiles --skill bin-script-scaffold
Repository Details
star Stars 21
call_split Forks 3
navigation Branch main
article Path SKILL.md
More from Creator