Community Tutorials Linux Basics Bash Scripting Fundamentals on Linux
Bash Scripting Fundamentals on Linux
LINUX BASICS

Bash Scripting Fundamentals on Linux

SKYLINE Knowledge Base
Photo by Sigmund on Unsplash

A field-tested, step-by-step guide. Bash Scripting Fundamentals on Linux — prerequisites, the actual commands, verification, and links to related Linux Basics topics.

Every Linux box ships bash (or close enough — Alpine ships ash, macOS now ships zsh by default). Knowing how to write a 50-line script the right way separates "operator" from "sysadmin."

Prerequisites

  • A Linux/macOS shell.
  • A text editor.

Step 1: The safe shebang and strict mode

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

What this does:

  • set -e — exit on the first command that fails.
  • set -u — error on use of undefined variable.
  • set -o pipefail — pipelines fail if any stage fails, not just the last.
  • IFS=$'\n\t' — split words on newline + tab only, not spaces (prevents "name with spaces" bugs).

Drop these four lines at the top of every script and you avoid 80 percent of bash footguns.

Step 2: Variables and quoting

name="World"
echo "Hello, ${name}"

# Always quote unless you specifically want word-splitting
file="my report.txt"
rm "$file"          # correct
rm $file            # WRONG — tries to delete "my" and "report.txt"

# Default value if unset
: "${PORT:=8080}"
echo "Port: $PORT"

Step 3: Conditionals

if [[ "$1" == "deploy" ]]; then
    echo "Deploying..."
elif [[ -z "$1" ]]; then
    echo "No arg given" >&2
    exit 2
else
    echo "Unknown action: $1" >&2
    exit 2
fi

# File tests
if [[ -f "/etc/passwd" ]]; then echo "exists"; fi
if [[ -d "/var/log" ]]; then echo "dir"; fi
if [[ -x "$(command -v jq)" ]]; then echo "jq installed"; fi
if [[ "$EUID" -ne 0 ]]; then echo "not root" >&2; exit 1; fi

Use [[ ]] not [ ] — modern bash, no surprising word-splitting, supports =~ regex.

Step 4: Loops

# Iterate files
for f in /var/log/*.log; do
    echo "Processing $f"
done

# Numbered range
for i in {1..5}; do
    echo "iteration $i"
done

# Read a file line by line
while IFS= read -r line; do
    echo "got: $line"
done < hosts.txt

# Loop over command output safely
while IFS= read -r host; do
    ping -c1 -W2 "$host" >/dev/null && echo "$host UP"
done < <(awk '{print $1}' /etc/hosts)

The < <(...) form runs in the current shell so any variables set inside the loop persist.

Step 5: Functions and arguments

log() {
    local level="$1"; shift
    echo "$(date +'%F %T') [$level] $*"
}

backup() {
    local src="$1"
    local dest="$2"
    log INFO "Backing up $src to $dest"
    rsync -a --delete "$src" "$dest"
}

# Main
[[ $# -lt 2 ]] && { log ERROR "Usage: $0 SRC DEST"; exit 2; }
backup "$1" "$2"
log INFO "Done"

Always local-scope variables inside functions — without it they leak into the global scope.

Step 6: Trap on exit for cleanup

tmpdir=$(mktemp -d)
trap 'rm -rf "$tmpdir"' EXIT

# Use $tmpdir freely; it's cleaned up on any exit, even errors.
curl -sfL https://example.sa/data.tar.gz -o "$tmpdir/data.tar.gz"
tar xzf "$tmpdir/data.tar.gz" -C "$tmpdir"

trap '...' EXIT runs your cleanup whether the script exits cleanly, errors out, or is killed.

Step 7: Common script template

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_DIR
readonly LOG_FILE="/var/log/$(basename "$0" .sh).log"

log()  { echo "$(date +'%F %T') [$1] ${*:2}" | tee -a "$LOG_FILE" >&2; }
die()  { log ERROR "$*"; exit 1; }
need() { command -v "$1" >/dev/null 2>&1 || die "Missing required command: $1"; }

usage() {
    cat <<EOF
Usage: $0 [-h] -i INPUT -o OUTPUT
  -h          show this help
  -i INPUT    input file
  -o OUTPUT   output directory
EOF
    exit 0
}

input=""; output=""
while getopts "hi:o:" opt; do
    case "$opt" in
        h) usage ;;
        i) input="$OPTARG" ;;
        o) output="$OPTARG" ;;
        *) die "Bad flag" ;;
    esac
done

[[ -z "$input"  ]] && die "Missing -i"
[[ -z "$output" ]] && die "Missing -o"
need rsync

log INFO "Copying $input -> $output"
rsync -a "$input" "$output"
log INFO "Done"

Step 8: Lint your scripts

sudo apt install -y shellcheck
shellcheck deploy.sh

shellcheck catches quoting bugs, missing $, mistaken [ ] vs [[ ]], all of it. CI it.

Verify

bash --version | head -1
shellcheck --version

Conclusion

set -euo pipefail, always-quote variables, [[ ]] instead of [ ], trap '...' EXIT for cleanup, and shellcheck in CI — that is bash done right.

Next steps

SKYLINE Engineering

@skyline

The engineering team at SKYLINE Industrial Solutions. We publish field-tested guides drawn from real KSA and GCC deployments.

See author profile
SKYLINE engineering services

Need this implemented for you?

Reading is free — building it right takes a team. SKYLINE engineers ship Linux Basics for Aramco vendors, banks, hospitals and government agencies across Saudi Arabia. Talk to us before you start.

Aramco Approved Contractor ISO 9001 · ISO 27001 SAMA CSF aligned NCA ECC ready 247+ KSA clients

Comments

0 total · 0 threads
Be the first to leave a comment.