Steven Rosales

Shell Scripting for DevOps

The goal is to document useful Bash scripting patterns that can be reused for troubleshooting, automation, health checks, log analysis, deployment validation, and daily Linux operations.


Table of Contents

1. Basic Shell Script Structure

A basic Bash script usually starts with a shebang, variables, commands, and optional logic.

#!/bin/bash

MESSAGE="Hello from Bash"
echo "$MESSAGE"

Run the script:

chmod +x script.sh
./script.sh

Basic structure:

#!/bin/bash

# Variables
APP_NAME="my-app"
ENVIRONMENT="dev"

# Command output
echo "Starting script for $APP_NAME in $ENVIRONMENT"

# Logic
if [ "$ENVIRONMENT" = "dev" ]; then
  echo "Running in development mode"
fi

2. Variables

Variables store values that can be reused in the script.

#!/bin/bash

APP_NAME="payment-service"
PORT=8080
ENVIRONMENT="production"

echo "Application: $APP_NAME"
echo "Port: $PORT"
echo "Environment: $ENVIRONMENT"

Use quotes around variables to avoid issues with spaces or empty values.

Good:

if [ "$APP_NAME" = "payment-service" ]; then
  echo "App matched"
fi

Avoid:

if [ $APP_NAME = payment-service ]; then
  echo "App matched"
fi

Why quotes matter:

APP_NAME="payment service"

if [ "$APP_NAME" = "payment service" ]; then
  echo "App matched"
fi

Without quotes, values with spaces can break the script.


3. Script Arguments

Arguments allow you to pass values to a script from the command line.

#!/bin/bash

echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Number of arguments: $#"

Run:

./deploy.sh dev payment-service

Example output:

Script name: ./deploy.sh
First argument: dev
Second argument: payment-service
Number of arguments: 2

Common use:

#!/bin/bash

ENVIRONMENT=$1
APP_NAME=$2

echo "Deploying $APP_NAME to $ENVIRONMENT"

Common special variables:

$0   Script name
$1   First argument
$2   Second argument
$3   Third argument
$#   Number of arguments
$?   Exit code of the last command
$$   Process ID of the current script
$@   All arguments

4. Validate Required Arguments

Always validate input before running automation.

#!/bin/bash

if [ $# -lt 2 ]; then
  echo "Usage: $0 <environment> <app-name>"
  exit 1
fi

ENVIRONMENT=$1
APP_NAME=$2

echo "Deploying $APP_NAME to $ENVIRONMENT"

This prevents accidental or incomplete execution.

Example with allowed values:

#!/bin/bash

ENVIRONMENT=$1

if [ "$ENVIRONMENT" != "dev" ] && [ "$ENVIRONMENT" != "staging" ] && [ "$ENVIRONMENT" != "prod" ]; then
  echo "Invalid environment. Use: dev, staging, or prod"
  exit 1
fi

echo "Environment selected: $ENVIRONMENT"

5. Exit Codes

Every command returns an exit code.

0        Success
Non-zero Failure

Check the last command with $?:

#!/bin/bash

systemctl restart nginx

if [ $? -eq 0 ]; then
  echo "Nginx restarted successfully"
else
  echo "Failed to restart Nginx"
  exit 1
fi

Better style:

#!/bin/bash

if systemctl restart nginx; then
  echo "Nginx restarted successfully"
else
  echo "Failed to restart Nginx"
  exit 1
fi

Why this matters in DevOps:


6. Conditions

Conditions are used to make decisions inside scripts.

String comparison

#!/bin/bash

ENVIRONMENT="prod"

if [ "$ENVIRONMENT" = "prod" ]; then
  echo "Production environment"
else
  echo "Non-production environment"
fi

Number comparison

#!/bin/bash

CPU_USAGE=85

if [ $CPU_USAGE -ge 90 ]; then
  echo "Critical CPU usage"
elif [ $CPU_USAGE -ge 70 ]; then
  echo "Warning: High CPU usage"
else
  echo "CPU usage is normal"
fi

Common numeric operators:

-eq  Equal
-ne  Not equal
-gt  Greater than
-ge  Greater than or equal
-lt  Less than
-le  Less than or equal

File checks

#!/bin/bash

FILE="/var/log/syslog"

if [ -f "$FILE" ]; then
  echo "File exists: $FILE"
else
  echo "File not found: $FILE"
fi

Common file checks:

-f  File exists and is a regular file
-d  Directory exists
-r  File is readable
-w  File is writable
-x  File is executable

Example checking if a directory exists:

#!/bin/bash

DIR="/var/log"

if [ -d "$DIR" ]; then
  echo "Directory exists: $DIR"
else
  echo "Directory not found: $DIR"
fi

7. Case Statement

Use case when there are multiple options.

#!/bin/bash

ACTION=$1
SERVICE=$2

case $ACTION in
  start)
    systemctl start "$SERVICE"
    ;;
  stop)
    systemctl stop "$SERVICE"
    ;;
  restart)
    systemctl restart "$SERVICE"
    ;;
  status)
    systemctl status "$SERVICE"
    ;;
  *)
    echo "Usage: $0 {start|stop|restart|status} <service>"
    exit 1
    ;;
