| name | shell-bash |
| description | Shell scripting and Bash programming patterns |
| domain | programming-languages |
| version | 1.0.0 |
| tags | ["bash","shell","scripting","automation","cli"] |
| triggers | {"keywords":{"primary":["bash","shell","sh","zsh","script","terminal","cli"],"secondary":["grep","sed","awk","pipe","cron","automation","makefile"]},"context_boost":["devops","linux","unix","automation","sysadmin"],"context_penalty":["web","frontend","mobile","gui"],"priority":"medium"} |
Shell & Bash Scripting
Mandatory Script Header
Every script must start with this exact header. No exceptions.
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "${BASH_SOURCE[0]}")"
set -e — exit on error
set -u — error on undefined variables (catches typos like $HOMR)
set -o pipefail — pipe failures propagate (without this false | true exits 0)
IFS=$'\n\t' — prevent word splitting on spaces
BASH_SOURCE[0] — correct script dir even when sourced or called via symlink
Trap Cleanup
Always register cleanup for temp files:
TEMP_FILE=""
cleanup() {
local exit_code=$?
[[ -n "$TEMP_FILE" ]] && rm -f "$TEMP_FILE"
exit "$exit_code"
}
trap cleanup EXIT
Argument Parsing
Use manual parsing — getopts doesn't support long options:
VERBOSE=false
DRY_RUN=false
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-v|--verbose) VERBOSE=true; shift ;;
-n|--dry-run) DRY_RUN=true; shift ;;
-h|--help) usage; exit 0 ;;
--) shift; break ;;
-*) die "Unknown option: $1" ;;
*) break ;;
esac
done
SOURCE="${1:-}"
DEST="${2:-}"
}
Variable Expansion
"${var:-default}"
"${var:=default}"
"${var:?error msg}"
"${str#prefix}"
"${str##*/}"
"${str%suffix}"
"${str%/*}"
"${str%%.*}"
"${str/old/new}"
"${str//old/new}"
"${#str}"
"${str^^}"
"${str,,}"
Arrays — Always Quote
declare -a items=("one" "two" "three")
items+=("four")
echo "${items[0]}"
echo "${items[-1]}"
echo "${#items[@]}"
echo "${items[@]}"
for item in "${items[@]}"; do
echo "$item"
done
for i in "${!items[@]}"; do
echo "$i: ${items[$i]}"
done
declare -A config=([host]="localhost" [port]="5432")
for key in "${!config[@]}"; do
echo "$key=${config[$key]}"
done
Test Operators
Use [[ not [. Never use [ — it's POSIX sh and lacks regex, &&/||, etc.
[[ -e "$path" ]]
[[ -f "$path" ]]
[[ -d "$path" ]]
[[ -r "$path" ]]
[[ -w "$path" ]]
[[ -x "$path" ]]
[[ -s "$path" ]]
[[ -z "$str" ]]
[[ -n "$str" ]]
[[ "$a" == "$b" ]]
[[ "$a" != "$b" ]]
[[ "$str" =~ ^[0-9]+$ ]]
(( a == b ))
(( a != b ))
(( a < b ))
(( a > b ))
(( a <= b ))
(( a >= b ))
Functions
Always use local for every variable inside a function:
process_file() {
local file="$1"
local result
result=$(some_command "$file")
echo "$result"
}
is_valid() {
local value="$1"
[[ "$value" =~ ^[0-9]+$ ]]
}
if is_valid "$input"; then
echo "ok"
fi
die() {
echo "ERROR: $*" >&2
exit 1
}
Reading Files and Command Output
while IFS= read -r line; do
echo "$line"
done < "$file"
while IFS= read -r entry; do
process "$entry"
done < <(find . -name "*.txt")
mapfile -t lines < "$file"
Common Mistakes to Avoid
for f in $(ls *.txt); do ...
for f in *.txt; do ...
cmd | grep pattern
if [[ $? -eq 0 ]]; then ...
if cmd | grep -q pattern; then ...
find . -name "*.txt" | while read -r f; do
VAR="$f"
done
echo "$VAR"
while IFS= read -r f; do
VAR="$f"
done < <(find . -name "*.txt")
echo "$VAR"
cp $file $dest
cp "$file" "$dest"
Logging Pattern
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$1] ${*:2}"; }
info() { log INFO "$@"; }
warn() { log WARN "$@" >&2; }
error() { log ERROR "$@" >&2; }
debug() { [[ "${VERBOSE:-false}" == true ]] && log DEBUG "$@" || true; }
Main Guard
Always guard execution so the script can be sourced for testing:
main() {
parse_args "$@"
}
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
main "$@"
fi