Levels 21–28: Expert
Contents
Level 21 — Cron & Scheduling
Automate everything. If you do it twice, cron it.
Crontab format:
MIN HOUR DAY MONTH WEEKDAY command
* * * * * /usr/local/bin/script.sh
| Field | Range | Special |
|---|---|---|
| Minute | 0–59 | |
| Hour | 0–23 | |
| Day of month | 1–31 | |
| Month | 1–12 (or Jan–Dec) | |
| Day of week | 0–7 (0 and 7 = Sunday) |
Special values:
| Syntax | Meaning |
|---|---|
* | Every value |
*/15 | Every 15 (step) |
1-5 | Range: 1 through 5 |
1,3,5 | List: 1, 3, and 5 |
Common patterns:
# Every minute
* * * * * /usr/local/bin/healthcheck.sh
# Every day at 2am
0 2 * * * /usr/local/bin/backup.sh
# Every 15 minutes
*/15 * * * * /usr/local/bin/sync.sh
# 9am weekdays (Mon–Fri)
0 9 * * 1-5 /usr/local/bin/report.sh
# First day of every month at midnight
0 0 1 * * /usr/local/bin/monthly.sh
crontab commands:
| Command | Action |
|---|---|
crontab -l | List current crontab |
crontab -e | Edit crontab |
crontab -r | Remove ALL cron jobs (no confirmation) |
crontab -u user -l | List another user’s crontab (root) |
at — one-time scheduled tasks:
echo '/usr/local/bin/restart.sh' | at 23:00
echo '/usr/local/bin/script.sh' | at 2:30 AM tomorrow
atq # List pending at jobs
atrm 3 # Remove at job #3
Level 22 — Logs & Monitoring
When something breaks at 3am, logs are your flashlight.
| Command | What it does |
|---|---|
tail -f /var/log/syslog | Follow log file live |
tail -f app.log \| grep --line-buffered ERROR | Follow with live filter |
journalctl | All systemd journal entries |
journalctl -f | Follow journal live |
journalctl -u nginx | Logs for specific service |
journalctl -fu nginx | Follow specific service live |
journalctl --since '1 hour ago' | Time-filtered logs |
journalctl --since '2024-01-15 08:00' --until '2024-01-15 09:00' | Time window |
journalctl -b | Logs from current boot only |
journalctl -b -1 | Logs from previous boot |
journalctl -n 50 | Last 50 lines |
journalctl -p err | Error priority and above |
logger 'message' | Write to system log from script |
logger -t mytag 'message' | Write with tag |
Always use grep --line-buffered when piping from tail -f. Without it, grep holds output in its buffer and filtered lines may not appear immediately.
Level 23 — Package Management
Same concepts, different commands — know your distro.
Debian / Ubuntu (apt):
sudo apt update # Refresh package index
sudo apt upgrade # Upgrade installed packages
sudo apt full-upgrade # Upgrade + handle dependency changes
sudo apt install -y packagename # Install (-y = no prompts)
sudo apt remove packagename # Remove (keep config files)
sudo apt purge packagename # Remove + delete config files
sudo apt autoremove # Remove orphaned dependencies
apt search keyword # Search available packages
apt show packagename # Show package details
dpkg -l | grep packagename # List installed package
RHEL / Fedora / CentOS (dnf/yum):
sudo dnf update # Update all packages
sudo dnf install -y packagename # Install
sudo dnf remove packagename # Remove
dnf search keyword # Search
dnf info packagename # Show details
rpm -qa | grep packagename # List installed
macOS (Homebrew):
brew update # Update Homebrew itself
brew upgrade # Upgrade all packages
brew install packagename # Install
brew uninstall packagename # Remove
brew search keyword # Search
brew info packagename # Show details
brew list # List installed
Level 24 — Compression Deep Dive
Different formats for different needs. Know the trade-offs.
| Format | Speed | Ratio | Best for |
|---|---|---|---|
gzip (.gz) | Fast | Good | Log rotation, web serving, general use |
bzip2 (.bz2) | Moderate | Better | Source code archives |
xz (.xz) | Slow | Best | Long-term storage, kernel releases |
zip (.zip) | Fast | Moderate | Windows compatibility |
| Command | What it does |
|---|---|
gzip file | Compress (replaces original) |
gzip -k file | Compress, keep original |
gunzip file.gz | Decompress |
zcat file.gz | Read compressed file (stdout) |
zgrep pattern file.gz | grep inside compressed file |
bzip2 file | Compress with bzip2 |
bunzip2 file.bz2 | Decompress bzip2 |
bzcat file.bz2 | Read bzip2 compressed |
xz file | Compress with xz |
xz -9 file | Maximum compression (slowest) |
unxz file.xz | Decompress xz |
xzcat file.xz | Read xz compressed |
zip -r out.zip dir/ | Create zip archive |
unzip out.zip | Extract zip |
unzip -l out.zip | List zip contents |
tar + compression combined:
tar -czf archive.tar.gz dir/ # gzip (.tar.gz / .tgz)
tar -cjf archive.tar.bz2 dir/ # bzip2 (.tar.bz2)
tar -cJf archive.tar.xz dir/ # xz (.tar.xz)
tar -xzf archive.tar.gz # extract gzip
tar -xJf archive.tar.xz # extract xz
tar -tzf archive.tar.gz # list contents
Level 25 — Bash String Processing
Built-in shell operations — no subprocess, no external commands.
| Syntax | Result | Example |
|---|---|---|
${#var} | String length | ${#NAME} → 9 |
${var:0:4} | Substring from offset 0, length 4 | ${NAME:0:4} → Bash |
${var: -3} | Last 3 characters (note the space) | ${NAME: -4} → uest |
${var%pattern} | Strip shortest suffix match | ${FILE%.gz} → file.tar |
${var%%pattern} | Strip longest suffix match | ${FILE%%.tar*} → file |
${var#pattern} | Strip shortest prefix match | ${PATH#*/} |
${var##pattern} | Strip longest prefix match | ${PATH##*/} → filename |
${var/find/replace} | Replace first occurrence | ${TEXT/cat/dog} |
${var//find/replace} | Replace all occurrences | ${TEXT//cat/dog} |
${var^^} | Uppercase all (bash 4+) | ${name^^} → NAME |
${var,,} | Lowercase all (bash 4+) | ${NAME,,} → name |
${var:-default} | Use default if var is unset/empty | ${PORT:-8080} |
${var:=default} | Assign default if unset/empty | ${DIR:=/tmp} |
macOS ships Bash 3.2 which does not support ${var^^} or ${var,,}. Use echo "$var" | tr 'a-z' 'A-Z' instead.
printf formatting:
printf '%-20s %5d\n' "Alice" 95 # Left-aligned name, right-aligned number
printf '%.2f\n' 3.14159 # 2 decimal places: 3.14
printf '%05d\n' 42 # Zero-padded: 00042
printf '%s\n' "${array[@]}" # One element per line
Level 26 — Arrays in Bash
Store multiple values. Iterate safely. No string-splitting bugs.
Indexed arrays:
# Create
SERVERS=(web01 web02 db01)
# Access
echo ${SERVERS[0]} # web01 (zero-indexed)
echo ${SERVERS[-1]} # db01 (last element, bash 4.2+)
echo ${SERVERS[@]} # all elements
echo ${#SERVERS[@]} # element count: 3
# Append
SERVERS+=(db02)
# Loop — always quote "${arr[@]}"
for s in "${SERVERS[@]}"; do
echo "Checking $s..."
done
# Slice
echo ${SERVERS[@]:1:2} # elements 1 and 2: web02 db01
Associative arrays (bash 4+):
declare -A PORTS=([http]=80 [https]=443 [ssh]=22)
echo ${PORTS[http]} # 80
echo ${!PORTS[@]} # all keys: http https ssh
echo ${PORTS[@]} # all values: 80 443 22
for key in "${!PORTS[@]}"; do
echo "$key → ${PORTS[$key]}"
done
macOS Bash 3.2 does not support declare -A (associative arrays) or negative indices. Install Bash 5 via Homebrew for full support: brew install bash.
Level 27 — Functions & Error Handling
Production scripts don’t silently fail.
The safety header — put this at the top of every production script:
#!/bin/bash
set -euo pipefail
| Option | Effect |
|---|---|
set -e | Exit immediately on any non-zero exit code |
set -u | Treat unset variables as an error |
set -o pipefail | Fail if any command in a pipe fails |
Functions:
# Define
greet() {
local name="$1" # local variables don't leak
echo "Hello, $name!"
}
# Call
greet Tony
# Return values (via exit code)
is_root() {
[ "$(id -u)" -eq 0 ] && return 0 || return 1
}
if is_root; then
echo "Running as root"
fi
Exit codes:
# $? = exit code of last command
ls /nonexistent 2>/dev/null
echo $? # 2 (non-zero = failure)
# Always check immediately — $? changes after every command
cp src.txt dst.txt
if [ $? -ne 0 ]; then
echo "Copy failed" >&2
exit 1
fi
trap — guaranteed cleanup:
TMPDIR=$(mktemp -d)
cleanup() {
rm -rf "$TMPDIR"
echo "Cleaned up."
}
trap cleanup EXIT # Runs on any exit
trap cleanup INT TERM # Also runs on Ctrl+C / kill
# Do work in $TMPDIR...
trap ... EXIT fires on: normal completion, set -e error, SIGTERM, Ctrl+C. The cleanup always runs.
Level 28 — Systemd & Services
systemd manages everything that runs in the background on modern Linux.
| Command | What it does |
|---|---|
systemctl status nginx | Show service status, PID, memory, recent logs |
sudo systemctl start nginx | Start service now |
sudo systemctl stop nginx | Stop service now |
sudo systemctl restart nginx | Stop then start (brief downtime) |
sudo systemctl reload nginx | Re-read config without restart (if supported) |
sudo systemctl enable nginx | Enable autostart on boot |
sudo systemctl disable nginx | Disable autostart on boot |
sudo systemctl enable --now nginx | Enable AND start in one command |
sudo systemctl disable --now nginx | Disable AND stop in one command |
systemctl list-units --type=service | List all service units |
systemctl --state=failed | Show only failed units |
journalctl -u nginx | Logs for this service |
journalctl -fu nginx | Follow logs live |
sudo systemctl daemon-reload | Reload unit files after editing |
Minimal unit file (/etc/systemd/system/myapp.service):
[Unit]
Description=My Application
After=network.target
[Service]
Type=simple
User=myapp
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.yml
Restart=on-failure
RestartSec=5
[Install]
WantedBy=multi-user.target
After creating or editing a unit file:
sudo systemctl daemon-reload
sudo systemctl enable --now myapp
journalctl -fu myapp # Watch it start
Service states:
| State | Meaning |
|---|---|
active (running) | Running normally |
active (exited) | Ran once and exited cleanly (oneshot) |
inactive (dead) | Not running |
failed | Exited with error or crashed |
activating | In the process of starting |