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.shVariables
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: 2Conditionals
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"
fiCommon test operators:
| Operator | Meaning |
|---|---|
-f file | File exists and is a regular file |
-d dir | Directory exists |
-e path | Path exists (file or directory) |
-r file | File is readable |
-w file | File is writable |
-x file | File is executable |
-z string | String is empty |
-n string | String is not empty |
str1 = str2 | Strings are equal |
num1 -eq num2 | Numbers are equal |
num1 -gt num2 | Greater than |
num1 -lt num2 | Less 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"
doneWhile loop — repeat while a condition is true:
#!/bin/bash
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1))
doneReading a file line by line:
#!/bin/bash
while IFS= read -r line; do
echo "Line: $line"
done < servers.txtFunctions
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 errorThe 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
donePractical 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
fiThe set -euo pipefail at the top is a best practice:
| Flag | Effect |
|---|---|
-e | Exit immediately if any command fails |
-u | Treat unset variables as errors |
-o pipefail | Fail 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.