# Without AI — Part 2: Bash Scripting **Track:** Advanced (No AI) **Time for this section:** 1–2 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 ''" >&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 < /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 ''" >&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 }" # --- 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)