5.1.1. Script Structure, Variables, and Control Flow
💡 First Principle: Every bash script should start with a shebang (#!/bin/bash) and set -euo pipefail. The shebang tells the OS which interpreter to use. set -e exits on any error, -u treats unset variables as errors, -o pipefail catches errors in pipes. These three settings catch most silent failure modes that make scripts unreliable.
Script Basics:
#!/bin/bash
# Description: Example script structure
set -euo pipefail # Exit on error, unset vars, pipe failures
# Variables — no spaces around =
NAME="Alice"
COUNT=5
CURRENT_DATE=$(date +%Y-%m-%d) # Command substitution
DISK_USAGE=$(df -h / | tail -1 | awk '{print $5}')
# Read-only variables
readonly CONFIG_DIR="/etc/myapp"
# Arrays
FILES=("file1.txt" "file2.txt" "file3.txt")
echo "${FILES[0]}" # First element
echo "${FILES[@]}" # All elements
echo "${#FILES[@]}" # Array length
# Special variables
echo "$0" # Script name
echo "$1" # First argument
echo "$@" # All arguments (individually quoted)
echo "$*" # All arguments (as single string)
echo "$#" # Number of arguments
echo "$?" # Exit code of last command
echo "$" # Current process PID
echo "$!" # PID of last background process
Conditionals:
# if/elif/else
if [ "$USER" = "root" ]; then
echo "Running as root"
elif [ "$USER" = "alice" ]; then
echo "Running as alice"
else
echo "Running as $USER"
fi
# Test operators — [ ] vs [[ ]]
# [ ] is POSIX sh; [[ ]] is bash-specific but more powerful
# Always use [[ ]] in bash scripts
# String tests
[[ -z "$VAR" ]] # True if empty
[[ -n "$VAR" ]] # True if non-empty
[[ "$A" = "$B" ]] # String equality
[[ "$A" != "$B" ]] # String inequality
[[ "$A" =~ ^[0-9]+$ ]] # Regex match (bash only)
# Numeric tests
[[ $A -eq $B ]] # Equal
[[ $A -ne $B ]] # Not equal
[[ $A -lt $B ]] # Less than
[[ $A -gt $B ]] # Greater than
[[ $A -le $B ]] # Less than or equal
[[ $A -ge $B ]] # Greater than or equal
# File tests
[[ -f "$FILE" ]] # Regular file exists
[[ -d "$DIR" ]] # Directory exists
[[ -e "$PATH" ]] # Anything exists
[[ -r "$FILE" ]] # Readable
[[ -w "$FILE" ]] # Writable
[[ -x "$FILE" ]] # Executable
[[ -s "$FILE" ]] # Exists and non-empty
# Compound conditions
[[ -f "$FILE" && -r "$FILE" ]] # AND
[[ -z "$A" || -z "$B" ]] # OR
# case statement
case "$DISTRO" in
ubuntu|debian)
apt update ;;
rhel|fedora|rocky)
dnf update ;;
*)
echo "Unknown distro: $DISTRO"
exit 1 ;;
esac
Loops:
# for loop — iterate over list
for FILE in /var/log/*.log; do
echo "Processing: $FILE"
gzip "$FILE"
done
# for loop — C-style
for ((i=1; i<=10; i++)); do
echo "Iteration $i"
done
# for loop — over command output
for USER in $(awk -F: '$3 >= 1000 {print $1}' /etc/passwd); do
echo "User: $USER"
done
# while loop
COUNT=0
while [[ $COUNT -lt 5 ]]; do
echo "Count: $COUNT"
((COUNT++))
done
# while read — process file line by line (best practice)
while IFS= read -r LINE; do
echo "Line: $LINE"
done < /etc/hosts
# until loop — opposite of while
until systemctl is-active nginx &>/dev/null; do
echo "Waiting for nginx..."
sleep 2
done
echo "Nginx is up"
Functions:
# Define function
check_disk() {
local THRESHOLD="${1:-80}" # Local variable; default 80 if not provided
local USAGE
USAGE=$(df / | tail -1 | awk '{print $5}' | tr -d '%')
if [[ $USAGE -gt $THRESHOLD ]]; then
echo "WARNING: Disk usage at ${USAGE}% (threshold: ${THRESHOLD}%)"
return 1 # Non-zero = failure
fi
echo "Disk OK: ${USAGE}%"
return 0
}
# Call function
check_disk 90
check_disk # Uses default (80)
# Capture return value
if ! check_disk 75; then
send_alert "Disk critical"
fi
⚠️ Exam Trap: $@ and $* behave differently when quoted. "$@" expands each argument as a separate word (preserving spaces within arguments). "$*" joins all arguments into one word with the first IFS character as separator. Always use "$@" when passing arguments to another command to preserve word boundaries.
Python for Linux Administration
💡 First Principle: Bash is ideal for gluing commands together, but once a script needs structured data, robust error handling, or reusable logic, Python is the better tool. XK0-006 expects you to read and reason about basic Python automation, not write large programs.
Core data types:
| Type | Purpose | Example |
|---|---|---|
str / int / float / bool | Scalars | name = "web01", port = 443 |
list | Ordered, mutable sequence | servers = ["web01", "web02"] |
dict | Key–value mapping — ideal for named config settings | cfg = {"port": 443, "tls": True} |
tuple | Ordered, immutable sequence | point = (10, 20) |
set | Unordered unique values | seen = {"a", "b"} |
A dict is the natural structure for storing configuration settings — each setting is a key mapped to its value and looked up by name (cfg["port"]), which is why it beats a list when data is accessed by label rather than position.
Running shell commands: subprocess.run() executes an external command and returns a completed-process object exposing .returncode, .stdout, and .stderr:
import subprocess
result = subprocess.run(["systemctl", "is-active", "nginx"],
capture_output=True, text=True)
if result.returncode == 0:
print("nginx active")
Reading files safely: always use the with open(...) context manager — it closes the file automatically even if an error occurs:
with open("/etc/hostname") as f:
host = f.read().strip()
Standard modules to recognize: os (environment variables and paths — os.getenv, os.path), sys (arguments, exit codes), subprocess (run commands), json / yaml (parse config — use yaml.safe_load(), never bare yaml.load(), which can execute embedded objects), requests (HTTP/REST calls — requests.get(url) returns a response with .status_code and .json()), and concurrent.futures.ThreadPoolExecutor (run one task against many servers in parallel).
Virtual environments (venv): python3 -m venv .venv creates an isolated per-project environment so pip install packages don't pollute the system Python or conflict between projects; activate with source .venv/bin/activate.
PEP 8 is Python's style guide. Rules the exam checks: snake_case for functions and variables (check_service, not CheckService), 4-space indentation, and UPPER_CASE for constants.
⚠️ Exam Trap: Use yaml.safe_load() for any untrusted input, and reach for a dict (not a list) when settings are looked up by name.
Reflection Question: Your script has set -euo pipefail. A command in the middle of the script exits with code 1. Describe what happens and why this behavior is desirable for production scripts.