Master bash scripting with variables, functions, control flow, and error handling. Build production-grade scripts for DevOps and automation.

Welcome to the final episode of the Linux Mastery series. We've covered:
Now we're bringing it all together with bash scripting mastery.
Bash scripting is where Linux becomes powerful. It's how you:
In this episode, we'll learn to write production-grade bash scripts that are:
By the end, you'll be able to write scripts that solve real-world problems and automate your infrastructure.
This is where theory meets practice. Let's build something useful.
Every production script should follow a consistent structure:
#!/bin/bash
################################################################################
# Script: backup_database.sh
# Description: Backs up the application database daily
# Author: DevOps Team
# Version: 1.0
# Last Modified: 2026-02-20
################################################################################
set -euo pipefail # Exit on error, undefined variables, pipe failures
# Configuration
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly LOG_FILE="/var/log/backup.log"
readonly BACKUP_DIR="/var/backups/database"
readonly RETENTION_DAYS=30
# Colors for output
readonly RED='\033[0;31m'
readonly GREEN='\033[0;32m'
readonly YELLOW='\033[1;33m'
readonly NC='\033[0m' # No Color
################################################################################
# Functions
################################################################################
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
error() {
echo -e "${RED}[ERROR]${NC} $*" >&2 | tee -a "$LOG_FILE"
exit 1
}
success() {
echo -e "${GREEN}[SUCCESS]${NC} $*" | tee -a "$LOG_FILE"
}
################################################################################
# Main Script
################################################################################
main() {
log "Starting database backup..."
# Your script logic here
success "Backup completed successfully"
}
# Run main function
main "$@"Document your scripts thoroughly:
#!/bin/bash
# Script purpose and usage
# Usage: ./script.sh [options] [arguments]
# Options:
# -h, --help Show this help message
# -v, --verbose Enable verbose output
# -d, --dry-run Show what would be done without doing it
# Function documentation
# backup_database()
# Description: Backs up the database to the specified location
# Arguments:
# $1 - Database name
# $2 - Backup destination
# Returns:
# 0 on success, 1 on failure
backup_database() {
local db_name="$1"
local backup_dest="$2"
# Implementation
}Always handle errors:
#!/bin/bash
# Exit on any error
set -e
# Exit on undefined variable
set -u
# Exit on pipe failure
set -o pipefail
# Or combine all
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO"' ERR
# Check command success
if ! command -v docker &> /dev/null; then
echo "Docker is not installed"
exit 1
fi# Simple variable
NAME="Alice"
# Variable with spaces (use quotes)
FULL_NAME="Alice Johnson"
# Numeric variable
AGE=30
# Array variable
FRUITS=("apple" "banana" "orange")
# Associative array (Bash 4+)
declare -A PERSON=([name]="Alice" [age]="30")
# Read-only variable
readonly API_KEY="secret123"
# Unset variable
unset TEMP_VAR# String
NAME="Alice"
# Integer
COUNT=42
# Array (indexed)
COLORS=("red" "green" "blue")
echo "${COLORS[0]}" # red
echo "${COLORS[@]}" # all elements
# Associative array
declare -A CONFIG=([host]="localhost" [port]="8080")
echo "${CONFIG[host]}" # localhost
# Boolean (convention)
DEBUG=true
if [ "$DEBUG" = true ]; then
echo "Debug mode enabled"
fi| Variable | Meaning |
|---|---|
$0 | Script name |
$1, $2, ... | Positional arguments |
$@ | All arguments (as separate words) |
$* | All arguments (as single word) |
$# | Number of arguments |
$? | Exit status of last command |
$$ | Process ID of script |
$! | Process ID of last background process |
$- | Current shell options |
$_ | Last argument of previous command |
# Default value
echo "${NAME:-default}" # Use default if NAME is unset
# Assign default
echo "${NAME:=default}" # Assign and use default
# Error if unset
echo "${NAME:?Name is required}"
# Use alternate if set
echo "${NAME:+alternate}"
# String length
echo "${#NAME}"
# Substring
echo "${NAME:0:3}" # First 3 characters
# Remove prefix
echo "${NAME#prefix}"
# Remove suffix
echo "${NAME%suffix}"
# Replace
echo "${NAME/old/new}" # Replace first occurrence
echo "${NAME//old/new}" # Replace all occurrences# Simple if
if [ "$AGE" -gt 18 ]; then
echo "Adult"
fi
# If-else
if [ "$AGE" -gt 18 ]; then
echo "Adult"
else
echo "Minor"
fi
# If-elif-else
if [ "$AGE" -lt 13 ]; then
echo "Child"
elif [ "$AGE" -lt 18 ]; then
echo "Teenager"
else
echo "Adult"
fi
# Nested if
if [ -f "$FILE" ]; then
if [ -r "$FILE" ]; then
echo "File is readable"
fi
fiCommon test operators:
| Operator | Meaning |
|---|---|
-eq | Equal (numeric) |
-ne | Not equal (numeric) |
-lt | Less than |
-le | Less than or equal |
-gt | Greater than |
-ge | Greater than or equal |
= | Equal (string) |
!= | Not equal (string) |
-z | String is empty |
-n | String is not empty |
-f | File exists and is regular file |
-d | Directory exists |
-r | File is readable |
-w | File is writable |
-x | File is executable |
-e | File exists |
# Numeric comparison
if [ "$COUNT" -gt 10 ]; then
echo "Count is greater than 10"
fi
# String comparison
if [ "$NAME" = "Alice" ]; then
echo "Hello Alice"
fi
# File tests
if [ -f "$FILE" ]; then
echo "File exists"
fi
# Logical operators
if [ "$AGE" -gt 18 ] && [ "$AGE" -lt 65 ]; then
echo "Working age"
fi
if [ "$STATUS" = "active" ] || [ "$STATUS" = "pending" ]; then
echo "Status is active or pending"
fi
# Negation
if [ ! -f "$FILE" ]; then
echo "File does not exist"
fi# Simple case
case "$1" in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
*)
echo "Unknown command: $1"
exit 1
;;
esac
# Pattern matching
case "$FILE" in
*.txt)
echo "Text file"
;;
*.log)
echo "Log file"
;;
*)
echo "Unknown file type"
;;
esac# Loop over list
for fruit in apple banana orange; do
echo "Fruit: $fruit"
done
# Loop over array
COLORS=("red" "green" "blue")
for color in "${COLORS[@]}"; do
echo "Color: $color"
done
# Loop over files
for file in *.txt; do
echo "Processing: $file"
done
# C-style loop
for ((i=1; i<=5; i++)); do
echo "Number: $i"
done
# Loop over command output
for line in $(cat file.txt); do
echo "Line: $line"
done# Simple while loop
COUNT=1
while [ "$COUNT" -le 5 ]; do
echo "Count: $COUNT"
((COUNT++))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# Infinite loop (with break)
while true; do
read -p "Enter command (q to quit): " cmd
if [ "$cmd" = "q" ]; then
break
fi
echo "You entered: $cmd"
done# Until loop (opposite of while)
COUNT=1
until [ "$COUNT" -gt 5 ]; do
echo "Count: $COUNT"
((COUNT++))
done# Break out of loop
for i in {1..10}; do
if [ "$i" -eq 5 ]; then
break
fi
echo "$i"
done
# Continue to next iteration
for i in {1..5}; do
if [ "$i" -eq 3 ]; then
continue
fi
echo "$i"
done# Function definition (style 1)
function greet() {
echo "Hello, $1!"
}
# Function definition (style 2)
greet() {
echo "Hello, $1!"
}
# Call function
greet "Alice"
# Function with multiple statements
backup() {
echo "Starting backup..."
tar -czf backup.tar.gz ~/documents
echo "Backup complete"
}# Access parameters
my_function() {
local first_param="$1"
local second_param="$2"
local all_params="$@"
local param_count="$#"
echo "First: $first_param"
echo "Second: $second_param"
echo "All: $all_params"
echo "Count: $param_count"
}
# Call with parameters
my_function "arg1" "arg2" "arg3"# Return exit code
check_file() {
if [ -f "$1" ]; then
return 0 # Success
else
return 1 # Failure
fi
}
# Use return value
if check_file "/etc/passwd"; then
echo "File exists"
else
echo "File not found"
fi
# Return value via echo
get_greeting() {
echo "Hello, $1!"
}
# Capture output
greeting=$(get_greeting "Alice")
echo "$greeting"# Global variable
GLOBAL_VAR="global"
my_function() {
# Local variable (only exists in function)
local local_var="local"
# Can access global
echo "$GLOBAL_VAR"
# Can access local
echo "$local_var"
}
my_function
echo "$local_var" # Empty (local_var doesn't exist here)# Factorial function
factorial() {
local n=$1
if [ "$n" -le 1 ]; then
echo 1
else
local prev=$((n - 1))
local prev_result=$(factorial "$prev")
echo $((n * prev_result))
fi
}
# Call recursive function
result=$(factorial 5)
echo "5! = $result" # Output: 5! = 120# Simple read
read -p "Enter your name: " name
echo "Hello, $name"
# Read without prompt
read input
echo "You entered: $input"
# Read into array
read -a array
echo "First element: ${array[0]}"
# Read with timeout
read -t 5 -p "Enter something (5 seconds): " input
# Read password (hidden input)
read -sp "Enter password: " password
echo ""
echo "Password received"
# Read multiple variables
read -p "Enter name and age: " name age
echo "Name: $name, Age: $age"# Access arguments
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
# Shift arguments
shift # $1 becomes $2, $2 becomes $3, etc.
echo "New first argument: $1"#!/bin/bash
# Process arguments with getopts
while getopts "hv:d:" opt; do
case $opt in
h)
echo "Usage: $0 [-h] [-v level] [-d dir]"
exit 0
;;
v)
VERBOSE="$OPTARG"
;;
d)
DIRECTORY="$OPTARG"
;;
*)
echo "Invalid option: -$OPTARG"
exit 1
;;
esac
done
# Shift to get remaining arguments
shift $((OPTIND - 1))
echo "Verbose: $VERBOSE"
echo "Directory: $DIRECTORY"
echo "Remaining args: $@"# Simple echo
echo "Hello, World!"
# Echo with variables
echo "Name: $NAME, Age: $AGE"
# Echo with escape sequences
echo -e "Line 1\nLine 2\nLine 3"
# Echo without newline
echo -n "Loading..."
# Printf for formatted output
printf "Name: %-10s Age: %3d\n" "Alice" 30
# Colored output
RED='\033[0;31m'
GREEN='\033[0;32m'
NC='\033[0m'
echo -e "${RED}Error${NC}"
echo -e "${GREEN}Success${NC}"# Test if file exists
if [ -e "$FILE" ]; then
echo "File exists"
fi
# Test if regular file
if [ -f "$FILE" ]; then
echo "Is a regular file"
fi
# Test if directory
if [ -d "$DIR" ]; then
echo "Is a directory"
fi
# Test if readable
if [ -r "$FILE" ]; then
echo "File is readable"
fi
# Test if writable
if [ -w "$FILE" ]; then
echo "File is writable"
fi
# Test if executable
if [ -x "$FILE" ]; then
echo "File is executable"
fi
# Test if file is empty
if [ -s "$FILE" ]; then
echo "File is not empty"
fi
# Test if file is newer than another
if [ "$FILE1" -nt "$FILE2" ]; then
echo "FILE1 is newer than FILE2"
fi# Read entire file
content=$(cat file.txt)
echo "$content"
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < file.txt
# Read file with line numbers
while IFS= read -r line; do
((line_num++))
echo "$line_num: $line"
done < file.txt
# Read specific lines
head -n 10 file.txt # First 10 lines
tail -n 5 file.txt # Last 5 lines
sed -n '5,10p' file.txt # Lines 5-10# Write to file (overwrite)
echo "Hello" > file.txt
# Append to file
echo "World" >> file.txt
# Write multiple lines
cat > file.txt << EOF
Line 1
Line 2
Line 3
EOF
# Write with variables
echo "Name: $NAME" > output.txt
# Write from command output
ls -la > listing.txt
# Write to file with redirection
{
echo "Line 1"
echo "Line 2"
echo "Line 3"
} > file.txt# Create file
touch newfile.txt
# Copy file
cp source.txt destination.txt
# Move/rename file
mv oldname.txt newname.txt
# Delete file
rm file.txt
# Create directory
mkdir mydir
# Create nested directories
mkdir -p /path/to/nested/dir
# Remove directory
rmdir emptydir
# Remove directory with contents
rm -r mydir
# Get file size
size=$(stat -f%z file.txt) # macOS
size=$(stat -c%s file.txt) # Linux
# Get file modification time
mtime=$(stat -f%m file.txt) # macOS
mtime=$(stat -c%Y file.txt) # Linux# Indexed array
FRUITS=("apple" "banana" "orange")
# Access element
echo "${FRUITS[0]}" # apple
# Access all elements
echo "${FRUITS[@]}" # apple banana orange
# Array length
echo "${#FRUITS[@]}" # 3
# Add element
FRUITS+=("grape")
# Loop over array
for fruit in "${FRUITS[@]}"; do
echo "$fruit"
done
# Associative array (Bash 4+)
declare -A PERSON
PERSON[name]="Alice"
PERSON[age]="30"
PERSON[city]="NYC"
echo "${PERSON[name]}" # Alice
# Loop over associative array
for key in "${!PERSON[@]}"; do
echo "$key: ${PERSON[$key]}"
done# String length
str="Hello"
echo "${#str}" # 5
# Substring
echo "${str:0:3}" # Hel (first 3 characters)
echo "${str:1:3}" # ell (from position 1, 3 characters)
# Remove prefix
str="prefix_value"
echo "${str#prefix_}" # value
# Remove suffix
str="value_suffix"
echo "${str%_suffix}" # value
# Replace
str="hello world"
echo "${str/world/universe}" # hello universe
# Replace all
str="aaa"
echo "${str//a/b}" # bbb
# Convert to uppercase (Bash 4+)
str="hello"
echo "${str^^}" # HELLO
# Convert to lowercase (Bash 4+)
str="HELLO"
echo "${str,,}" # hello# Arithmetic expansion
result=$((2 + 3))
echo "$result" # 5
# Increment
((count++))
((count += 5))
# Decrement
((count--))
((count -= 3))
# Arithmetic in conditions
if ((count > 10)); then
echo "Count is greater than 10"
fi
# Floating point (use bc)
result=$(echo "scale=2; 10 / 3" | bc)
echo "$result" # 3.33# Command substitution with $()
current_date=$(date)
echo "Current date: $current_date"
# Nested command substitution
result=$(echo $(date +%Y))
# Backticks (older syntax, avoid)
current_date=`date`
# Capture both stdout and stderr
output=$(command 2>&1)
# Capture exit code
command
exit_code=$?# Compare outputs of two commands
diff <(ls dir1) <(ls dir2)
# Read from multiple files
while read line1 && read line2 <&3; do
echo "File1: $line1, File2: $line2"
done < file1.txt 3< file2.txt
# Parallel processing
{
command1
command2
command3
} | sort# Every command returns an exit code
ls /nonexistent
echo $? # Non-zero (error)
ls /home
echo $? # 0 (success)
# Check exit code
if [ $? -eq 0 ]; then
echo "Command succeeded"
else
echo "Command failed"
fi
# Or use && and ||
command && echo "Success" || echo "Failed"
# Exit script with code
exit 0 # Success
exit 1 # Failure#!/bin/bash
# Trap errors
trap 'echo "Error on line $LINENO"; exit 1' ERR
# Trap specific signals
trap 'echo "Script interrupted"; exit 130' INT
# Trap on exit
trap 'echo "Cleaning up..."; rm -f /tmp/tempfile' EXIT
# Trap multiple signals
trap 'handle_error' ERR INT TERM
handle_error() {
echo "An error occurred"
exit 1
}#!/bin/bash
# Enable debug mode
set -x # Print commands as executed
set -v # Print commands as read
# Disable debug mode
set +x
set +v
# Debug specific section
set -x
# Commands here will be printed
set +x
# Use PS4 to customize debug output
export PS4='+ ${BASH_SOURCE}:${LINENO}: '
set -x
# Run script in debug mode
bash -x script.sh
# Run with verbose output
bash -v script.sh#!/bin/bash
LOG_FILE="/var/log/myscript.log"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2 | tee -a "$LOG_FILE"
exit 1
}
success() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] SUCCESS: $*" | tee -a "$LOG_FILE"
}
# Usage
log "Script started"
success "Operation completed"
error "Something went wrong"#!/bin/bash
# System monitoring script
# Monitors CPU, memory, and disk usage
set -euo pipefail
readonly ALERT_THRESHOLD=80
check_cpu() {
local cpu_usage=$(top -bn1 | grep "Cpu(s)" | awk '{print int($2)}')
echo "CPU Usage: ${cpu_usage}%"
if [ "$cpu_usage" -gt "$ALERT_THRESHOLD" ]; then
echo "WARNING: CPU usage is high!"
fi
}
check_memory() {
local mem_usage=$(free | grep Mem | awk '{printf("%.0f", $3/$2 * 100)}')
echo "Memory Usage: ${mem_usage}%"
if [ "$mem_usage" -gt "$ALERT_THRESHOLD" ]; then
echo "WARNING: Memory usage is high!"
fi
}
check_disk() {
local disk_usage=$(df / | tail -1 | awk '{print $5}' | sed 's/%//')
echo "Disk Usage: ${disk_usage}%"
if [ "$disk_usage" -gt "$ALERT_THRESHOLD" ]; then
echo "WARNING: Disk usage is high!"
fi
}
main() {
echo "=== System Monitoring Report ==="
echo "Time: $(date)"
echo ""
check_cpu
echo ""
check_memory
echo ""
check_disk
}
main "$@"#!/bin/bash
# Database backup script
set -euo pipefail
readonly BACKUP_DIR="/var/backups/database"
readonly DB_NAME="myapp"
readonly RETENTION_DAYS=30
readonly LOG_FILE="/var/log/backup.log"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*" >> "$LOG_FILE"
}
backup_database() {
local backup_file="$BACKUP_DIR/${DB_NAME}_$(date +%Y%m%d_%H%M%S).sql.gz"
log "Starting backup of $DB_NAME..."
if mysqldump "$DB_NAME" | gzip > "$backup_file"; then
log "Backup completed: $backup_file"
echo "Backup successful"
else
log "Backup failed"
echo "Backup failed" >&2
return 1
fi
}
cleanup_old_backups() {
log "Cleaning up backups older than $RETENTION_DAYS days..."
find "$BACKUP_DIR" -name "${DB_NAME}_*.sql.gz" -mtime "+$RETENTION_DAYS" -delete
}
main() {
mkdir -p "$BACKUP_DIR"
backup_database
cleanup_old_backups
log "Backup process completed"
}
main "$@"#!/bin/bash
# Log rotation script
set -euo pipefail
readonly LOG_DIR="/var/log/myapp"
readonly MAX_SIZE=$((10 * 1024 * 1024)) # 10 MB
readonly RETENTION_DAYS=30
rotate_log() {
local log_file="$1"
if [ ! -f "$log_file" ]; then
return 0
fi
local file_size=$(stat -c%s "$log_file" 2>/dev/null || echo 0)
if [ "$file_size" -gt "$MAX_SIZE" ]; then
local timestamp=$(date +%Y%m%d_%H%M%S)
mv "$log_file" "${log_file}.${timestamp}"
gzip "${log_file}.${timestamp}"
touch "$log_file"
echo "Rotated: $log_file"
fi
}
cleanup_old_logs() {
find "$LOG_DIR" -name "*.gz" -mtime "+$RETENTION_DAYS" -delete
}
main() {
for log_file in "$LOG_DIR"/*.log; do
rotate_log "$log_file"
done
cleanup_old_logs
}
main "$@"#!/bin/bash
# Application deployment script
set -euo pipefail
readonly APP_DIR="/opt/myapp"
readonly REPO_URL="https://github.com/user/repo.git"
readonly BRANCH="${1:-main}"
log() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] $*"
}
error() {
echo "[$(date +'%Y-%m-%d %H:%M:%S')] ERROR: $*" >&2
exit 1
}
deploy() {
log "Starting deployment of branch: $BRANCH"
# Clone or update repository
if [ -d "$APP_DIR/.git" ]; then
log "Updating existing repository..."
cd "$APP_DIR"
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
else
log "Cloning repository..."
git clone -b "$BRANCH" "$REPO_URL" "$APP_DIR"
cd "$APP_DIR"
fi
# Install dependencies
log "Installing dependencies..."
npm install
# Build application
log "Building application..."
npm run build
# Restart service
log "Restarting service..."
sudo systemctl restart myapp
log "Deployment completed successfully"
}
main() {
deploy || error "Deployment failed"
}
main "$@"# DON'T: Spawn subshell for each iteration
for file in *.txt; do
$(cat "$file") # Slow
done
# DO: Use built-in commands
for file in *.txt; do
cat "$file" # Faster
done
# DON'T: Use grep in a loop
for file in *.log; do
grep "error" "$file" # Slow
done
# DO: Use grep with multiple files
grep "error" *.log # Faster
# DON'T: Use sed in a loop
for file in *.txt; do
sed 's/old/new/' "$file" # Slow
done
# DO: Use sed with multiple files
sed -i 's/old/new/' *.txt # Faster# Use local variables in functions
my_function() {
local var="value" # Faster than global
echo "$var"
}
# Avoid unnecessary subshells
# DON'T
result=$(echo "hello" | tr 'a-z' 'A-Z')
# DO
result="${var^^}" # Bash 4+
# Use built-in string operations
# DON'T
length=$(echo "$str" | wc -c)
# DO
length="${#str}"
# Avoid unnecessary pipes
# DON'T
count=$(cat file.txt | wc -l)
# DO
count=$(wc -l < file.txt)#!/bin/bash
# Benchmark script execution time
time_command() {
local start=$(date +%s%N)
"$@"
local end=$(date +%s%N)
local duration=$(( (end - start) / 1000000 ))
echo "Execution time: ${duration}ms"
}
# Usage
time_command my_function arg1 arg2
# Or use built-in time
time my_function# Validate file path
validate_path() {
local path="$1"
# Check if path is empty
if [ -z "$path" ]; then
echo "Error: Path cannot be empty"
return 1
fi
# Check if path contains dangerous characters
if [[ "$path" =~ [^a-zA-Z0-9._/-] ]]; then
echo "Error: Invalid characters in path"
return 1
fi
return 0
}
# Validate email
validate_email() {
local email="$1"
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
return 0
else
return 1
fi
}
# Validate number
validate_number() {
local num="$1"
if [[ "$num" =~ ^[0-9]+$ ]]; then
return 0
else
return 1
fi
}# Create temporary file securely
temp_file=$(mktemp)
trap "rm -f $temp_file" EXIT
# Create temporary directory securely
temp_dir=$(mktemp -d)
trap "rm -rf $temp_dir" EXIT
# Set restrictive permissions
touch sensitive_file.txt
chmod 600 sensitive_file.txt
# Avoid world-readable files
umask 0077 # New files will be 600# DON'T: Use eval
eval "command $user_input" # Dangerous!
# DO: Use arrays and proper quoting
command "$user_input"
# DON'T: Use backticks with user input
result=`echo $user_input` # Dangerous!
# DO: Use $() with proper quoting
result=$(echo "$user_input")
# DON'T: Use unquoted variables in commands
rm $file_to_delete # Dangerous!
# DO: Quote variables
rm "$file_to_delete"
# DON'T: Use user input in SQL
query="SELECT * FROM users WHERE id=$user_id" # SQL injection!
# DO: Use parameterized queries or escape properly
query="SELECT * FROM users WHERE id='$(printf '%s\n' "$user_id" | sed "s/'/''/g")'"# DON'T: Hardcode credentials
DB_PASSWORD="secret123"
# DO: Read from environment or secure file
DB_PASSWORD="${DB_PASSWORD:-}"
if [ -z "$DB_PASSWORD" ]; then
echo "Error: DB_PASSWORD not set"
exit 1
fi
# DO: Read from secure file with restricted permissions
if [ -f ~/.db_credentials ]; then
source ~/.db_credentials
fi
# DON'T: Log sensitive information
echo "Password: $PASSWORD" # Dangerous!
# DO: Redact sensitive information
echo "Password: ****"Mistake: Not quoting variables, causing word splitting
# DON'T
FILE=my file.txt
cat $FILE # Tries to open "my" and "file.txt"
# DO
FILE="my file.txt"
cat "$FILE" # Opens "my file.txt"Mistake: Using wrong operators for string vs. numeric comparison
# DON'T
if [ "$COUNT" = 10 ]; then # String comparison
echo "Count is 10"
fi
# DO
if [ "$COUNT" -eq 10 ]; then # Numeric comparison
echo "Count is 10"
fiMistake: Not checking if commands succeed
# DON'T
cd /nonexistent
rm -rf * # Deletes current directory!
# DO
cd /nonexistent || exit 1
rm -rf *Mistake: Hardcoding values that should be configurable
# DON'T
BACKUP_DIR="/var/backups"
DB_NAME="mydb"
# DO
BACKUP_DIR="${BACKUP_DIR:-/var/backups}"
DB_NAME="${DB_NAME:-mydb}"
# Or use configuration file
source /etc/myapp/config.sh# Test script with different inputs
./script.sh arg1 arg2
# Test with edge cases
./script.sh "" # Empty argument
./script.sh "special chars !@#$%"
# Test error conditions
./script.sh /nonexistent/path
# Test with different environments
DEBUG=1 ./script.sh
VERBOSE=1 ./script.sh#!/bin/bash
# Simple test framework
test_count=0
test_passed=0
assert_equals() {
local expected="$1"
local actual="$2"
local message="$3"
((test_count++))
if [ "$expected" = "$actual" ]; then
echo "✓ PASS: $message"
((test_passed++))
else
echo "✗ FAIL: $message"
echo " Expected: $expected"
echo " Actual: $actual"
fi
}
# Test functions
test_add() {
local result=$((2 + 3))
assert_equals "5" "$result" "2 + 3 = 5"
}
test_string_length() {
local str="hello"
local length="${#str}"
assert_equals "5" "$length" "Length of 'hello' is 5"
}
# Run tests
test_add
test_string_length
echo ""
echo "Tests passed: $test_passed / $test_count"# Use shellcheck to lint scripts
shellcheck script.sh
# Common issues shellcheck finds
# - Unquoted variables
# - Incorrect test syntax
# - Unused variables
# - Potential bugs
# Format script with shfmt
shfmt -i 2 -w script.shBash is powerful for scripting, but has limitations:
Use Python when:
#!/usr/bin/env python3
import os
import sys
from pathlib import Path
def backup_database(db_name: str, backup_dir: str) -> bool:
"""Backup database to specified directory."""
try:
backup_file = Path(backup_dir) / f"{db_name}.sql.gz"
# Implementation
return True
except Exception as e:
print(f"Error: {e}", file=sys.stderr)
return False
if __name__ == "__main__":
success = backup_database("mydb", "/var/backups")
sys.exit(0 if success else 1)Use Go when:
package main
import (
"fmt"
"os"
)
func backupDatabase(dbName string, backupDir string) error {
// Implementation
return nil
}
func main() {
if err := backupDatabase("mydb", "/var/backups"); err != nil {
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
os.Exit(1)
}
}Rule of thumb: Start with bash for simple scripts. Move to Python for complex logic. Use Go for performance-critical tools.
You've completed the Linux Mastery series:
Official Documentation
Learning Resources
Tools and Utilities
Linux mastery is a journey, not a destination. The skills you've learned in this series are foundational. The real learning happens when you apply these concepts to real-world problems.
Whether you're pursuing a career in DevOps, cloud engineering, SRE, or software engineering, Linux is the foundation. Master it, and you'll be unstoppable.
Keep learning. Keep building. Keep automating.
Welcome to the Linux community.