kitestacks-homelab/homelab-mastery/build-guide/without-ai/02-bash-scripting.md
kenpat 1e8319ee75 docs: comprehensive homelab-mastery rewrite with full build guides
Complete documentation suite for KiteStacks covering all 11 services across
2-host active-active architecture. Includes beginner track (with AI, 8 files)
and advanced track (without AI, 7 files) with time estimates, real troubleshooting
cases, and command-by-command explanations. Updates certifications roadmap to
reflect July 7 2026 A+ Core 2 exam goal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 01:08:43 -05:00

333 lines
7.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Without AI — Part 2: Bash Scripting
**Track:** Advanced (No AI)
**Time for this section:** 12 weeks
Bash is the language of the Linux shell. Almost every automation script in this
homelab is a Bash script. You do not need to master it — you need to be able to
read it, write simple scripts, and understand what a script does before you run it.
---
## What Is a Script?
A script is a text file containing a sequence of shell commands. Instead of typing
commands one by one, you put them in a file and run the file.
```bash
#!/usr/bin/env bash
# This is a comment
echo "Hello from my script"
```
The first line (`#!/usr/bin/env bash`) is called the **shebang**. It tells Linux
which interpreter to use to run this file. Without it, Linux may use the wrong shell.
To run a script:
```bash
chmod +x myscript.sh # Make it executable
./myscript.sh # Run it
```
Or without making it executable:
```bash
bash myscript.sh
```
---
## Variables
Variables store values you want to reuse:
```bash
name="kenpat"
port=3000
greeting="Hello, $name"
echo $name # prints: kenpat
echo $port # prints: 3000
echo $greeting # prints: Hello, kenpat
echo "${name}s" # prints: kenpats (braces needed when appending)
```
**Special variables:**
```bash
$0 # The script's own filename
$1 $2 $3 # Command-line arguments (first, second, third)
$# # Number of arguments passed
$? # Exit code of the last command (0 = success, non-zero = error)
$$ # Current process ID (PID)
$HOME # Your home directory path
$USER # Your username
```
**Read-only environment variables:**
```bash
export MY_VAR="value" # Make available to child processes
printenv # List all environment variables
printenv MY_VAR # Print one variable
```
---
## Conditionals (if/else)
```bash
if [[ condition ]]; then
# commands if true
elif [[ other_condition ]]; then
# commands if second condition is true
else
# commands if nothing was true
fi
```
**Common conditions:**
```bash
[[ -f /path/to/file ]] # True if file exists and is a regular file
[[ -d /path/to/dir ]] # True if directory exists
[[ -s /path/to/file ]] # True if file exists and is non-empty
[[ -z "$var" ]] # True if variable is empty
[[ -n "$var" ]] # True if variable is NOT empty
[[ "$a" == "$b" ]] # True if strings are equal
[[ "$a" != "$b" ]] # True if strings are NOT equal
[[ $n -eq 5 ]] # True if number equals 5
[[ $n -gt 5 ]] # True if number is greater than 5
[[ $n -lt 5 ]] # True if number is less than 5
```
**Real example from the homelab:**
```bash
if [[ $# -ne 1 ]]; then
echo "Usage: $0 '<cloudflare_tunnel_token>'" >&2
exit 2
fi
```
This checks that exactly one argument was provided (`$# -ne 1` means "number of args
is not equal to 1"). If not, it prints usage instructions and exits with code 2 (error).
The `>&2` sends the message to stderr (error output) instead of stdout (normal output).
---
## Loops
**For loop — iterate over a list:**
```bash
for item in one two three; do
echo "Item: $item"
done
# Iterate over files
for file in *.yml; do
echo "Found compose file: $file"
done
# Iterate over a range of numbers
for i in {1..10}; do
echo "Number: $i"
done
```
**While loop — repeat while a condition is true:**
```bash
count=0
while [[ $count -lt 5 ]]; do
echo "Count: $count"
count=$(( count + 1 ))
done
# Wait until a container is healthy
while [[ "$(docker inspect --format '{{.State.Health.Status}}' authentik)" != "healthy" ]]; do
echo "Waiting for authentik..."
sleep 5
done
echo "Authentik is healthy"
```
---
## Functions
```bash
greet() {
local name="$1" # local = only exists inside this function
echo "Hello, $name"
}
greet "kenpat" # prints: Hello, kenpat
greet "world" # prints: Hello, world
```
**Why local variables matter:** Without `local`, variables are global and can
accidentally overwrite values from other parts of the script.
---
## Error Handling
```bash
set -euo pipefail
```
Put this near the top of every script you write. It sets three behaviors:
- `-e` — exit immediately if any command fails (returns non-zero exit code)
- `-u` — exit if you use an undefined variable
- `-o pipefail` — if any command in a pipeline fails, the whole pipeline fails
Without this, a script can silently continue after an error, potentially causing
damage downstream (like deleting data after a failed backup).
**Checking a command's result:**
```bash
if curl -s https://example.com > /dev/null; then
echo "Site is up"
else
echo "Site is down"
fi
```
**Exit codes:**
```bash
exit 0 # Success
exit 1 # Generic error
exit 2 # Misuse (bad arguments)
```
---
## String Manipulation
```bash
var="TUNNEL_TOKEN=abc123"
# Split by delimiter, take field 2
echo "$var" | cut -d= -f2 # prints: abc123
# But what if the value itself contains = signs?
echo "$var" | cut -d= -f2- # prints: abc123 (f2- = from field 2 to end)
# Remove trailing newline
echo "hello" | tr -d '\n'
# Convert to lowercase
echo "HELLO" | tr '[:upper:]' '[:lower:]'
# Replace text
echo "hello world" | sed 's/world/there/' # prints: hello there
echo "aabbcc" | sed 's/b/B/g' # prints: aaBBcc (g = all occurrences)
# Extract with grep
echo "addr: 192.168.1.1" | grep -oP '\d+\.\d+\.\d+\.\d+' # prints: 192.168.1.1
```
---
## Here Documents (heredoc)
A heredoc lets you write multi-line strings inline:
```bash
cat <<'EOF'
This is line one
This is line two
Variables like $HOME are NOT expanded (because of the quotes around EOF)
EOF
cat <<EOF
This is line one
HOME is: $HOME (expanded because no quotes)
EOF
```
Used in this homelab to write multi-line content to files:
```bash
cat > /tmp/fix.sql <<'EOF'
BEGIN;
UPDATE ServerSetting SET Value='{"enabled":true}' WHERE "Key"=40;
COMMIT;
EOF
```
---
## Real Scripts in This Homelab
### The Token Rotation Script
`~/kitestacks-homelab/scripts/rollout-cloudflared-token.sh`:
```bash
#!/usr/bin/env bash
set -euo pipefail
if [[ $# -ne 1 ]]; then
echo "Usage: $0 '<cloudflare_tunnel_token>'" >&2
exit 2
fi
token="$1"
monk_dir="${MONK_CLOUDFLARED_DIR:-$HOME/kitestacks-live/docker/cloudflared}"
kscloud1_host="${KSCLOUD1_HOST:?set KSCLOUD1_HOST, for example user@host}"
kscloud1_key="${KSCLOUD1_KEY:-$HOME/.ssh/id_ed25519_kscloud1}"
kscloud1_dir="${KSCLOUD1_CLOUDFLARED_DIR:-/opt/kitestacks/docker/cloudflared}"
```
Walking through each line:
- `set -euo pipefail` — fail fast and safely
- `$# -ne 1` — check exactly one argument was given
- `${MONK_CLOUDFLARED_DIR:-$HOME/...}` — use environment variable if set, otherwise use default
- `${KSCLOUD1_HOST:?...}` — if `KSCLOUD1_HOST` is not set, exit with that error message
This is a real production script. Read it in full at that path.
---
## Writing Your Own Scripts
**Template for any script:**
```bash
#!/usr/bin/env bash
set -euo pipefail
# --- Configuration (change these) ---
MY_VAR="${MY_ENV_VAR:-default_value}"
TARGET_HOST="${1:?Usage: $0 <hostname>}"
# --- Functions ---
log() {
echo "[$(date '+%H:%M:%S')] $*"
}
die() {
echo "ERROR: $*" >&2
exit 1
}
# --- Main ---
log "Starting..."
if [[ ! -d "$TARGET_HOST" ]]; then
die "Directory does not exist: $TARGET_HOST"
fi
log "Done."
```
---
## Practice Exercises
1. Write a script that checks if Docker is running and prints "Docker is up" or "Docker is down"
2. Write a script that takes a service name as an argument and shows its logs:
`./show-logs.sh forgejo`
3. Write a script that loops through all directories in `~/kitestacks-live/docker/`
and prints the service name and whether it has a `.env` file
4. Write a script that checks if a URL returns 200 OK and prints "UP" or "DOWN":
`./check-url.sh https://gitforge.kitestacks.com`
5. Read and understand every line of `scripts/rollout-cloudflared-token.sh`
---
**Next:** [Part 3 — Python Basics](03-python-basics.md)