> ## Documentation Index
> Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Linux Shell Scripting

> Automate tasks with Bash scripting

# Linux Shell Scripting

Automate repetitive tasks and build powerful scripts with Bash. Shell scripting is the duct tape of system administration -- it is how you turn a sequence of commands you type repeatedly into something the machine does for you. Every DevOps pipeline, every deployment script, every monitoring check started as a shell script.

***

## Basic Script Structure

Every Bash script starts with a **shebang** line that tells the system which interpreter to use.

```bash theme={null}
#!/bin/bash
# The shebang (#!/bin/bash) must be the very first line of the file.
# Without it, the system may try to interpret your script with the wrong shell.

# Comments start with # and are ignored by the interpreter.
# Write comments for your future self -- you will forget what the script does in 3 months.

echo "Hello, World!"
```

```bash theme={null}
# Make the script executable (required before you can run it directly)
chmod +x myscript.sh

# Run it
./myscript.sh

# Or explicitly invoke bash (does not require execute permission)
bash myscript.sh
```

<Tip>
  **Best practice**: Always start scripts with `set -euo pipefail` right after the shebang. This combination catches most common bugs: `-e` exits on any error, `-u` treats unset variables as errors, `-o pipefail` catches failures in piped commands. Without this, scripts silently continue after errors, which is how you accidentally delete the wrong directory.
</Tip>

```bash theme={null}
#!/bin/bash
set -euo pipefail

# Now if any command fails, the script stops immediately
# If you reference an undefined variable, the script stops
# If any command in a pipeline fails, the pipeline fails
```

***

## Variables

```bash theme={null}
#!/bin/bash

# Assign variables (no spaces around the = sign -- this is a common gotcha)
NAME="John"
AGE=30

# Use variables with $ prefix
echo "Name: $NAME"
echo "Age: $AGE"

# Use curly braces when the variable name could be ambiguous
echo "File: ${NAME}_report.txt"  # Without braces, bash looks for $NAME_report

# Command substitution -- capture the output of a command into a variable
CURRENT_DIR=$(pwd)
DATE=$(date +%Y-%m-%d)
FILE_COUNT=$(ls -1 | wc -l)
echo "Today is $DATE, we are in $CURRENT_DIR with $FILE_COUNT files"

# Special variables (these are set automatically)
echo "Script name: $0"          # The name of the script itself
echo "First argument: $1"       # First argument passed to the script
echo "All arguments: $@"        # All arguments as separate words
echo "Argument count: $#"       # Number of arguments
echo "Last exit code: $?"       # Exit code of the last command (0 = success)
echo "Current PID: $$"          # Process ID of the current script
```

<Warning>
  **Always quote your variables**: Use `"$VAR"` not `$VAR`. Without quotes, a variable containing spaces gets split into multiple words, causing subtle bugs. For example, `rm $FILE` where FILE="my report.txt" tries to delete "my" and "report.txt" separately. `rm "$FILE"` does the right thing.
</Warning>

***

## Conditionals

Bash conditionals test exit codes. In Linux, an exit code of 0 means success (true), and anything else means failure (false). This is the opposite of most programming languages.

```bash theme={null}
#!/bin/bash

# Test based on script arguments
if [ "$1" == "start" ]; then
    echo "Starting..."
elif [ "$1" == "stop" ]; then
    echo "Stopping..."
else
    echo "Usage: $0 {start|stop}"
    exit 1  # Exit with error code to signal incorrect usage
fi
```

### File and String Tests

```bash theme={null}
# File tests (the checks you use most often in real scripts)
if [ -f "/etc/nginx/nginx.conf" ]; then
    echo "Nginx config exists"   # -f tests if a regular file exists
fi

if [ -d "/var/log" ]; then
    echo "Log directory exists"  # -d tests if a directory exists
fi

if [ -r "$FILE" ]; then
    echo "File is readable"      # -r tests read permission
fi

if [ -s "$FILE" ]; then
    echo "File is not empty"     # -s tests if file exists and has size > 0
fi

# String tests
if [ -z "$VAR" ]; then
    echo "Variable is empty"     # -z tests for zero-length string
fi

if [ -n "$VAR" ]; then
    echo "Variable is not empty" # -n tests for non-zero-length string
fi

# Numeric comparisons (use -eq, -ne, -lt, -gt, not == or <)
if [ "$COUNT" -gt 10 ]; then
    echo "More than 10 items"
fi
```

