Shell Scripting Basics

Introduction

Repeating the same commands for backup, deploy, and health checks wastes time and invites typos. Bash scripts automate those steps. This chapter covers your first executable script, variables, conditionals, loops, functions, and safe defaults—enough to glue together curl, tar, and systemctl from earlier chapters.

Prerequisites

Your First Script

Create hello.sh:

bash
#!/usr/bin/env bash
# Greet the user running the script
echo "Hello, $(whoami). Today is $(date +%Y-%m-%d)."

Make executable and run:

bash
chmod +x hello.sh
./hello.sh

Shebang #!/usr/bin/env bash finds bash on PATH—portable across distros.

Run without execute bit:

bash
bash hello.sh

Positional Parameters

args.sh:

bash
#!/usr/bin/env bash
# Script name is $0; first argument is $1
echo "Script: $0"
echo "Argument count: $#"
echo "All args: $@"
echo "First arg: $1"
bash
chmod +x args.sh
./args.sh foo bar
VariableMeaning
$0Script path
$1, $2Arguments
$#Count
$@All args as list
$?Exit code of last command

Variables and Command Substitution

Conditionals

File and string tests with [[ ]] (bash):

Common tests:

TestTrue when
-f pathRegular file exists
-d pathDirectory exists
-z "$s"String empty
-n "$s"String non-empty

Numeric compare inside (( )):

bash
COUNT=3
if (( COUNT > 0 )); then
  echo "positive"
fi

Chain commands:

bash
# Run step2 only if step1 succeeds
./build.sh && ./deploy.sh
 
# Run cleanup even if deploy fails
./deploy.sh || echo "Deploy failed"

Loops

bash
#!/usr/bin/env bash
# Loop over arguments
for host in prod staging; do
  echo "Checking $host"
  ssh "$host" 'hostname'
done
 
# C-style loop
for i in {1..3}; do
  echo "Attempt $i"
done

While read lines from a file:

bash
#!/usr/bin/env bash
while IFS= read -r line; do
  echo "Line: $line"
done < servers.txt

Functions

local keeps variables inside the function.

Safer Scripts: set -euo pipefail

Add near the top of production scripts:

bash
#!/usr/bin/env bash
set -euo pipefail
OptionEffect
-eExit on first failing command
-uError on unset variables
-o pipefailPipeline fails if any stage fails

Use set +e temporarily when you expect a command to fail and handle $? yourself.

Practical Example: Backup Logs

backup-logs.sh:

bash
#!/usr/bin/env bash
set -euo pipefail
 
SRC=/var/log/myapp
DEST=/backup
STAMP=$(date +%Y%m%d-%H%M%S)
ARCHIVE="$DEST/myapp-logs-$STAMP.tar.gz"
 
mkdir -p "$DEST"
tar -czf "$ARCHIVE" -C "$(dirname "$SRC")" "$(basename "$SRC")"
echo "Created $ARCHIVE"

Schedule with cron.

Practical Example: HTTP Health Check

health.sh:

bash
#!/usr/bin/env bash
set -euo pipefail
 
URL=${1:-http://127.0.0.1:8080/health}
 
if curl -sf "$URL" | grep -q '"status":"UP"'; then
  echo "Health OK: $URL"
  exit 0
fi
 
echo "Health check failed: $URL" >&2
exit 1
bash
chmod +x health.sh
./health.sh http://127.0.0.1:8080/actuator/health

Wire into cron or a deploy script: run after systemctl restart myapp.

Practical Example: Restart App Wrapper

restart-myapp.sh:

bash
#!/usr/bin/env bash
set -euo pipefail
 
sudo systemctl restart myapp
sleep 3
./health.sh

Keeps systemd as source of truth; script only orchestrates.

FAQ

#!/bin/bash vs #!/usr/bin/env bash?

env finds bash wherever it lives—better for mixed distros and macOS/Linux shared scripts.

Script says Permission denied?

chmod +x or run with bash script.sh. Windows line endings (\r) break shebang—use dos2unix script.sh.

Why quote "$var"?

Unquoted $var word-splits on spaces and expands globs—classic bug.

ShellCheck?

Install shellcheck script.sh for lint hints before production.

What comes next?

Developer workflow on Linux.