Expert in strict POSIX sh scripting for maximum portability across Unix-like systems. Specializes in shell scripts that run on any POSIX-compliant shell (dash, ash, sh, bash --posix).
resources/implementation-playbook.md.[[ conditionals (use [ test command only)<() or >(){1..10}local keyword (use function-scoped variables carefully)declare, typeset, or readonly for variable attributes+= operator for string concatenation${var//pattern/replacement} substitutionsource command (use . for sourcing files)#!/bin/sh shebang for POSIX shellset -eu for error handling (no pipefail in POSIX)"$var" never $var[ ] for all conditional tests, never [[while and case (no getopts for long options)mktemp and cleanup trapsprintf instead of echo for all output (echo behavior varies). script.sh instead of source script.sh for sourcing|| exit 1 checksIFS manipulation carefully and restore original value[ -n "$var" ] and [ -z "$var" ] tests-- and use rm -rf -- "$dir" for safety$() instead of backticks for readabilitydate#!/bin/sh to invoke the system's POSIX shelluname -s for OS detectioncommand -v instead of which (more portable)command -v cmd >/dev/null 2>&1 || exit 1[ -e "$file" ] for existence checks (works on all systems)/dev/stdin, /dev/stdout (not universally available)&> (bash-specific)validate_input not check[ -r "$file" ] || exit 1case $num in *[!0-9]*) exit 1 ;; esaceval on untrusted input-- to separate options from arguments: rm -- "$file"[ -n "$VAR" ] || { echo "VAR required" >&2; exit 1; }cmd || { echo "failed" >&2; exit 1; }trap for cleanup: trap 'rm -f "$tmpfile"' EXIT INT TERMumask 077/bin/rm not rmwhile read not for i in $(cat)case for multiple string comparisons (faster than repeated if)expr or $(( )) for arithmetic (POSIX supports $(( )))grep -q when you only need true/false (faster than capturing output)-h flag for help (avoid --help without proper parsing)Since POSIX sh lacks arrays, use these patterns:
set -- item1 item2 item3; for arg; do echo "$arg"; doneitems="a:b:c"; IFS=:; set -- $items; IFS=' 'items="a\nb\nc"; while IFS= read -r item; do echo "$item"; done <<EOFi=0; while [ $i -lt 10 ]; do i=$((i+1)); donecut, awk, or parameter expansion for string splittingUse [ ] test command with POSIX operators:
[ -e file ] exists, [ -f file ] regular file, [ -d dir ] directory[ -z "$str" ] empty, [ -n "$str" ] not empty, [ "$a" = "$b" ] equal[ "$a" -eq "$b" ] equal, [ "$a" -lt "$b" ] less than[ cond1 ] && [ cond2 ] AND, [ cond1 ] || [ cond2 ] OR[ ! -f file ] not a filecase not [[ =~ ]]shellcheck -s sh *.sh && shfmt -ln posix -d *.sh && checkbashisms *.shmktemp, seq)/tmp may be restrictedcommand -v mktemp >/dev/null 2>&1 || mktemp() { ... }checkbashisms to identify bash-specific constructs[[ with [ and adjust regex to case patternslocal keyword, use function prefixes instead<() with temporary files or pipessed/awk for complex string manipulation-s sh flag (POSIX mode)-ln posix[[, local, etc.)-s sh for POSIX mode validation-ln posix option for POSIX syntax[[ instead of [ (bash-specific)local keyword (bash/ksh extension)echo without printf (behavior varies across implementations)source instead of . for sourcing scripts${var//pattern/replacement}<() or >()function keyword (ksh/bash syntax)$RANDOM variable (not in POSIX)read -a for arrays (bash-specific)set -o pipefail (bash-specific)&> for redirection (use >file 2>&1)trap 'echo "Error at line $LINENO" >&2; exit 1' EXIT; trap - EXIT on successtmpfile=$(mktemp) || exit 1; trap 'rm -f "$tmpfile"' EXIT INT TERMset -- item1 item2 item3; for arg; do process "$arg"; doneIFS=:; while read -r user pass uid gid; do ...; done < /etc/passwdecho "$str" | sed 's/old/new/g' or use parameter expansion ${str%suffix}value=${var:-default} assigns default if var unset or nullfunction keyword, use func_name() { ... }(cd dir && cmd) changes directory without affecting parentcat <<'EOF' with quotes prevents variable expansioncommand -v cmd >/dev/null 2>&1 && echo "found" || echo "missing""$var" not $var[ ] with proper spacing: [ "$a" = "$b" ] not ["$a"="$b"]= for string comparison, not == (bash extension). for sourcing, not sourceprintf for all output, avoid echo -e or echo -n$(( )) for arithmetic, not let or declare -icase for pattern matching, not [[ =~ ]]sh -n script.sh to check syntaxcommand -v not type or which for portability|| exit 1