Bash scripting for beginners
If you manage Linux servers, you’ll write bash scripts. Not because bash is a good programming language — it isn’t — but because it’s always there and it can call every other tool on the system. A 20-line bash script that automates a daily task saves hours over a year.
The basics
Every bash script starts with a shebang:
#!/bin/bash
Make it executable:
chmod +x myscript.sh
./myscript.sh
Or run it explicitly:
bash myscript.sh
Variables
No spaces around the equals sign:
name="production"
count=42
filepath="/var/log/syslog"
Use the variable:
echo "Deploying to $name"
echo "Count: ${count}total"
Curly braces help when the variable name runs into surrounding text.
Command substitution:
current_date=$(date +%Y-%m-%d)
hostname=$(hostname)
file_count=$(ls -1 | wc -l)
Always use $() instead of backticks. It’s clearer and can be nested.
Conditionals
if [ "$1" = "start" ]; then
echo "Starting..."
elif [ "$1" = "stop" ]; then
echo "Stopping..."
else
echo "Usage: $0 {start|stop}"
exit 1
fi
Important: spaces around brackets are mandatory. [ "$1" = "start" ] works. ["$1"="start"] doesn’t.
File tests:
if [ -f "/etc/nginx/nginx.conf" ]; then
echo "Config exists"
fi
if [ -d "/opt/app" ]; then
echo "Directory exists"
fi
if [ -r "/etc/shadow" ]; then
echo "File is readable"
fi
Numeric comparisons:
if [ "$count" -gt 10 ]; then
echo "More than 10"
fi
Use -eq, -ne, -gt, -lt, -ge, -le for numbers. Use = and != for strings.
Loops
For loops:
for file in /var/log/*.log; do
echo "Processing $file"
wc -l "$file"
done
for i in {1..10}; do
echo "Iteration $i"
done
While loops:
while read -r line; do
echo "Line: $line"
done < /etc/hosts
Reading a file line by line with while read is a common pattern. The -r flag prevents backslash interpretation.
Functions
log_message() {
local level="$1"
local message="$2"
echo "$(date '+%Y-%m-%d %H:%M:%S') [$level] $message"
}
log_message "INFO" "Script started"
log_message "ERROR" "Something went wrong"
local keeps variables scoped to the function. Without it, they’re global.
Arguments and exit codes
Script arguments are $1, $2, $3, etc. $0 is the script name. $# is the argument count.
if [ $# -lt 2 ]; then
echo "Usage: $0 <source> <destination>"
exit 1
fi
source="$1"
destination="$2"
Exit codes: 0 means success, anything else means failure. Set them explicitly:
if ! command -v nginx > /dev/null 2>&1; then
echo "nginx is not installed"
exit 1
fi
Error handling
Exit on errors:
set -euo pipefail
-e— exit if any command fails-u— treat unset variables as errors-o pipefail— catch errors in pipes
This should be at the top of every script. Without it, failures get silently ignored and your script continues with bad data.
Trap for cleanup:
cleanup() {
rm -f "$temp_file"
}
trap cleanup EXIT
temp_file=$(mktemp)
The EXIT trap runs when the script exits, whether normally or due to an error.
Practical patterns
Check if running as root:
if [ "$(id -u)" -ne 0 ]; then
echo "This script must be run as root" >&2
exit 1
fi
Wait for a service to be ready:
wait_for_port() {
local port=$1
local timeout=30
local count=0
while ! nc -z localhost "$port" 2>/dev/null; do
sleep 1
count=$((count + 1))
if [ $count -ge $timeout ]; then
echo "Timed out waiting for port $port"
exit 1
fi
done
}
Logging with timestamps:
log() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" | tee -a /var/log/myscript.log
}
Common mistakes
Not quoting variables. rm $file breaks if the filename has spaces. rm "$file" is safe. Always quote variable expansions.
Forgetting set -e. Without it, your script happily continues after a command fails, potentially doing more damage.
Using bash arrays. They exist but the syntax is painful. If you need complex data structures, use Python instead.
Remarks
Bash scripting is glue code. It connects tools, automates sequences, and handles the simple cases. For anything complex — parsing JSON, HTTP requests, complex logic — use Python or another real language. But for “run these five commands in order, check if they worked, and send me an email,” bash is the fastest way to get it done.
Related Posts
Vim survival guide for Linux admins
The minimum vim knowledge you need to survive on a Linux server. Editing config files without wanting to throw your keyboard.
Linux memory management: free, vmstat, and /proc
Understanding how Linux uses memory. Reading free correctly, using vmstat, and what /proc/meminfo actually tells you.
tmux: terminal multiplexing made simple
Using tmux to manage terminal sessions. Panes, windows, and why your SSH sessions should survive disconnects.