name: developing-shell-scripts
description: Use when writing or reviewing .sh, .bash, or .zsh files, Makefile shell snippets, CI shell commands, or choosing between Zsh, Bash, and POSIX sh in this repo.
Developing Shell Scripts
Overview
Choose the simplest shell that meets the runtime constraints, then make the script explicit about safety, dependencies, and I/O behavior. In this repo, prefer Zsh for developer-facing scripts when availability allows, then Bash, then POSIX sh.
When to Use
- Writing or reviewing shell scripts in this repo
- Adding shell snippets to Makefiles or CI configuration
- Choosing between Zsh, Bash, and POSIX
sh - Hardening script error handling, quoting, option parsing, or cleanup behavior
Do not use this skill for non-shell implementation work or as a substitute for checking the runtime environment the script will actually run in.
Quick Reference
| Context | Default |
|---|---|
| Developer-facing script on macOS/Linux | Zsh with #!/usr/bin/env zsh |
| CI or minimal container | Bash with #!/usr/bin/env bash |
| Maximum portability | POSIX sh with #!/bin/sh |
| Safety prologue | set -euo pipefail for Bash, add -E when relying on ERR traps, use set -eu for sh, and emulate -L zsh plus explicit options in Zsh functions |
| Output channels | stdout for actual output, stderr for errors and diagnostics |
| Tool preference | Shell built-ins first, then standard utilities, then npx, then python |
| Bash script directory | Resolve the script file with realpath -- "${BASH_SOURCE[0]}", then derive its directory with parameter expansion |
| Static checks | ShellCheck and shfmt for sh or Bash, and zsh -n for Zsh |
Implementation
- Use shell-specific extensions and shebangs for executable scripts; omit the shebang only for sourced shell libraries
- Keep scripts self-contained and do not rely on interactive init files such as
.zshrcor.bashrc - In Bash scripts, locate the script with
script_path="$(realpath -- "${BASH_SOURCE[0]}")"andscript_dir="${script_path%/*}"; this resolves symlinked script files, unlikecd "$(dirname ...)" && pwd, and stays portable across the repo's current macOS and Linux floor when used without GNU-onlyrealpathflags - Prefer
$()over backticks, and usecaseor arithmetic contexts where they improve clarity - In Zsh, follow Zsh expansion rules: do not quote ordinary scalar expansions just from Bash/POSIX habit; keep quotes where they preserve argv exactly, protect literal text, or are required by another shell or file format
- Put reusable logic in functions, declare locals locally, and route top-level execution through
main "$@" - In Zsh functions, start with
emulate -L zshand opt into options explicitly so caller settings do not leak into control flow - Check prerequisites with
command -v, validate required environment variables, and clean up temporary files with traps - Prefer
print -r --in Zsh, keep help text in single quotes or heredocs, and support both short and long GNU-style options when a CLI is user-facing
Read references/operational-details.md for shell-specific safety details, CLI parsing, portability gotchas, toolchain choices, and cleanup patterns.
Common Mistakes
- Picking a shell that the target environment does not reliably provide
- Letting caller options leak into Zsh library behavior
- Relying on interactive shell startup files in non-interactive contexts
- Leaving variables unquoted or assuming the current working directory
- Mixing normal output with diagnostics on stdout