Happy Dussehra! May good always win. Happy Diwali! Wishing you a bright year ahead. Merry Christmas! Warm wishes from Vizag. Happy New Year! Here’s to a great year ahead.

Archive ยท Linux

Shell Scripting Basics

Variables, conditions, loops, functions and exit codes, with the strict-mode header, quoting rules and logging pattern that make scripts safe to run on servers.

3 min read

  • Ubuntu 18.04.3 LTS
  • bash 4.4
  • ShellCheck 0.4
  • CentOS 7.7

A shell script is a saved sequence of commands, and the glue of server automation: backups, cleanups, deployments, health checks. Most script bugs come from unquoted variables and ignored errors, so this post starts with a safe header and builds up.

Versions in use on this date. bash 4.4 on Ubuntu 18.04.3 LTS (bash 4.2 on CentOS 7.7). ShellCheck, a linter that catches common mistakes, is in the Ubuntu 18.04 repositories as version 0.4.6.

The shape of a good script

flowchart TB
  H[Shebang and strict mode] --> A[Parse arguments and check prerequisites]
  A --> L[Take a lock so two copies cannot run]
  L --> M[Main work in functions]
  M --> E{Any command fails?}
  E -->|yes| T[trap runs cleanup and logs error]
  E -->|no| OK[Log success and exit 0]
  T --> NZ[Exit non-zero so cron or CI notices]

The header every script needs

#!/usr/bin/env bash
set -euo pipefail
IFS=$'\n\t'
  • -e stops on the first failing command.
  • -u treats an unset variable as an error (it prevents rm -rf "$DIR/" with an empty DIR).
  • pipefail makes a pipeline fail if any stage fails, not just the last.

Make it executable: chmod +x backup.sh, then run ./backup.sh.

Variables, quoting and arguments

name="web01"
today=$(date +%F)                     # command substitution
echo "Backing up ${name} on ${today}"
dest="/backup/${name}/${today}"
echo "first arg: $1, all: $*, count: $#"
: "${BACKUP_DIR:?BACKUP_DIR must be set}"      # fail if unset

Always quote expansions ("$file"), otherwise a filename with a space becomes two arguments. Use ${var:-default} for defaults.

Conditions

if [[ -f "$file" ]]; then echo "exists"; fi
if [[ ! -d "$dir" ]]; then mkdir -p "$dir"; fi
if [[ "$USER" != "root" ]]; then echo "run as root" >&2; exit 1; fi
if [[ $count -gt 10 && "$env" == "prod" ]]; then ...; fi
if grep -q "ERROR" app.log; then echo "errors found"; fi    # use the command's exit code

Prefer [[ ... ]] in bash over [ ... ]. Common tests: -f file, -d directory, -s non-empty, -z empty string, -n non-empty string.

Loops and functions

for host in web01 web02 db01; do
  echo "Checking $host"
  ssh "$host" uptime || echo "$host unreachable" >&2
done

while IFS= read -r line; do
  echo "line: $line"
done < hosts.txt

log() { echo "$(date '+%F %T') $*" | tee -a /var/log/myscript.log; }
log "starting"

Reading files with while read -r handles spaces correctly, which for x in $(cat file) does not.

Exit codes, traps and cleanup

Every command returns an exit code: 0 means success; anything else is failure. $? holds the last one.

tmp=$(mktemp -d)
cleanup() { rm -rf "$tmp"; }
trap cleanup EXIT
trap 'echo "failed at line $LINENO" >&2' ERR

trap ... EXIT runs on success and on failure, so temporary files are always removed.

A complete, safe example

#!/usr/bin/env bash
set -euo pipefail

src="/var/www"; dest="/backup/www"; keep=7
lock=/var/lock/www-backup.lock
exec 9>"$lock"; flock -n 9 || { echo "already running" >&2; exit 1; }

stamp=$(date +%F)
mkdir -p "$dest"
tar -czf "$dest/www-$stamp.tar.gz" -C "$src" .
tar -tzf "$dest/www-$stamp.tar.gz" > /dev/null          # verify the archive reads
find "$dest" -name 'www-*.tar.gz' -mtime +"$keep" -delete
echo "backup ok: www-$stamp.tar.gz"

Quality habits

  • Run shellcheck script.sh (install with apt install shellcheck) before every commit.
  • Test with bash -n script.sh (syntax) and bash -x script.sh (trace).
  • Never put passwords in scripts or arguments; they appear in ps. Read them from a protected file or the environment.
  • Send errors to stderr (>&2) and keep stdout for results.
  • Use command -v tool >/dev/null || { echo "tool missing"; exit 1; } to check dependencies.
  • Keep scripts in Git, and have them log what they do so cron failures can be investigated.