### Modern Test Syntax

```bash theme={null}
# Double brackets [[ ]] are the modern, safer alternative to [ ]
# They support pattern matching and do not require quoting variables
if [[ "$HOSTNAME" == *prod* ]]; then
    echo "This is a production server"
fi

# Combine conditions with && (AND) and || (OR)
if [[ -f "$CONFIG" && -r "$CONFIG" ]]; then
    echo "Config file exists and is readable"
fi
```

***

## Loops

```bash theme={null}
#!/bin/bash

# For loop with a range
for i in {1..5}; do
    echo "Number: $i"
done

# For loop over a list of values
for env in dev staging production; do
    echo "Deploying to $env..."
done

# While loop with a counter
counter=1
while [ $counter -le 5 ]; do
    echo "Count: $counter"
    ((counter++))
done

# Loop through files in a directory (a very common pattern)
for file in /var/log/*.log; do
    echo "Processing: $file"
    wc -l "$file"  # Count lines in each log file
done

# Read a file line by line (use this for config files, server lists, etc.)
while IFS= read -r line; do
    echo "Server: $line"
    ssh "$line" "uptime"
done < servers.txt
```

<Tip>
  **Gotcha with for loops and files**: If no files match a glob pattern like `*.txt`, bash passes the literal string "\*.txt" to the loop. Protect against this with `shopt -s nullglob` at the top of your script, which makes unmatched globs expand to nothing instead.
</Tip>

***

## Functions

Functions let you organize code into reusable blocks. They are essential once your script grows beyond 20-30 lines.

```bash theme={null}
#!/bin/bash

# Define a function (two equivalent syntaxes)
greet() {
    local name="$1"  # 'local' keeps the variable scoped to this function
    echo "Hello, $name!"
}

# Call the function with an argument
greet "Alice"
greet "Bob"

# A more practical example: a function that logs with timestamps
log() {
    local level="$1"
    local message="$2"
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] [$level] $message"
}

log "INFO" "Deployment started"
log "ERROR" "Connection to database failed"

# Functions can return exit codes (0 = success, 1+ = failure)
check_service() {
    if systemctl is-active --quiet "$1"; then
        return 0  # Success
    else
        return 1  # Failure
    fi
}

if check_service "nginx"; then
    log "INFO" "Nginx is running"
else
    log "ERROR" "Nginx is down, attempting restart..."
    sudo systemctl start nginx
fi
```

***

## Practical Script: Deployment Example

Here is a realistic script that ties together variables, conditionals, functions, and error handling. This is the kind of script you would actually use in production -- notice how every step validates its preconditions, logs what it does, and has a path for failure.

