Module 7.1: Bash Fundamentals
Shell Scripting | Complexity:
[MEDIUM]| Time: 30-35 min
Prerequisites
Section titled “Prerequisites”Before starting this module:
- Required: Module 1.1: Kernel Architecture for understanding commands
- Required: Basic command line experience
- Helpful: Any programming experience
What You’ll Be Able to Do
Section titled “What You’ll Be Able to Do”After this module, you will be able to:
- Write bash scripts with variables, conditionals, loops, and functions
- Handle errors safely using set -euo pipefail and trap
- Process command-line arguments and validate input in scripts
- Debug failing scripts using set -x, shellcheck, and systematic testing
Why This Module Matters
Section titled “Why This Module Matters”Shell scripting is the glue of DevOps. Every operational task—deployments, backups, health checks, log analysis—can be automated with Bash. It’s available on every Linux system, no installation required.
Understanding Bash helps you:
- Automate repetitive tasks — Stop doing things manually
- Chain commands together — Build powerful pipelines
- Write portable scripts — Run anywhere Linux runs
- Work faster in exams — CKA/CKAD allow shell scripting
Bash is the minimum viable automation skill.
Did You Know?
Section titled “Did You Know?”-
Bash is 35+ years old — Released in 1989 as a free replacement for the Bourne shell. It’s still the default on most Linux distros.
-
POSIX compliance matters — Scripts using
#!/bin/share more portable but can’t use Bash-specific features like arrays. Alpine Linux uses ash, not bash. -
Exit codes are everything — Every command returns 0 for success, non-zero for failure. Scripts should check and propagate these codes.
-
Quoting is the #1 source of bugs — Unquoted variables with spaces break scripts. Always quote:
"$var"not$var.
Script Basics
Section titled “Script Basics”First Script
Section titled “First Script”#!/bin/bash# This is a commentecho "Hello, World!"# Create and runcat > hello.sh << 'EOF'#!/bin/bashecho "Hello, World!"EOF
chmod +x hello.sh./hello.sh# Hello, World!Shebang Line
Section titled “Shebang Line”#!/bin/bash # Use Bash#!/bin/sh # Use system shell (more portable)#!/usr/bin/env bash # Find bash in PATH (most portable)
# The shebang must be the first line, no spaces before #!Running Scripts
Section titled “Running Scripts”# Method 1: Make executablechmod +x script.sh./script.sh
# Method 2: Run with interpreterbash script.sh
# Method 3: Source (runs in current shell)source script.sh. script.sh # Same as sourceVariables
Section titled “Variables”Basic Variables
Section titled “Basic Variables”# Assignment (no spaces around =)name="John"count=42
# Using variablesecho "Hello, $name"echo "Count: ${count}"
# ${} is clearer and required for some casesfile="log"echo "${file}s" # logsecho "$files" # Empty (no variable named files)
# Read-onlyreadonly PI=3.14159PI=3 # Error: PI: readonly variableSpecial Variables
Section titled “Special Variables”$0 # Script name$1, $2... # Positional arguments$# # Number of arguments$@ # All arguments (as separate strings)$* # All arguments (as single string)$? # Exit code of last command$$ # Current process ID$! # PID of last background commandArrays
Section titled “Arrays”# Indexed arrayfruits=("apple" "banana" "cherry")echo ${fruits[0]} # appleecho ${fruits[@]} # All elementsecho ${#fruits[@]} # Length: 3
# Add elementfruits+=("date")
# Associative array (Bash 4+)declare -A colorscolors[red]="#FF0000"colors[green]="#00FF00"echo ${colors[red]}Command Substitution
Section titled “Command Substitution”Pause and predict: What happens if you run
files=$(ls -l /nonexistent)? Does the error message get stored in thefilesvariable or printed to the screen?
# Modern syntax (preferred)now=$(date)files=$(ls -1 | wc -l)
# Old syntax (still works)now=`date`
# Use in stringsecho "Current time: $(date +%H:%M)"echo "You have $(ls | wc -l) files"Conditionals
Section titled “Conditionals”If Statements
Section titled “If Statements”# Basic ifif [ condition ]; then echo "true"fi
# If-elseif [ condition ]; then echo "true"else echo "false"fi
# If-elif-elseif [ condition1 ]; then echo "first"elif [ condition2 ]; then echo "second"else echo "else"fiTest Operators
Section titled “Test Operators”# String comparisons[ "$a" = "$b" ] # Equal[ "$a" != "$b" ] # Not equal[ -z "$a" ] # Empty[ -n "$a" ] # Not empty
# Number comparisons[ "$a" -eq "$b" ] # Equal[ "$a" -ne "$b" ] # Not equal[ "$a" -lt "$b" ] # Less than[ "$a" -le "$b" ] # Less than or equal[ "$a" -gt "$b" ] # Greater than[ "$a" -ge "$b" ] # Greater than or equal
# File tests[ -e "$file" ] # Exists[ -f "$file" ] # Is regular file[ -d "$dir" ] # Is directory[ -r "$file" ] # Is readable[ -w "$file" ] # Is writable[ -x "$file" ] # Is executable[ -s "$file" ] # Has size > 0[[ ]] vs [ ]
Section titled “[[ ]] vs [ ]”# [[ ]] is Bash-specific but safer[[ "$name" = "John" ]] # Works even if $name is empty[[ "$file" = *.txt ]] # Pattern matching[[ "$a" =~ ^[0-9]+$ ]] # Regex matching
# [ ] is POSIX, more portable# But requires more quoting[ "$name" = "John" ]
# Recommendation: Use [[ ]] in Bash scriptsLogical Operators
Section titled “Logical Operators”# ANDif [[ condition1 && condition2 ]]; then echo "both true"fi
# ORif [[ condition1 || condition2 ]]; then echo "at least one true"fi
# NOTif [[ ! condition ]]; then echo "not true"fi
# With [ ] syntaxif [ condition1 ] && [ condition2 ]; then echo "both true"fiFor Loops
Section titled “For Loops”# List iterationfor fruit in apple banana cherry; do echo "$fruit"done
# Array iterationfruits=("apple" "banana" "cherry")for fruit in "${fruits[@]}"; do echo "$fruit"done
# Rangefor i in {1..5}; do echo "$i"done
# C-stylefor ((i=0; i<5; i++)); do echo "$i"done
# Filesfor file in *.txt; do echo "Processing $file"done
# Command outputfor pod in $(kubectl get pods -o name); do echo "Pod: $pod"doneWhile Loops
Section titled “While Loops”# Basic whilecount=0while [ $count -lt 5 ]; do echo "$count" ((count++))done
# Read file line by linewhile IFS= read -r line; do echo "Line: $line"done < file.txt
# Read command outputkubectl get pods -o name | while read pod; do echo "Pod: $pod"done
# Infinite loopwhile true; do echo "Running..." sleep 5doneLoop Control
Section titled “Loop Control”# Breakfor i in {1..10}; do if [ $i -eq 5 ]; then break fi echo "$i"done
# Continuefor i in {1..5}; do if [ $i -eq 3 ]; then continue fi echo "$i"doneFunctions
Section titled “Functions”Basic Functions
Section titled “Basic Functions”# Definitiongreet() { echo "Hello, $1!"}
# Callgreet "World"# Hello, World!
# Alternative syntaxfunction greet { echo "Hello, $1!"}Arguments and Return
Section titled “Arguments and Return”# Function argumentsadd() { local a=$1 local b=$2 echo $((a + b))}
result=$(add 5 3)echo "5 + 3 = $result"
# Return codesis_even() { if (( $1 % 2 == 0 )); then return 0 # Success/true else return 1 # Failure/false fi}
if is_even 4; then echo "4 is even"fi
# Return string via echoget_name() { echo "John"}name=$(get_name)Local Variables
Section titled “Local Variables”# Without local - global scopebroken() { x=10 # Modifies global x!}
# With local - function scopecorrect() { local x=10 # Only in this function}
# Always use local for function variablesInput/Output
Section titled “Input/Output”Reading Input
Section titled “Reading Input”# Simple readecho -n "Enter name: "read nameecho "Hello, $name"
# With promptread -p "Enter name: " name
# Silent (for passwords)read -sp "Password: " passwordecho
# Read with defaultread -p "Port [8080]: " portport=${port:-8080}
# Read with timeoutread -t 5 -p "Quick! " answer || echo "Too slow"Output
Section titled “Output”# stdoutecho "Normal output"printf "Formatted: %s %d\n" "text" 42
# stderrecho "Error message" >&2
# Redirect to fileecho "text" > file.txt # Overwriteecho "text" >> file.txt # Append
# Redirect bothcommand > output.txt 2>&1command &> output.txt # Bash shorthandHere Documents
Section titled “Here Documents”# Here-doccat << EOFThis is a multi-linestring with $variables expandedEOF
# Here-doc without expansioncat << 'EOF'This keeps $variables literalEOF
# Here-doc to commandmysql << EOFSELECT * FROM users;EOFExit Codes and Error Handling
Section titled “Exit Codes and Error Handling”Exit Codes
Section titled “Exit Codes”# Exit with codeexit 0 # Successexit 1 # General errorexit 2 # Misuse of command
# Check last exit codecommandif [ $? -eq 0 ]; then echo "Success"fi
# Or more idiomaticallyif command; then echo "Success"else echo "Failed"fiError Handling Options
Section titled “Error Handling Options”Stop and think: If your script uses
set -eand runsgrep "error" log.txt, what happens to your script if the word “error” is not found in the log file?
#!/bin/bashset -e # Exit on errorset -u # Exit on undefined variableset -o pipefail # Exit on pipe failureset -x # Debug: print commands
# Combined (recommended for scripts)set -euo pipefail
# Trap errorstrap 'echo "Error on line $LINENO"' ERR
# Trap exit (cleanup)cleanup() { rm -f /tmp/tempfile}trap cleanup EXITDefensive Programming
Section titled “Defensive Programming”# Check command existscommand -v kubectl &> /dev/null || { echo "kubectl not found" >&2 exit 1}
# Check file existsif [[ ! -f "$config_file" ]]; then echo "Config not found: $config_file" >&2 exit 1fi
# Default valuesname=${1:-"default"}port=${PORT:-8080}Common Mistakes
Section titled “Common Mistakes”| Mistake | Problem | Solution |
|---|---|---|
name = "John" | Spaces around = | name="John" |
if [ $var = "x" ] | Unquoted variable | if [ "$var" = "x" ] |
echo $array | Only first element | echo "${array[@]}" |
Not checking $? | Silent failures | Use set -e or check explicitly |
cd without check | Script continues in wrong dir | `cd dir |
Parsing ls output | Breaks on special filenames | Use globs: for f in * |
Question 1
Section titled “Question 1”You are writing a script and try to set a name variable like this: name = "John". When you run the script, you get an error saying name: command not found. Why does this happen and how do you fix it?
Show Answer
This happens because Bash is highly sensitive to spaces around the equals sign during variable assignment. When it sees name = "John", it interprets name as a command to execute, and = and "John" as arguments passed to that command. Since there is no command called name on your system, it fails. To fix this, you must remove the spaces so Bash recognizes it as an assignment operation.
name="John"Question 2
Section titled “Question 2”Your automated backup script needs to read a configuration file at /etc/backup.conf. Before attempting to read the file, you want to ensure the script fails gracefully if the file is missing or lacks read permissions. How do you write this check?
Show Answer
You should use conditional file test operators to verify the file’s state before interacting with it. The -f flag checks if the path exists and is a regular file, while the -r flag checks if the script’s execution context has permission to read it. Using [[ ]] allows you to combine these checks logically with &&. Alternatively, just checking -r is often sufficient because a file must exist to be readable, but explicitly checking both can make your intent clearer.
if [[ -f "$file" && -r "$file" ]]; then echo "File exists and is readable"fiOr using two tests:
if [[ -r "$file" ]]; then # -r implies file exists echo "File is readable"fiQuestion 3
Section titled “Question 3”Your CI/CD pipeline runs a script that fetches data and parses it: curl -s https://api.example.com/data | jq '.items' > output.json. The API goes down and curl fails, but the script still exits with a success code (0) and the pipeline continues, causing downstream errors. You already have set -e at the top of your script. Why did it still succeed, and how do you fix it?
Show Answer
The script succeeded because set -e only triggers an exit if the last command in a pipeline fails. In your pipeline, jq succeeded (it successfully parsed empty input from the failed curl and wrote an empty file), so the overall pipeline returned 0. To fix this, you need to enable pipefail, which forces the pipeline to return the exit code of the rightmost command that failed. This ensures that a failure anywhere in the pipeline will cause the entire script to halt when combined with set -e.
Without pipefail:
false | true # Returns 0 (success)With pipefail:
false | true # Returns 1 (failure)Question 4
Section titled “Question 4”You have a script that needs to process a list of file paths passed as arguments. Some of the file paths contain spaces, such as report 2023.pdf. You need to loop through each argument exactly as it was provided, without splitting the paths with spaces into multiple arguments. How do you construct this loop?
Show Answer
You must use "$@" to safely iterate through positional arguments while preserving spaces. When wrapped in double quotes, $@ expands each positional parameter into a separate quoted string, matching exactly how they were passed to the script. If you omit the quotes and use $@ or $*, Bash will perform word splitting on the spaces, treating report and 2023.pdf as two completely different files. This quoting practice is essential for preventing silent data corruption in scripts dealing with user input or file systems.
# Using $@for arg in "$@"; do echo "$arg"done
# Important: Quote "$@" to handle spaces./script.sh "hello world" foo# With "$@": two iterations - "hello world", "foo"# With $@: three iterations - "hello", "world", "foo"Question 5
Section titled “Question 5”You are reviewing a legacy script that sets a timestamp variable using backticks: timestamp=`date +%s`. Your team’s style guide requires updating this to modern syntax. What is the modern syntax, and why is it preferred over backticks?
Show Answer
The modern syntax uses $() for command substitution instead of backticks. While backticks still work for backward compatibility, $() is significantly easier to read visually and distinguishes clearly from single quotes. More importantly, $() can be easily nested without requiring complex backslash escaping, whereas nesting backticks requires escaping the inner backticks. This avoids the syntax errors and readability issues that are common when maintaining older scripts.
# Modern syntax (preferred)now=$(date)
# Old syntax (still works)now=`date`Hands-On Exercise
Section titled “Hands-On Exercise”Bash Scripting Practice
Section titled “Bash Scripting Practice”Objective: Write basic Bash scripts using variables, conditionals, loops, and functions.
Environment: Any Linux/Mac system with Bash
Part 1: Variables and Arguments
Section titled “Part 1: Variables and Arguments”# Create a scriptcat > /tmp/greet.sh << 'EOF'#!/bin/bash
# Get name from argument or defaultname=${1:-"World"}
# Get greeting from environment or defaultgreeting=${GREETING:-"Hello"}
echo "$greeting, $name!"echo "Script: $0"echo "Arguments: $#"EOF
chmod +x /tmp/greet.sh
# Test it/tmp/greet.sh/tmp/greet.sh AliceGREETING="Hi" /tmp/greet.sh BobPart 2: Conditionals
Section titled “Part 2: Conditionals”cat > /tmp/check_file.sh << 'EOF'#!/bin/bashset -euo pipefail
file=${1:-""}
if [[ -z "$file" ]]; then echo "Usage: $0 <filename>" >&2 exit 1fi
if [[ ! -e "$file" ]]; then echo "Does not exist: $file"elif [[ -d "$file" ]]; then echo "Directory: $file" echo "Contents: $(ls -1 "$file" | wc -l) items"elif [[ -f "$file" ]]; then echo "File: $file" echo "Size: $(stat -f%z "$file" 2>/dev/null || stat -c%s "$file") bytes" [[ -r "$file" ]] && echo "Readable: yes" || echo "Readable: no" [[ -w "$file" ]] && echo "Writable: yes" || echo "Writable: no"fiEOF
chmod +x /tmp/check_file.sh/tmp/check_file.sh /tmp/tmp/check_file.sh /etc/passwd/tmp/check_file.sh /nonexistentPart 3: Loops
Section titled “Part 3: Loops”cat > /tmp/count_lines.sh << 'EOF'#!/bin/bashset -euo pipefail
dir=${1:-.}
echo "Counting lines in $dir"echo "========================"
total=0for file in "$dir"/*.sh 2>/dev/null; do if [[ -f "$file" ]]; then lines=$(wc -l < "$file") echo "$file: $lines lines" total=$((total + lines)) fidone
if [[ $total -eq 0 ]]; then echo "No .sh files found"else echo "========================" echo "Total: $total lines"fiEOF
chmod +x /tmp/count_lines.sh/tmp/count_lines.sh /tmpPart 4: Functions
Section titled “Part 4: Functions”cat > /tmp/utility.sh << 'EOF'#!/bin/bashset -euo pipefail
# Logging functionlog() { local level=$1 shift echo "[$(date +%H:%M:%S)] [$level] $*"}
# Check if command existsrequire() { local cmd=$1 if ! command -v "$cmd" &> /dev/null; then log ERROR "Required command not found: $cmd" return 1 fi return 0}
# Mainmain() { log INFO "Starting utility script"
if require ls; then log INFO "ls is available" fi
if require nonexistent_cmd; then log INFO "Command available" else log WARN "Missing optional command" fi
log INFO "Done"}
main "$@"EOF
chmod +x /tmp/utility.sh/tmp/utility.shPart 5: Error Handling
Section titled “Part 5: Error Handling”cat > /tmp/safe_script.sh << 'EOF'#!/bin/bashset -euo pipefail
# Cleanup on exitcleanup() { local exit_code=$? echo "Cleaning up (exit code: $exit_code)..." rm -f /tmp/safe_script_temp_* exit $exit_code}trap cleanup EXIT
# Error handleron_error() { echo "Error on line $1" >&2}trap 'on_error $LINENO' ERR
# Main logicecho "Creating temp file..."tempfile=$(mktemp /tmp/safe_script_temp_XXXXXX)echo "Temp file: $tempfile"
echo "Writing data..."echo "Hello" > "$tempfile"
echo "Reading data..."cat "$tempfile"
echo "Done - cleanup will run automatically"EOF
chmod +x /tmp/safe_script.sh/tmp/safe_script.shSuccess Criteria
Section titled “Success Criteria”- Created script with variables and arguments
- Wrote conditionals for file checking
- Used loops to process files
- Created and used functions
- Implemented error handling with traps
Key Takeaways
Section titled “Key Takeaways”-
Quote your variables —
"$var"prevents word splitting bugs -
Use
set -euo pipefail— Catch errors early -
Functions are essential — For readable, reusable code
-
[[ ]]over[ ]— Safer and more features in Bash -
Test with edge cases — Empty strings, spaces, special characters
What’s Next?
Section titled “What’s Next?”In Module 7.2: Text Processing, you’ll learn grep, sed, awk, and jq—the essential tools for processing text and data in shell scripts.
Further Reading
Section titled “Further Reading”- Bash Reference Manual
- ShellCheck — Script linter
- Bash Pitfalls
- Google Shell Style Guide