esac

Run:

./service-manager.sh restart nginx

Common use cases:


8. Loops

Loops repeat tasks.

For loop

#!/bin/bash

for SERVER in web01 web02 web03; do
  echo "Checking server: $SERVER"
done

Loop through files

#!/bin/bash

for FILE in /var/log/*.log; do
  echo "Processing $FILE"
done

While loop

#!/bin/bash

COUNT=1

while [ $COUNT -le 5 ]; do
  echo "Attempt $COUNT"
  COUNT=$((COUNT + 1))
done

Infinite monitoring loop

#!/bin/bash

while true; do
  date
  uptime
  sleep 10
done

Use infinite loops carefully.

Break example

break exits a loop immediately.

#!/bin/bash

for NUMBER in 1 2 3 4 5; do
  if [ $NUMBER -eq 3 ]; then
    break
  fi

  echo "$NUMBER"
done

Continue example

continue skips the current iteration and moves to the next one.

#!/bin/bash

for NUMBER in 1 2 3 4 5; do
  if [ $NUMBER -eq 3 ]; then
    continue
  fi

  echo "$NUMBER"
done

9. Functions

Functions help organize reusable logic.

#!/bin/bash

log_info() {
  echo "[INFO] $1"
}

log_error() {
  echo "[ERROR] $1"
}

log_info "Starting deployment"
log_error "Deployment failed"

Example with service check:

#!/bin/bash

check_service() {
  SERVICE=$1

  if systemctl is-active --quiet "$SERVICE"; then
    echo "$SERVICE is running"
  else
    echo "$SERVICE is not running"
  fi
}

check_service nginx
check_service ssh

Why functions are useful:


10. Reading User Input

Use read for interactive scripts.

#!/bin/bash

read -p "Enter service name: " SERVICE
systemctl status "$SERVICE"

Confirmation example:

#!/bin/bash

read -p "Are you sure you want to restart nginx? yes/no: " ANSWER

if [ "$ANSWER" = "yes" ]; then
  systemctl restart nginx
else
  echo "Cancelled"
fi

Useful for:


11. Command Substitution

Command substitution stores command output in a variable.

#!/bin/bash

CURRENT_DATE=$(date)
HOST=$(hostname)
UPTIME=$(uptime -p)

echo "Date: $CURRENT_DATE"
echo "Host: $HOST"
echo "Uptime: $UPTIME"

Example:

FAILED_LOGINS=$(grep "Failed password" /var/log/auth.log | wc -l)
echo "Failed SSH logins: $FAILED_LOGINS"

Preferred syntax:

VALUE=$(command)

Older syntax:

VALUE=`command`

The $(command) syntax is preferred because it is easier to read.


12. Input and Output Redirection

Redirection sends output, errors, or input to/from files.

Overwrite a file:

echo "Starting deployment" > deploy.log

Append to a file:

echo "Deployment completed" >> deploy.log

Redirect errors:

ls /wrong/path 2> error.log

Redirect output and errors:

command > output.log 2>&1

Using tee to show output and save it:

kubectl get pods -A | tee pods-output.txt

Append with tee:

kubectl get events -A | tee -a troubleshooting.log

Useful for:


13. Pipes

Pipes send the output of one command to another command.

ps aux | grep nginx

Count failed SSH logins:

grep "Failed password" /var/log/auth.log | wc -l

Find most common IPs in a log:

awk '{print $1}' access.log | sort | uniq -c | sort -nr | head

Find errors in application logs:

grep -i "error" app.log | tail -n 50

Useful for:


14. Useful Script Safety Options

These options make scripts safer.

#!/bin/bash
set -euo pipefail

Meaning:

set -e          Exit if a command fails
set -u          Exit if using an undefined variable
set -o pipefail Fail if any command in a pipe fails

Example:

#!/bin/bash
set -euo pipefail

echo "Starting backup"
tar -czf backup.tar.gz /important/data
echo "Backup completed"

Use this carefully because the script exits immediately when something fails.

Recommended for:


15. Logging Pattern

A simple logging pattern makes scripts easier to troubleshoot.

#!/bin/bash

LOG_FILE="script.log"

log() {
  echo "$(date '+%Y-%m-%d %H:%M:%S') - $1" | tee -a "$LOG_FILE"
}

log "Script started"
log "Checking disk usage"
df -h | tee -a "$LOG_FILE"
log "Script completed"

Why this is useful:


16. Basic DevOps Script Examples

Example 1: Service Health Check

#!/bin/bash

SERVICE=$1

if [ $# -ne 1 ]; then
  echo "Usage: $0 <service-name>"
  exit 1
fi

if systemctl is-active --quiet "$SERVICE"; then
  echo "$SERVICE is running"
else
  echo "$SERVICE is not running"
  exit 1
fi

Run:

./check-service.sh nginx

Example 2: Disk Usage Alert

#!/bin/bash

THRESHOLD=80
USAGE=$(df / | awk 'NR==2 {print $5}' | sed 's/%//')

if [ "$USAGE" -ge "$THRESHOLD" ]; then
  echo "WARNING: Disk usage is ${USAGE}%"
else
  echo "Disk usage is normal: ${USAGE}%"
fi

Example 3: Check if a Website is Up

#!/bin/bash

URL=$1

if [ $# -ne 1 ]; then
  echo "Usage: $0 <url>"
  exit 1
fi

if curl -s --fail "$URL" > /dev/null; then
  echo "Site is reachable: $URL"
else
  echo "Site is down or unreachable: $URL"
  exit 1
fi

Run:

./check-url.sh https://example.com

Example 4: Collect Linux Troubleshooting Information

#!/bin/bash

OUTPUT="system-report-$(date +%F-%H%M%S).log"

{
  echo "===== HOSTNAME ====="
  hostname

  echo "===== UPTIME ====="
  uptime

  echo "===== DISK USAGE ====="
  df -h

  echo "===== MEMORY ====="
  free -h

  echo "===== TOP CPU PROCESSES ====="
  ps aux --sort=-%cpu | head

  echo "===== TOP MEMORY PROCESSES ====="
  ps aux --sort=-%mem | head

  echo "===== LISTENING PORTS ====="
  ss -tulpn
} | tee "$OUTPUT"

echo "Report saved to $OUTPUT"

Example 5: Search Errors in Logs

#!/bin/bash

LOG_FILE=$1

if [ $# -ne 1 ]; then
  echo "Usage: $0 <log-file>"
  exit 1
fi

if [ ! -f "$LOG_FILE" ]; then
  echo "File not found: $LOG_FILE"
  exit 1
fi

echo "Last 50 errors from $LOG_FILE"
grep -i "error" "$LOG_FILE" | tail -n 50

Example 6: Kubernetes Pod Quick Check

#!/bin/bash

NAMESPACE=${1:-default}

echo "Checking pods in namespace: $NAMESPACE"

kubectl get pods -n "$NAMESPACE" -o wide

echo "Recent events:"
kubectl get events -n "$NAMESPACE" --sort-by=.metadata.creationTimestamp | tail -n 20

Run:

./k8s-check.sh default
./k8s-check.sh production

Example 7: Kubernetes Logs Collector

#!/bin/bash

NAMESPACE=$1
POD=$2

if [ $# -ne 2 ]; then
  echo "Usage: $0 <namespace> <pod-name>"
  exit 1
fi

OUTPUT="${POD}-logs-$(date +%F-%H%M%S).log"

kubectl logs "$POD" -n "$NAMESPACE" | tee "$OUTPUT"

echo "Logs saved to $OUTPUT"

Example 8: Restart a Kubernetes Deployment

#!/bin/bash

NAMESPACE=$1
DEPLOYMENT=$2

if [ $# -ne 2 ]; then
  echo "Usage: $0 <namespace> <deployment-name>"
  exit 1
fi

echo "Restarting deployment $DEPLOYMENT in namespace $NAMESPACE"

kubectl rollout restart deployment "$DEPLOYMENT" -n "$NAMESPACE"
kubectl rollout status deployment "$DEPLOYMENT" -n "$NAMESPACE"

Example 9: Simple Backup Script

#!/bin/bash

SOURCE_DIR=$1
BACKUP_DIR=$2

if [ $# -ne 2 ]; then
  echo "Usage: $0 <source-dir> <backup-dir>"
  exit 1
fi

TIMESTAMP=$(date +%F-%H%M%S)
BACKUP_FILE="backup-$TIMESTAMP.tar.gz"

tar -czf "$BACKUP_DIR/$BACKUP_FILE" "$SOURCE_DIR"

echo "Backup created: $BACKUP_DIR/$BACKUP_FILE"

Example 10: Deployment Validation Script

#!/bin/bash

APP_URL=$1
EXPECTED_STATUS=200

if [ $# -ne 1 ]; then
  echo "Usage: $0 <app-url>"
  exit 1
fi

STATUS_CODE=$(curl -s -o /dev/null -w "%{http_code}" "$APP_URL")

if [ "$STATUS_CODE" -eq "$EXPECTED_STATUS" ]; then
  echo "Deployment validation passed. HTTP $STATUS_CODE"
else
  echo "Deployment validation failed. HTTP $STATUS_CODE"
  exit 1
fi

17. Bash Scripting Best Practices