shell-dev-practices

majiayu000's avatarfrom majiayu000

Shell scripting best practices for bash, sh, zsh with focus on error handling, portability, and automation.

0stars🔀0forks📁View on GitHub🕐Updated Jan 5, 2026

When & Why to Use This Skill

This Claude skill serves as a comprehensive guide for developing robust, production-grade shell scripts across Bash, Sh, and Zsh environments. It prioritizes reliability and security by enforcing industry standards such as strict mode, POSIX compatibility, and advanced error handling. By integrating these best practices, developers can create portable automation tools and resilient runbooks that minimize runtime failures and simplify debugging in complex IT infrastructures.

Use Cases

  • Building fail-safe CI/CD deployment scripts that utilize 'set -euo pipefail' and trap-based cleanup to prevent partial or corrupted releases.
  • Standardizing shell script development across engineering teams to ensure scripts run consistently on both macOS and various Linux distributions.
  • Refactoring legacy automation scripts to implement secure variable quoting and input validation, reducing vulnerabilities to word splitting and glob expansion.
  • Creating professional-grade CLI tools with robust argument parsing using getopts and standardized logging for better observability in automated tasks.
nameshell-dev-practices
descriptionShell scripting best practices for bash, sh, zsh with focus on error handling, portability, and automation.

Shell development practices

Purpose

Guide for shell scripting best practices covering POSIX compatibility, error handling, and robust automation scripts.

When to use

This skill activates when:

  • Writing shell scripts
  • Creating build/deploy automation
  • Writing bash/sh/zsh code
  • Debugging shell issues
  • Making scripts portable

Core principles

Always use strict mode

#!/usr/bin/env bash
set -euo pipefail

# -e: Exit on error
# -u: Error on undefined variables
# -o pipefail: Fail on pipe errors

Quote all variables

# Bad: Word splitting and glob expansion
echo $filename
rm $path/*

# Good: Properly quoted
echo "$filename"
rm "$path"/*

Error handling

Trap for cleanup

#!/usr/bin/env bash
set -euo pipefail

TEMP_DIR=""

cleanup() {
    if [[ -n "$TEMP_DIR" && -d "$TEMP_DIR" ]]; then
        rm -rf "$TEMP_DIR"
    fi
}

trap cleanup EXIT

TEMP_DIR=$(mktemp -d)
# Use temp dir...

Check command existence

# Check if command exists
if ! command -v git &> /dev/null; then
    echo "Error: git is required but not installed" >&2
    exit 1
fi

Validate inputs

validate_args() {
    if [[ $# -lt 1 ]]; then
        echo "Usage: $0 <filename>" >&2
        exit 1
    fi

    if [[ ! -f "$1" ]]; then
        echo "Error: File not found: $1" >&2
        exit 1
    fi
}

validate_args "$@"

Functions

Function definitions

# Good: Local variables, clear return
process_file() {
    local file="$1"
    local output=""

    if [[ ! -f "$file" ]]; then
        return 1
    fi

    output=$(cat "$file")
    echo "$output"
}

Return values

# Use return for status, echo for output
get_value() {
    local key="$1"

    if [[ -z "$key" ]]; then
        return 1
    fi

    echo "value_for_$key"
}

# Capture output
if value=$(get_value "mykey"); then
    echo "Got: $value"
else
    echo "Failed to get value"
fi

Conditionals

Test syntax

# Modern syntax with [[]]
if [[ -f "$file" ]]; then
    echo "File exists"
fi

# String comparison
if [[ "$string" == "value" ]]; then
    echo "Match"
fi

# Numeric comparison
if [[ "$count" -gt 10 ]]; then
    echo "Greater than 10"
fi

Common tests

# File tests
[[ -f "$path" ]]  # Regular file
[[ -d "$path" ]]  # Directory
[[ -e "$path" ]]  # Exists
[[ -r "$path" ]]  # Readable
[[ -w "$path" ]]  # Writable
[[ -x "$path" ]]  # Executable

# String tests
[[ -z "$var" ]]   # Empty
[[ -n "$var" ]]   # Not empty
[[ "$a" == "$b" ]] # Equal
[[ "$a" != "$b" ]] # Not equal

Arrays

# Declare array
declare -a files=()

# Add elements
files+=("file1.txt")
files+=("file2.txt")

# Iterate
for file in "${files[@]}"; do
    echo "Processing: $file"
done

# Array length
echo "Count: ${#files[@]}"

Input handling

Reading input

# Read line
read -r line < "$file"

# Read with prompt
read -rp "Enter name: " name

# Read password
read -rsp "Enter password: " password
echo  # newline after hidden input

Process arguments

#!/usr/bin/env bash
set -euo pipefail

usage() {
    echo "Usage: $0 [-v] [-o output] input"
    exit 1
}

verbose=false
output=""

while getopts "vo:" opt; do
    case $opt in
        v) verbose=true ;;
        o) output="$OPTARG" ;;
        *) usage ;;
    esac
done

shift $((OPTIND - 1))

if [[ $# -lt 1 ]]; then
    usage
fi

input="$1"

Output

Logging

# Log levels
log_info() { echo "[INFO] $*"; }
log_warn() { echo "[WARN] $*" >&2; }
log_error() { echo "[ERROR] $*" >&2; }

# Usage
log_info "Starting process"
log_error "Failed to open file"

Colors (optional)

# Only use if terminal supports it
if [[ -t 1 ]]; then
    RED='\033[0;31m'
    GREEN='\033[0;32m'
    NC='\033[0m'
else
    RED=''
    GREEN=''
    NC=''
fi

echo -e "${GREEN}Success${NC}"
echo -e "${RED}Error${NC}"

Portability

POSIX compatibility

# More portable alternatives
# Instead of: echo -e
printf '%s\n' "$message"

# Instead of: [[ ]]
[ -f "$file" ]

# Instead of: $()
`command`  # Though $() is preferred when available

Path handling

# Get script directory
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

# Resolve symlinks
REAL_PATH="$(realpath "$path")"

ShellCheck

Always validate with ShellCheck:

# Install
brew install shellcheck  # macOS
apt install shellcheck   # Ubuntu

# Run
shellcheck script.sh

Checklist

  • Uses set -euo pipefail
  • All variables quoted
  • Functions use local variables
  • Cleanup with trap
  • Inputs validated
  • ShellCheck passes

Additional resources: