developing-complex-posix-shell-scripts
developing-complex-posix-shell-scripts skill
This skill defines guidelines for writing complex POSIX shell scripts: production utilities, reusable system tools, or scripts that require multiple flags, structured logging, or robust error handling — while remaining strictly portable.
Use developing-simple-posix-shell-scripts for ad-hoc tasks or scripts under ~50 lines without CLI scaffolding.
When to Use This Skill
- Reusable system utilities or production automation scripts targeting
/bin/sh - Scripts with multiple named flags / options
- Requires structured logging with severity levels
- Needs
-h/ help output - Has cleanup logic, dependency checks, or complex control flow
- Must run portably on Alpine, BusyBox, embedded, or other minimal systems
Core Requirements
Shebang & Safety Modes
#!/bin/sh
set -e
set -u
set -e: exit immediately on error.set -u: exit on reference to an unset variable.set -o pipefailis not POSIX — do not use it.
Tooling
- All scripts MUST pass
shellcheck --shell=shwithout warnings. - Format with
shfmt -ln posixbefore considering the script done.
POSIX Compliance — Do NOT Use These Bash-isms
| Bash feature | POSIX replacement |
|---|---|
[[ ... ]] |
[ ... ] |
local var |
prefix with _funcname_var (see Variables section) |
declare -a arr |
not available — restructure logic |
source file |
. file |
function f { } |
f() { } |
(( expr )) |
$(( expr )) |
$'...' strings |
printf |
<<< here-strings |
printf ... | or temp file |
<(cmd) process substitution |
temp file or pipe |
echo -e |
printf |
Variables & Quoting
- Always use
${var}(braces) for variable expansion. - Always quote expansions:
"${var}". - POSIX
shhas nolocalkeyword. Simulate function-local variables by prefixing with the function name:_log_message_color,_parse_args_opt, etc. Unset them at the end of the function if needed. - Use
$(...)for command substitution, never backticks. - Prefer
printfoverechofor reliable, portable output.
Reference Code Blocks
Pick and compose the sections you need. Do NOT copy everything blindly — only include what the script actually uses.
1. Script Identity
SCRIPT_NAME="$(basename "${0}")"
2. Logging Subsystem
Use this block when the script needs more than simple printf output. Because POSIX sh has no associative arrays, level priorities are resolved via a helper function.
# Logging configuration
LOG_LEVEL="INFO" # ERROR, WARNING, INFO, DEBUG
LOG_FORMAT="simple" # simple, level, full
get_log_priority() {
case "${1}" in
DEBUG) printf '10' ;;
INFO) printf '20' ;;
WARNING) printf '30' ;;
ERROR) printf '40' ;;
CRITICAL) printf '50' ;;
*) printf '0' ;;
esac
}
log_color() {
_log_color_code="${1}"
shift
if [ -t 2 ]; then
# shellcheck disable=SC2059
printf "\033[0;%sm%s\033[0m\n" "${_log_color_code}" "${*}" >&2
else
printf '%s\n' "${*}" >&2
fi
unset _log_color_code
}
log_message() {
_log_message_color="${1}"
_log_message_level="${2}"
shift 2
_log_message_current_prio=$(get_log_priority "${LOG_LEVEL}")
_log_message_msg_prio=$(get_log_priority "${_log_message_level}")
if [ "${_log_message_msg_prio}" -lt "${_log_message_current_prio}" ]; then
unset _log_message_color _log_message_level _log_message_current_prio _log_message_msg_prio
return 0
fi
case "${LOG_FORMAT}" in
simple) log_color "${_log_message_color}" "${*}" ;;
level) log_color "${_log_message_color}" "[${_log_message_level}] ${*}" ;;
full)
_log_message_ts=$(date '+%Y-%m-%dT%H:%M:%S' 2>/dev/null || printf 'unknown')
log_color "${_log_message_color}" "[${_log_message_ts}][${_log_message_level}] ${*}"
unset _log_message_ts
;;
*) log_color "${_log_message_color}" "${*}" ;;
esac
unset _log_message_color _log_message_level _log_message_current_prio _log_message_msg_prio
}
log_error() { log_message '31' 'ERROR' "${@}"; }
log_info() { log_message '32' 'INFO' "${@}"; }
log_warning() { log_message '33' 'WARNING' "${@}"; }
log_debug() { log_message '34' 'DEBUG' "${@}"; }
log_critical() { log_message '36' 'CRITICAL' "${@}"; }
3. Log Level & Format Setters
Include these when the script exposes -l / log-format flags.
set_log_level() {
# tr to uppercase without bashism
_set_log_level_val=$(printf '%s' "${1}" | tr '[:lower:]' '[:upper:]')
case "${_set_log_level_val}" in
DEBUG | INFO | WARNING | ERROR | CRITICAL)
LOG_LEVEL="${_set_log_level_val}"
;;
*)
log_error "Invalid log level: ${1}. Valid levels: DEBUG, INFO, WARNING, ERROR, CRITICAL"
exit 1
;;
esac
unset _set_log_level_val
}
set_log_format() {
case "${1}" in
simple | level | full)
LOG_FORMAT="${1}"
;;
*)
log_error "Invalid log format: ${1}. Valid formats: simple, level, full"
exit 1
;;
esac
}
4. Dependency Check
Include this when the script relies on external commands that may not be present.
require_command() {
_require_command_missing=''
for _require_command_c in "${@}"; do
if ! command -v "${_require_command_c}" >/dev/null 2>&1; then
_require_command_missing="${_require_command_missing} ${_require_command_c}"
fi
done
if [ -n "${_require_command_missing}" ]; then
log_error "Required command(s) not installed:${_require_command_missing}"
log_error "Please install the missing dependencies and try again"
exit 1
fi
unset _require_command_missing _require_command_c
}
5. Cleanup Handler
Include this when the script creates temporary files or resources that must be cleaned up on exit.
cleanup() {
_cleanup_exit_code=$?
# Add cleanup logic here (e.g., rm -f "${_tmpfile:-}")
exit "${_cleanup_exit_code}"
}
trap cleanup EXIT INT TERM
6. Usage / Help
Argument ordering convention: list template flags first (-h, -l, -f), then script-specific flags. This mirrors the ordering required in the getopts optstring and case statement (see Section 7), keeping all three in sync.
usage() {
_usage_exit_code="${1:-0}"
cat <<EOF
USAGE:
${SCRIPT_NAME} [OPTIONS]
Short description of what the script does.
OPTIONS:
-h Show this help message
-l LEVEL Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
Default: INFO
-f FORMAT Set log format (simple, level, full)
simple: message only
level: [LEVEL] message
full: [timestamp][LEVEL] message
Default: simple
<script-specific flags go here>
EXAMPLES:
${SCRIPT_NAME} -h
${SCRIPT_NAME} -l DEBUG -f full
EOF
exit "${_usage_exit_code}"
}
7. Argument Parsing
Use getopts (POSIX built-in, short options only). Adjust the optstring to match the script's flags.
Argument ordering convention: template flags come first in the optstring and case statement, script-specific flags come after. This keeps usage, getopts optstring, and case in sync.
parse_args() {
# --- template flag defaults ---
# LOG_LEVEL and LOG_FORMAT already defaulted at top of script
# --- script-specific flag defaults ---
# MY_FLAG="default"
# template flags first (hl:f:), then script-specific flags
while getopts ':hl:f:' _parse_args_opt; do
case "${_parse_args_opt}" in
# --- template flags (always first) ---
h) usage 0 ;;
l) set_log_level "${OPTARG}" ;;
f) set_log_format "${OPTARG}" ;;
# --- script-specific flags ---
\?)
log_error "Invalid option: -${OPTARG}"
usage 1
;;
:)
log_error "Option -${OPTARG} requires an argument"
usage 1
;;
esac
done
shift $((OPTIND - 1))
# Remaining positional arguments are now in "$@"
}
8. Main Entry Point
main() {
parse_args "${@}"
log_debug "Log level: ${LOG_LEVEL}, Log format: ${LOG_FORMAT}"
# Script logic here
}
main "${@}"
Composition Guide
- Always include: Script Identity + Shebang/Safety Modes +
main. - Include Logging Subsystem when output needs severity levels or colour.
- Include Log Level/Format Setters only when exposing
-l/-fflags. - Include Dependency Check when relying on non-standard external commands.
- Include Cleanup Handler when the script allocates resources (temp files, locks, etc.).
- Include Usage + Argument Parsing when the script accepts any named flags.
Compose only the sections the script actually needs. A complex script with no temp files does not need a cleanup handler.
More from ak1ra-komj/agents-skills
summarize-current-session
Use when the user asks to summarize this session, write a session log, save what we did today, or similar.
20developing-ansible
Use when writing, reviewing, or refactoring Ansible playbooks, roles, or tasks.
15developing-bash-scripts
Use when writing, reviewing, or refactoring a Bash script.
14developing-posix-shell-scripts
Use when writing, reviewing, or refactoring a POSIX shell script (/bin/sh), or when targeting Alpine, BusyBox, or any environment where Bash cannot be assumed.
13keep-a-changelog
Use when the user mentions preparing or publishing a new release, or asks to review or refactor CHANGELOG.md, following Keep a Changelog format and Semantic Versioning.
9init-agents-md
Generate or overwrite AGENTS.md in the project root. Use when the user asks to "create AGENTS.md", "write an agent guide", "document the project for AI agents", or wants to orient AI coding tools to a new repository.
7