Linux10 min read

Bash scripting for beginners

linuxbashscriptingautomation

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