```bash theme={null}
#!/bin/bash
set -euo pipefail

# ============================================================
# Configuration -- all tunables at the top for easy adjustment
# ============================================================
APP_NAME="myapp"
DEPLOY_DIR="/opt/$APP_NAME"
BACKUP_DIR="/opt/backups"
LOG_FILE="/var/log/${APP_NAME}-deploy.log"
HEALTH_CHECK_RETRIES=5        # How many times to check if the service is healthy
HEALTH_CHECK_INTERVAL=2       # Seconds between health checks

# ============================================================
# Helper function: timestamped logging to both stdout and file
# ============================================================
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE"
}

# ============================================================
# Input validation -- fail fast with a clear error message
# ============================================================
if [ $# -lt 1 ]; then
    echo "Usage: $0 <artifact-path>"
    echo "Example: $0 /tmp/myapp-v2.3.1.tar.gz"
    exit 1
fi

ARTIFACT="$1"

if [ ! -f "$ARTIFACT" ]; then
    log "ERROR: Artifact not found: $ARTIFACT"
    exit 1
fi

# ============================================================
# Backup current version before deploying (you will thank yourself later)
# ============================================================
log "Creating backup..."
mkdir -p "$BACKUP_DIR"
BACKUP_PATH="$BACKUP_DIR/${APP_NAME}-$(date +%Y%m%d-%H%M%S)"
cp -r "$DEPLOY_DIR" "$BACKUP_PATH"
log "Backup saved to $BACKUP_PATH"

# ============================================================
# Deploy new version
# ============================================================
log "Deploying $ARTIFACT..."
tar -xzf "$ARTIFACT" -C "$DEPLOY_DIR"

# ============================================================
# Restart the service and verify it came up healthy
# ============================================================
log "Restarting $APP_NAME..."
sudo systemctl restart "$APP_NAME"

# Check health with retries -- services often need a few seconds to initialize
for i in $(seq 1 $HEALTH_CHECK_RETRIES); do
    sleep "$HEALTH_CHECK_INTERVAL"
    if systemctl is-active --quiet "$APP_NAME"; then
        log "Deployment successful (healthy after $((i * HEALTH_CHECK_INTERVAL))s)"
        exit 0
    fi
    log "Health check $i/$HEALTH_CHECK_RETRIES: not ready yet..."
done

# If we get here, the service never became healthy
log "ERROR: Service failed to start after deployment. Rolling back..."
rm -rf "$DEPLOY_DIR"
cp -r "$BACKUP_PATH" "$DEPLOY_DIR"
sudo systemctl restart "$APP_NAME"
log "Rolled back to previous version"
exit 1
```

<Tip>
  **What makes this production-ready**: (1) It validates inputs before doing anything destructive. (2) It creates a backup before overwriting. (3) It uses retries for the health check instead of a single fixed sleep. (4) It rolls back automatically on failure. A script without rollback capability is a script you will regret running at 2 AM.
</Tip>

***

## Cron Jobs

Cron is the Linux scheduler. It runs scripts at specified times, unattended. Think of it as setting an alarm for your server.

```bash theme={null}
# Edit your crontab (each user has their own)
crontab -e

# Cron format: minute hour day-of-month month day-of-week command
# ┌───────── minute (0-59)
# │ ┌─────── hour (0-23)
# │ │ ┌───── day of month (1-31)
# │ │ │ ┌─── month (1-12)
# │ │ │ │ ┌─ day of week (0-7, 0 and 7 are Sunday)
# │ │ │ │ │
# * * * * * command

# Run backup every day at 2:00 AM
0 2 * * * /opt/scripts/backup.sh >> /var/log/backup.log 2>&1

# Run health check every 5 minutes
*/5 * * * * /opt/scripts/healthcheck.sh

# Run cleanup every Sunday at midnight
0 0 * * 0 /opt/scripts/cleanup.sh

# Run report on the first day of every month at 9:00 AM
0 9 1 * * /opt/scripts/monthly-report.sh

# View current cron jobs
crontab -l
```

<Warning>
  **Common cron pitfalls**: (1) Cron runs in a minimal environment -- your `$PATH` is different from your interactive shell, so use full paths to commands (`/usr/bin/python3` not just `python3`). (2) Always redirect output to a log file (`>> /var/log/myscript.log 2>&1`), otherwise cron sends email for every run. (3) Cron does not load your `.bashrc` -- if your script depends on environment variables, source them explicitly.
</Warning>

***

## Key Takeaways

* Start every script with `#!/bin/bash` and `set -euo pipefail`
* Always quote variables (`"$VAR"`) to handle spaces and special characters safely
* Use `local` in functions to avoid polluting the global scope
* Test files with `-f`, `-d`, `-r`, `-s` before operating on them
* Use `[[ ]]` (double brackets) instead of `[ ]` for safer, more powerful tests
* Cron runs in a minimal environment -- use full paths and redirect output
* Write logs with timestamps in production scripts so you can trace what happened

***

Congratulations -- you have completed the Linux Crash Course.

Next: [Docker Crash Course →](/courses/devops-tools/docker-overview)
