Linux Bash Scripting & Systems Automation
Shell scripting is the primary automation mechanism in Linux environments, enabling administrators and software engineers to orchestrate deployments, backup workflows, batch processing, and system maintenance. This guide covers production-grade shell scripting standards, control structures, functions, argument parsing, error trapping, and automation patterns.
⚡ Quick Dive
Bash Scripting Core Syntax Cheat Sheet
| Feature | Syntax / Command | Example |
|---|---|---|
| Shebang & Strict Mode | #!/usr/bin/env bashset -euo pipefail |
Fail immediately on error, unset variable, or pipe failure |
| Variable Declaration | VAR="value" (no spaces around =) |
TARGET_DIR="/var/backups" |
| Command Substitution | $(command) |
CURRENT_DATE=$(date +%F) |
| Arithmetic Evaluation | $(( expression )) |
NEXT_COUNT=$(( COUNT + 1 )) |
| Conditional String Test | [[ "$STR1" == "$STR2" ]] |
[[ "$ENV" == "production" ]] |
| Conditional Number Test | [[ "$NUM" -gt 10 ]] |
[[ "$EXIT_CODE" -eq 0 ]] |
| File Condition Test | [[ -f "$FILE" ]] / [[ -d "$DIR" ]] |
[[ -s "/etc/hosts" ]] (exists & non-empty) |
| Function Definition | function_name() { ... } |
log_info() { echo "[INFO] $*"; } |
| Trap Signal Handler | trap cleanup_handler EXIT ERR |
trap 'rm -rf "$TMP_DIR"' EXIT |
| Script Argument List | $# (count), $@ (all), $1..$9 |
echo "Running on target: $1" |
Production Script Skeleton
#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
# Global constants & variables
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/tmp/automation.log"
# Cleanup handler executed on exit (success or failure)
cleanup() {
local exit_code=$?
echo "[INFO] Cleaning temporary files..."
# rm -f /tmp/lockfile
exit "$exit_code"
}
trap cleanup EXIT INT TERM
log() {
echo "[$(date +'%Y-%m-%dT%H:%M:%S%z')] $*" | tee -a "$LOG_FILE"
}
main() {
log "Starting automated workflow from ${SCRIPT_DIR}..."
# Core logic here
log "Task completed successfully."
}
main "$@"
📖 Extended Guide
1. Bash Strict Mode & Safety Directives
Writing production-grade Bash requires preventing silent failures:
set -euo pipefail
-e(errexit): Exit immediately if any command returns a non-zero exit code (unless wrapped in anif,while, oruntilcondition).-u(nounset): Treat unset or uninitialized variables as an error and exit immediately. Prevents disastrous typos likerm -rf "$UNSET_VAR/*".-o pipefail: Returns the exit code of the last command in a pipeline that failed (failedcommand1 | command2won't be masked by a successfulcommand2).IFS=$'\n\t': Internal Field Separator set to newline and tab (prevents accidental word splitting on spaces in file names).
2. Variables, Parameter Expansion, and Types
Default & Fallback Expansions
# Use default if unset or empty
PORT="${APP_PORT:-8080}"
# Assign default if unset
: "${BACKUP_DIR:=/tmp/backup}"
# Error and exit if variable is unset
DB_PASS="${DATABASE_PASSWORD:?Database password must be provided}"
String Manipulation in Native Bash
Avoid calling external binaries (cut, sed, awk) for simple string transformations:
FILENAME="app_v2.1.0_linux_amd64.tar.gz"
# Substring Removal from Front
echo "${FILENAME#*_}" # Non-greedy: v2.1.0_linux_amd64.tar.gz
echo "${FILENAME##*_}" # Greedy: amd64.tar.gz
# Substring Removal from End (Extract extension or basename)
echo "${FILENAME%.*}" # Non-greedy: app_v2.1.0_linux_amd64.tar
echo "${FILENAME%%.*}" # Greedy: app_v2
# Search and Replace
echo "${FILENAME//_/-}" # Replace all underscores with dashes: app-v2.1.0-linux-amd64.tar.gz
# String Length & Slicing
echo "${#FILENAME}" # Character count
echo "${FILENAME:0:3}" # First 3 characters: app
3. Conditional Structures & Comparisons
Always use modern double brackets [[ ... ]] instead of legacy single brackets [ ... ] or test:
Comparison Operators Matrix
| Comparison Type | Operator | Meaning / Example |
|---|---|---|
| Integer | -eq, -ne |
Equal / Not equal: [[ $count -eq 0 ]] |
| Integer | -lt, -le, -gt, -ge |
Numeric comparisons: [[ $age -ge 18 ]] |
| String | ==, != |
Equality: [[ "$role" == "admin" ]] |
| String | =~ |
Regex matching: [[ "$email" =~ ^[a-z0-9._%+-]+@[a-z0-9.-]+\.[a-z]{2,}$ ]] |
| String | -z, -n |
Empty check (-z) / Non-empty check (-n): [[ -z "$TOKEN" ]] |
| Filesystem | -f, -d, -e |
Is regular file / Directory / Path exists |
| Filesystem | -s |
Exists and size > 0 (non-empty file) |
| Filesystem | -r, -w, -x |
Readable / Writable / Executable by current user |
# Branching Example
if [[ -f "/etc/nginx/nginx.conf" && -x "$(command -v nginx)" ]]; then
nginx -t
elif [[ -d "/etc/caddy" ]]; then
echo "Using Caddy Web Server"
else
echo "No supported web server found" >&2
exit 1
fi
4. Loops & Iteration Patterns
Iterating Files Safely with Globs
# Always quote variables to handle spaces safely
for file in /var/log/nginx/*.log; do
[[ -f "$file" ]] || continue # Handle empty directory edge case
echo "Processing $file (Size: $(stat -c%s "$file") bytes)"
done
Reading Files Line-by-Line (The Standard Idiom)
# Safe against backslashes and trailing newlines
while IFS= read -r line || [[ -n "$line" ]]; do
# Skip comments and blank lines
[[ "$line" =~ ^[[:space:]]*# ]] && continue
[[ -z "$line" ]] && continue
echo "Entry: $line"
done < "/etc/hosts"
C-Style Loops & Ranges
# Numeric brace expansion
for i in {1..5}; do
echo "Retry attempt: $i"
done
# C-style syntax
for ((i=0; i<10; i+=2)); do
echo "Index: $i"
done
5. Robust Function Design
# Functions should use local variables and explicit return codes
deploy_service() {
local service_name="$1"
local target_env="${2:-staging}"
echo "[INFO] Deploying ${service_name} to ${target_env}..."
if ! systemctl is-active --quiet "$service_name"; then
echo "[ERROR] Service ${service_name} is not running!" >&2
return 1
fi
return 0
}
# Invocation and error check
if ! deploy_service "nginx" "production"; then
echo "Deployment failed!"
fi
6. Command-Line Argument Parsing with getopts
For production CLI scripts, parse flags and parameters cleanly:
#!/usr/bin/env bash
usage() {
echo "Usage: $0 [-h] -e <env> -p <port> [target]"
echo " -h Display help"
echo " -e <env> Target environment (dev|prod)"
echo " -p <port> Port number"
exit 1
}
ENVIRONMENT=""
PORT="8080"
while getopts ":he:p:" opt; do
case "$opt" in
h) usage ;;
e) ENVIRONMENT="$OPTARG" ;;
p) PORT="$OPTARG" ;;
:) echo "Error: Option -$OPTARG requires an argument." >&2; usage ;;
\?) echo "Error: Invalid option -$OPTARG" >&2; usage ;;
esac
done
shift $((OPTIND - 1))
# Remaining positional arguments
TARGET="${1:-localhost}"
if [[ -z "$ENVIRONMENT" ]]; then
echo "Error: Environment (-e) is required." >&2
usage
fi
echo "Deploying to $TARGET on port $PORT in $ENVIRONMENT mode."
7. Signal Trapping & Temporary Directories
Use trap to guarantee resource cleanup and prevent orphaned processes or temporary files:
# Create a secure temporary directory that is guaranteed to be deleted on script exit
TMP_DIR=$(mktemp -d /tmp/backup_job.XXXXXX)
trap 'rm -rf "$TMP_DIR"' EXIT
# Perform work inside isolated temporary directory
tar -czf "${TMP_DIR}/archive.tar.gz" /data/
cp "${TMP_DIR}/archive.tar.gz" /mnt/backups/
# When script exits (normal, Ctrl+C, or error), TMP_DIR is deleted automatically