Shell Scripting for DevOps: A Complete Practical Guide

Shell scripting is one of the most important skills for any DevOps engineer. Whether you're automating server setup, deploying applications, or managing infrastructure, bash scripts are everywhere. This guide covers everything from basics to real-world automation patterns.
1. Script Structure & Shebang
Every shell script starts with a shebang line that tells the OS which interpreter to use.
#!/bin/bash
VAR_NAME=VALUE
Run a script and pass arguments:
sh script-name.sh first-arg second-arg
$1→ first argument (first-arg)$2→ second argument (second-arg)... and so on for
\(3,\)4, etc.
2. Reading Input & Command Substitution
read VAR_NAME
VAR_NAME=$(command)
read takes user input interactively. Command substitution $(command) captures the output of a command into a variable — extremely useful for storing results like IP addresses, hostnames, or version numbers.
3. Built-in Shell Variables
Bash automatically provides several useful variables:
echo "All variables passed to script: $@"
echo "Number of variables passed: $#"
echo "First variable: $1"
echo "Script name: $0"
echo "Who is running this: $USER"
echo "Which directory: $PWD"
echo "Home directory: $HOME"
echo "PID of the current script: $$"
sleep 5 &
echo "PID of the background command running just now: $!"
wait $!
echo "Line number: $LINENO"
echo "Script executed in $SECONDS seconds"
echo "Random number: $RANDOM"
| Variable | Meaning |
|---|---|
$@ |
All arguments passed to script |
$# |
Number of arguments |
\(1, \)2... |
Positional arguments |
$0 |
Script name |
$USER |
Current logged-in user |
$PWD |
Present working directory |
$HOME |
Home directory of the user |
$$ |
PID of current script |
$! |
PID of the last background process |
$LINENO |
Current line number in script |
$SECONDS |
Time elapsed since the script started |
$RANDOM |
Random integer generator |
4. Exit Codes
$?
Every command returns an exit code after execution:
0→ success1–127→ failure (different numbers represent different failure reasons)
Best practice: It is the script writer's responsibility to check the exit code of every critical command and handle failures gracefully — never assume success.
command
if [ $? -eq 0 ]; then
echo "Success"
else
echo "Failed with exit code $?"
exit 1
fi
5. Variables & Data Types
Shell, by default, treats every variable as a string. There's no strict type system like in programming languages (Java/Python), where you'd declare:
int i = 10;
String name = "siva123";
Common data types across languages for reference: int, float, long, decimal, boolean, string, char, double, array, arraylist, map, set.
Example — type matters in real systems:
| Rocket | Speed | Data Type |
|---|---|---|
| Ariane 4 | 9 km/sec | Integer |
| Ariane 5 | 8.2 km/sec | Float |
integer speed = 8.2 # wrong - would cause data loss/precision issue
float speed = 8.2 # correct
This is a famous real-world failure case — Ariane 5's flight software reused Ariane 4's integer-based code for a value that needed to be a float, leading to an overflow and the rocket's destruction shortly after launch.
6. Conditional Statements
General if-else (language agnostic pseudocode)
if (expression) {
execute these lines if expression is true
}
if (expression) {
execute if true
} else {
execute if false
}
if (expression) {
...
} else if (expression) {
...
} else if (expression) {
...
} else {
...
}
Example logic
number = 20
if (number > 10) {
print "20 is greater than 10"
} else {
print "20 is less than 10"
}
Bash equivalent
NUMBER=20
if [ $NUMBER -gt 20 ]; then
echo "number is greater than 20"
else
echo "number is less than 20"
fi
Common comparison operators
| Operator | Meaning | |
|---|---|---|
-eq |
equal to | |
-ne |
not equal to | |
-gt |
greater than | |
-lt |
less than | |
-ge |
greater than or equal | |
-le |
less than or equal to | |
-z |
string is empty | |
-n |
string is not empty | |
-f |
file exists | |
-d |
directory exists |
7. Real Use Case: Install MySQL via Shell Script
Requirements:
The user should have root access or run the script with root access.
If not root, exit the script with a proper reason.
Check if the package is already installed.
If installed → print "already installed".
If not installed → install it.
Install the package.
Check exit status:
0→ show successnon-zero → show failure
Sample script:
#!/bin/bash
# Step 1: Check root access
if [ $USERID -ne 0 ]; then
echo "ERROR: Please run this script with root access"
exit 1
fi
# Step 2: Check if mysql is already installed
dnf list installed mysql &>/dev/null
if [ $? -eq 0 ]; then
echo "MySQL is already installed"
else
dnf install mysql -y &>/dev/null
if [ $? -eq 0 ]; then
echo "MySQL installed successfully"
else
echo "MySQL installation failed"
exit 1
fi
fi
Always validate: (1) permissions, (2) idempotency (don't reinstall what's already there), and (3) exit status of every critical operation.
8. Functions
Functions help you avoid repeating code. Write the logic once, call it multiple times with different arguments.
Benefits:
Fewer lines of code
Less time to write/maintain
Less resource usage / better readability
FUNC_NAME(){
\(1 \)2
# keep the repeated logic here
}
FUNC_NAME arg-1 arg-2
Practical example — logging function:
LOG(){
echo "\((date +%F-%T) - \)1"
}
LOG "Starting installation"
LOG "Installation completed"
9. Redirections
Redirection controls where command output and errors go.
| Symbol | Meaning |
|---|---|
< |
input redirection |
> |
output redirection (overwrite) |
>> |
append to output |
1 |
stdout (success/normal output) |
2 |
stderr (error output) |
& |
both stdout and stderr |
Examples:
ls -l 1> output.log # redirect success output
ls -l 2> error.log # redirect error output
ls -l 1> output.log 2> error.log
ls -l &> all_output.log # both stdout and stderr to same file
ls -l >> output.log # append instead of overwrite
10. Loops
General pseudocode (for loop)
for (int i = 0; i < 5; i++) {
print i;
}
Bash for loop — iterating over script arguments
#!/bin/bash
# usage: sh loop.sh mysql nginx
for SERVICE in "$@"
do
echo "Processing: $SERVICE"
done
for <expression>
do
statements
done
11. Colored Output in Terminal (ANSI Escape Codes)
Bash supports ANSI color codes to make log output more readable.
| Color | Code |
|---|---|
| Red | 31 |
| Green | 32 |
| Yellow | 33 |
Format: \e[<code>m
R="\e[31m"
G="\e[32m"
Y="\e[33m"
N="\e[0m" # reset to normal
echo -e "\({G}2026-05-25 08:34:44 INFO Installing Mysql\){N}"
echo -e "\({R}2026-05-25 08:34:50 ERROR Installation failed\){N}"
echo -e "\({Y}2026-05-25 08:35:00 WARNING Service not started\){N}"
12. Error Handling
There are two broad categories of errors:
Expected errors — anticipated failure points (e.g., package not found, service not running, permission denied). You write conditional checks for these.
Unexpected errors — failures you didn't predict (e.g., disk full, network timeout). Use
set -e,trap, and logging to catch these gracefully.
set -e # exit immediately if any command fails
trap 'echo "Error occurred on line $LINENO"; exit 1' ERR
13. Real-World Example: AWS Security Group via Script
Goal: Create a security group named roboshop-common for roboshop-mongodb.
Requirements:
- Script should run with root access (or have correctly configured AWS credentials).
#!/bin/bash
if [ $USERID -ne 0 ]; then
echo "Please run with root access"
exit 1
fi
aws ec2 create-security-group \
--group-name roboshop-common \
--description "Common SG for Roboshop services" \
--vpc-id <vpc-id>
if [ $? -eq 0 ]; then
echo "Security group created successfully"
else
echo "Failed to create security group"
exit 1
fi
14. The sed Editor (Stream Line Editor)
sed is used for searching, replacing, and editing text directly from the command line — without opening a file in an editor.
| Command | Action |
|---|---|
i |
insert text before a line |
a |
insert text after a line |
s |
substitute / find & replace |
d |
delete a line |
c |
change/replace a line |
/pattern/d |
delete lines matching a pattern |
Examples:
sed '3i\New line before line 3' file.txt
sed '3a\New line after line 3' file.txt
sed 's/old/new/g' file.txt # replace all occurrences
sed '5d' file.txt # delete line 5
sed '/error/d' file.txt # delete all lines containing "error"
sed '3c\Replaced line 3' file.txt
15. Putting It All Together: Roboshop Multi-Service Installer
#!/bin/bash
# usage: sh roboshop.sh mongodb redis mysql rabbitmq
USERID=$(id -u)
R="\e[31m"
G="\e[32m"
Y="\e[33m"
N="\e[0m"
LOG_FOLDER="/var/log/roboshop-logs"
SCRIPT_NAME=\((basename "\)0" | cut -d "." -f1)
LOGFILE="\(LOG_FOLDER/\)SCRIPT_NAME.log"
mkdir -p "$LOG_FOLDER"
VALIDATE(){
if [ $1 -eq 0 ]; then
echo -e "\(2 ... \){G}SUCCESS${N}"
else
echo -e "\(2 ... \){R}FAILED${N}"
exit 1
fi
}
if [ $USERID -ne 0 ]; then
echo -e "\({R}ERROR:: Please run this script with root access\){N}"
exit 1
fi
for SERVICE in "$@"
do
echo "Installing \(SERVICE" | tee -a "\)LOGFILE"
dnf list installed "\(SERVICE" &>>"\)LOGFILE"
if [ $? -eq 0 ]; then
echo -e "\(SERVICE is already installed ... \){Y}SKIPPING\({N}" | tee -a "\)LOGFILE"
continue
fi
dnf install "\(SERVICE" -y &>>"\)LOGFILE"
VALIDATE \(? "Installing \)SERVICE"
done
What this script demonstrates:
Root access validation
Logging with timestamps and color-coded output
Idempotency checks (skip if already installed)
Reusable
VALIDATEfunction for exit status checkingLooping over multiple services passed as arguments
Centralized logging to a log file using
tee -a
Key Takeaways
Always validate permissions before performing privileged operations.
Always check exit status (
$?) after critical commands — never assume success.Build idempotent scripts — re-running them shouldn't break or duplicate work.
Use functions to reduce repetition and improve maintainability.
Use logging and color codes for better visibility into script execution.
Handle both expected and unexpected errors gracefully.
Master
sedfor quick, scriptable text manipulation — a huge time-saver in automation pipelines.
If you found this useful, follow for more DevOps automation content covering AWS, Docker, Kubernetes, and CI/CD pipelines.



