Skip to main content
Linux Command Line·Lesson 5 of 5

Shell Scripting

Shell scripting lets you combine terminal commands into reusable programs. Instead of typing the same sequence of commands every day, you write a script once and run it whenever you need it.

Your First Script

Create a file called hello.sh:

#!/bin/bash

echo "Hello, world!"
echo "Today is $(date)"
echo "You are logged in as $(whoami)"

The first line #!/bin/bash is called the shebang. It tells the system which interpreter to use.

Make it executable and run it:

chmod +x hello.sh
./hello.sh

Variables

Variables store values for later use. In bash, there are no spaces around the = sign:

#!/bin/bash

name="Sabaoon"
project="DevOps Pipeline"
version=3

echo "Author: $name"
echo "Project: $project"
echo "Version: $version"

Command substitution captures the output of a command into a variable:

current_date=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)
hostname=$(hostname)

echo "Date: $current_date"
echo "Files in directory: $file_count"
echo "Host: $hostname"

Reading user input:

#!/bin/bash

echo -n "Enter your name: "
read username

echo "Welcome, $username!"

Script Arguments

Scripts can accept arguments from the command line:

#!/bin/bash

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

Running it:

./greet.sh Alice Bob
# Script name: ./greet.sh
# First argument: Alice
# Second argument: Bob
# All arguments: Alice Bob
# Number of arguments: 2

Conditionals

Use if statements to make decisions:

#!/bin/bash

file=$1

if [ -z "$file" ]; then
    echo "Usage: $0 <filename>"
    exit 1
fi

if [ -f "$file" ]; then
    echo "$file exists and is a regular file"
    echo "Size: $(wc -c < "$file") bytes"
elif [ -d "$file" ]; then
    echo "$file is a directory"
else
    echo "$file does not exist"
fi

Common test operators:

OperatorMeaning
-f fileFile exists and is a regular file
-d dirDirectory exists
-e pathPath exists (file or directory)
-r fileFile is readable
-w fileFile is writable
-x fileFile is executable
-z stringString is empty
-n stringString is not empty
str1 = str2Strings are equal
num1 -eq num2Numbers are equal
num1 -gt num2Greater than
num1 -lt num2Less than

Loops

For loop — iterate over a list:

#!/bin/bash

# Loop over files
for file in *.log; do
    echo "Processing $file"
    wc -l "$file"
done

# Loop over a range of numbers
for i in {1..5}; do
    echo "Iteration $i"
done

# C-style for loop
for ((i=0; i<10; i++)); do
    echo "Count: $i"
done

While loop — repeat while a condition is true:

#!/bin/bash

count=1
while [ $count -le 5 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

Reading a file line by line:

#!/bin/bash

while IFS= read -r line; do
    echo "Line: $line"
done < servers.txt

Functions

Functions let you organize reusable blocks of code:

#!/bin/bash

log() {
    local timestamp=$(date "+%Y-%m-%d %H:%M:%S")
    echo "[$timestamp] $1"
}

check_service() {
    local service=$1
    if systemctl is-active --quiet "$service"; then
        log "$service is running"
        return 0
    else
        log "$service is NOT running"
        return 1
    fi
}

# Use the functions
log "Starting health check"
check_service "nginx"
check_service "postgresql"
log "Health check complete"

The local keyword restricts a variable's scope to the function. Always use it to avoid polluting the global scope.

Exit Codes

Every command returns an exit code. 0 means success, anything else means failure:

#!/bin/bash

mkdir /tmp/test-dir
if [ $? -eq 0 ]; then
    echo "Directory created successfully"
else
    echo "Failed to create directory"
fi

# Set your own exit code
exit 0  # Success
exit 1  # General error

The special variable $? holds the exit code of the last command.

String Operations

#!/bin/bash

filename="backup-2026-03-24.tar.gz"

# String length
echo "Length: ${#filename}"

# Substring extraction
echo "First 6 chars: ${filename:0:6}"

# Replace first occurrence
echo "${filename/backup/archive}"

# Replace all occurrences
echo "${filename//-/_}"

# Remove prefix pattern
echo "${filename#backup-}"

# Remove suffix pattern
echo "${filename%.tar.gz}"

Arrays

#!/bin/bash

# Define an array
servers=("web01" "web02" "db01" "cache01")

# Access an element
echo "First server: ${servers[0]}"

# All elements
echo "All servers: ${servers[@]}"

# Array length
echo "Server count: ${#servers[@]}"

# Loop over array
for server in "${servers[@]}"; do
    echo "Pinging $server..."
    ping -c 1 -W 2 "$server" > /dev/null 2>&1
    if [ $? -eq 0 ]; then
        echo "  $server is reachable"
    else
        echo "  $server is unreachable"
    fi
done

Practical Example: Deployment Script

Here is a real-world deployment script that pulls code, builds, and restarts a service:

#!/bin/bash

set -euo pipefail

APP_DIR="/var/www/myapp"
LOG_FILE="/var/log/deploy.log"
BRANCH="${1:-main}"

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

log "Starting deployment of branch: $BRANCH"

# Pull latest code
cd "$APP_DIR"
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
log "Code updated to latest $BRANCH"

# Install dependencies
pnpm install --frozen-lockfile
log "Dependencies installed"

# Build the application
pnpm build
log "Build completed"

# Restart the service
sudo systemctl restart myapp
log "Service restarted"

# Verify the service is running
sleep 2
if systemctl is-active --quiet myapp; then
    log "Deployment successful — service is running"
else
    log "ERROR: Service failed to start after deployment"
    exit 1
fi

The set -euo pipefail at the top is a best practice:

FlagEffect
-eExit immediately if any command fails
-uTreat unset variables as errors
-o pipefailFail if any command in a pipe fails

Debugging Scripts

# Run with debug output (shows each command before execution)
bash -x script.sh

# Enable debug mode inside a script
set -x  # Turn on
set +x  # Turn off

# Print variables for inspection
echo "DEBUG: variable=$variable"

Summary

You can now write shell scripts with variables, conditionals, loops, functions, and error handling. You learned practical patterns for automation, from simple file processing to full deployment scripts. These skills form the foundation for DevOps — automating everything that can be automated.