Bash Scripting Essentials: From One-Liners to Real Scripts
Bash scripts automate everything from deployments to backups. Here’s how to write them properly. Script Structure 1 2 3 4 #!/bin/bash set -euo pipefail # Your code here The shebang (#!/bin/bash) tells the system which interpreter to use. The set options make scripts safer: -e: Exit on error -u: Error on undefined variables -o pipefail: Catch errors in pipelines Variables 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 # Assignment (no spaces!) NAME="Alice" COUNT=42 # Usage echo "Hello, $NAME" echo "Count is ${COUNT}" # Command substitution DATE=$(date +%Y-%m-%d) FILES=$(ls -1 | wc -l) # Default values ENVIRONMENT=${1:-production} # First arg, default "production" LOG_LEVEL=${LOG_LEVEL:-info} # Env var with default Variable Scope 1 2 3 4 5 6 7 8 # Global by default GLOBAL="I'm everywhere" # Local to function my_function() { local LOCAL="I'm only here" echo "$LOCAL" } Conditionals 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 # String comparison if [ "$NAME" = "Alice" ]; then echo "Hi Alice" elif [ "$NAME" = "Bob" ]; then echo "Hi Bob" else echo "Who are you?" fi # Numeric comparison if [ "$COUNT" -gt 10 ]; then echo "More than 10" fi # File tests if [ -f "$FILE" ]; then echo "File exists" fi if [ -d "$DIR" ]; then echo "Directory exists" fi if [ -z "$VAR" ]; then echo "Variable is empty" fi if [ -n "$VAR" ]; then echo "Variable is not empty" fi Comparison Operators String Numeric Meaning = -eq Equal != -ne Not equal -lt Less than -le Less or equal -gt Greater than -ge Greater or equal Modern Syntax 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Double brackets (safer, more features) if [[ "$NAME" == "Alice" ]]; then echo "Hi" fi # Pattern matching if [[ "$FILE" == *.txt ]]; then echo "Text file" fi # Regex if [[ "$EMAIL" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then echo "Valid email" fi # Logical operators if [[ "$A" == "yes" && "$B" == "yes" ]]; then echo "Both yes" fi if [[ "$A" == "yes" || "$B" == "yes" ]]; then echo "At least one yes" fi Loops 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 # For loop over list for NAME in Alice Bob Charlie; do echo "Hello, $NAME" done # For loop over files for FILE in *.txt; do echo "Processing $FILE" done # For loop with range for i in {1..10}; do echo "Number $i" done # C-style for loop for ((i=0; i<10; i++)); do echo "Index $i" done # While loop while [ "$COUNT" -gt 0 ]; do echo "$COUNT" COUNT=$((COUNT - 1)) done # Read lines from file while IFS= read -r line; do echo "Line: $line" done < file.txt # Read from command while read -r file; do echo "Found: $file" done < <(find . -name "*.log") Functions 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 # Basic function greet() { echo "Hello, $1" } greet "World" # With local variables calculate() { local a=$1 local b=$2 echo $((a + b)) } result=$(calculate 5 3) echo "Result: $result" # Return values (0 = success, non-zero = failure) is_valid() { if [[ "$1" =~ ^[0-9]+$ ]]; then return 0 else return 1 fi } if is_valid "42"; then echo "Valid number" fi Arguments 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash # Positional arguments echo "Script: $0" echo "First arg: $1" echo "Second arg: $2" echo "All args: $@" echo "Arg count: $#" # Shift through args while [ $# -gt 0 ]; do echo "Arg: $1" shift done # Getopts for flags while getopts "vf:o:" opt; do case $opt in v) VERBOSE=true ;; f) FILE="$OPTARG" ;; o) OUTPUT="$OPTARG" ;; ?) echo "Usage: $0 [-v] [-f file] [-o output]"; exit 1 ;; esac done Error Handling 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 #!/bin/bash set -euo pipefail # Trap errors trap 'echo "Error on line $LINENO"; exit 1' ERR # Trap cleanup on exit cleanup() { echo "Cleaning up..." rm -f "$TEMP_FILE" } trap cleanup EXIT # Create temp file TEMP_FILE=$(mktemp) # Check command success if ! command -v docker &> /dev/null; then echo "Docker not installed" exit 1 fi # Or with || docker ps || { echo "Docker not running"; exit 1; } Useful Patterns Check if root 1 2 3 4 if [ "$EUID" -ne 0 ]; then echo "Please run as root" exit 1 fi Confirm before proceeding 1 2 3 4 5 read -p "Are you sure? (y/n) " -n 1 -r echo if [[ ! $REPLY =~ ^[Yy]$ ]]; then exit 1 fi Logging 1 2 3 4 5 6 7 8 LOG_FILE="/var/log/myscript.log" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } log "Starting script" log "Task completed" Config file 1 2 3 4 5 6 7 8 # config.sh DB_HOST="localhost" DB_PORT=5432 DB_NAME="myapp" # main.sh source ./config.sh echo "Connecting to $DB_HOST:$DB_PORT" Lock file (prevent concurrent runs) 1 2 3 4 5 6 7 8 9 10 11 LOCKFILE="/tmp/myscript.lock" if [ -f "$LOCKFILE" ]; then echo "Script already running" exit 1 fi trap "rm -f $LOCKFILE" EXIT touch "$LOCKFILE" # Your script here Arrays 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 # Define array SERVERS=("web1" "web2" "web3") # Access elements echo "${SERVERS[0]}" # First element echo "${SERVERS[@]}" # All elements echo "${#SERVERS[@]}" # Length # Loop over array for server in "${SERVERS[@]}"; do echo "Deploying to $server" done # Add element SERVERS+=("web4") # Associative array (bash 4+) declare -A PORTS PORTS[web]=80 PORTS[api]=8080 echo "${PORTS[web]}" String Operations 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 STR="Hello, World!" # Length echo "${#STR}" # 13 # Substring echo "${STR:0:5}" # Hello # Replace echo "${STR/World/Bash}" # Hello, Bash! # Replace all echo "${STR//o/0}" # Hell0, W0rld! # Remove prefix FILE="document.txt" echo "${FILE%.txt}" # document # Remove suffix PATH="/home/user/file.txt" echo "${PATH##*/}" # file.txt echo "${PATH%/*}" # /home/user Real Script Example 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 #!/bin/bash set -euo pipefail # Deploy script with logging and error handling SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" LOG_FILE="$SCRIPT_DIR/deploy.log" ENVIRONMENT="${1:-staging}" log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$ENVIRONMENT] $1" | tee -a "$LOG_FILE" } die() { log "ERROR: $1" exit 1 } cleanup() { log "Cleanup complete" } trap cleanup EXIT # Validate environment if [[ ! "$ENVIRONMENT" =~ ^(staging|production)$ ]]; then die "Invalid environment: $ENVIRONMENT" fi # Production confirmation if [ "$ENVIRONMENT" = "production" ]; then read -p "Deploy to PRODUCTION? (yes/no) " confirm [ "$confirm" = "yes" ] || die "Aborted" fi log "Starting deployment to $ENVIRONMENT" # Pull latest code log "Pulling latest code..." git pull origin main || die "Git pull failed" # Build log "Building..." npm run build || die "Build failed" # Deploy log "Deploying..." rsync -avz ./dist/ "deploy@$ENVIRONMENT.example.com:/var/www/" || die "Deploy failed" log "Deployment complete!" Quick Reference 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 # Variables VAR="value" VAR=${VAR:-default} VAR=$(command) # Conditionals [[ -f file ]] # File exists [[ -d dir ]] # Dir exists [[ -z "$var" ]] # Empty [[ "$a" == "$b" ]] # String equal [[ $n -eq 5 ]] # Numeric equal # Loops for i in list; do ...; done while cond; do ...; done # Functions func() { local x=$1; echo $x; } # Error handling set -euo pipefail trap 'cleanup' EXIT command || { echo "failed"; exit 1; } Bash isn’t pretty, but it’s everywhere. Master these patterns and you can automate anything. ...