Bash scripting is a powerful tool for automating tasks, managing systems, and boosting your productivity as a developer. Whether you're a command-line newbie or a seasoned coder, this guide will take you from Bash basics to advanced scripting with practical examples, tips, and resources.
What is Bash?
Bash (Bourne Again SHell) is a Unix shell and command language. It's the default shell on:
Linux distributions
macOS (until Catalina, now Zsh)
Windows Subsystem for Linux (WSL)
Most Unix-like systems
Why Learn Bash Scripting?
🚀 Automation: Save hours by scripting repetitive tasks
🛠 System Administration: Essential for DevOps and sysadmins
💻 Developer Productivity: Combine tools and process data quickly
📊 Data Processing: Powerful text manipulation capabilities
Getting Started
Your First Bash Script
-
Create a new file:
touch hello.sh
-
Add the following content:
#!/bin/bash # This is a comment echo "Hello, World!"
-
Make it executable:
chmod +x hello.sh
-
Run it:
./hello.sh
Key Components:
#!/bin/bash (Shebang line) tells the system to use Bash
# indicates comments
echo prints text to the terminal
Pro Tip: Always include the shebang line for portability, even though Bash is often the default.
Variables and Parameters
Basic Variables
name="Mohammad" # No spaces around = age=10 greeting="Good morning, $name!"
Accessing Variables:
echo "$name is $age years old." # Mohammad is 10 years old. echo "Message: $greeting" # Good morning, Mohammad!
Command Substitution
Store command output in a variable:
current_date=$(date) echo "Today is $current_date" # Alternative syntax files=`ls` echo "Files: $files"
Positional Parameters
Access command-line arguments:
#!/bin/bash echo "Script name: $0" echo "First argument: $1" echo "Second argument: $2" echo "All arguments: $@" echo "Number of arguments: $#"
Run with:
./script.sh arg1 arg2 arg3
Special Variables
Variable | Description |
---|---|
$0 | Script name |
$1-$9 | Positional arguments |
$# | Number of arguments |
$@ | All arguments as separate strings |
$* | All arguments as single string |
$? | Exit status of last command |
$$ | Process ID (PID) of script |
$! | PID of last background command |
Using getopts for Flags
Handle command-line options professionally:
#!/bin/bash while getopts "f:d:v" opt; do case $opt in f) file="$OPTARG" ;; d) dir="$OPTARG" ;; v) verbose=true ;; \?) echo "Invalid option: -$OPTARG" >&2 exit 1 ;; esac done echo "File: ${file:-not specified}" echo "Directory: ${dir:-not specified}" echo "Verbose: ${verbose:-false}"
Usage:
./script.sh -f data.txt -d /home/user -v
Best Practices:
Always quote variables: "$var"
Use ${var:-default} for default values
Prefer $@ over $* to preserve argument separation
Control Structures
Conditionals (if/elif/else)
Basic Syntax:
if [[ condition ]]; then # commands elif [[ another_condition ]]; then # commands else # commands fi
Common Test Operators:
Operator | Description | Example |
---|---|---|
-eq | Equal | [[ "$a" -eq "$b" ]] |
-ne | Not equal | [[ "$a" -ne "$b" ]] |
-lt | Less than | [[ "$a" -lt "$b" ]] |
-gt | Greater than | [[ "$a" -gt "$b" ]] |
-z | String is empty | [[ -z "$str" ]] |
-n | String is not empty | [[ -n "$str" ]] |
== | String equality | [[ "$str1" == "$str2" ]] |
=~ | Regex match | [[ "$str" =~ ^[0-9]+$ ]] |
-e | File exists | [[ -e "file.txt" ]] |
-d | Directory exists | [[ -d "/path" ]] |
Example:
#!/bin/bash read -p "Enter temperature (°C): " temp if [[ "$temp" -gt 30 ]]; then echo "It's hot outside! 🥵" elif [[ "$temp" -lt 10 ]]; then echo "It's cold outside! ❄️" else echo "It's pleasant weather. 🌞" fi
Case Statements
#!/bin/bash read -p "Enter your favorite fruit: " fruit case "$fruit" in apple) echo "Apples are crunchy!" ;; banana) echo "Bananas are rich in potassium!" ;; orange|lemon) echo "Citrus fruits are vitamin C rich!" ;; *) echo "I don't know about $fruit" ;; esac
Loops
For Loop:
# Basic for loop for i in {1..5}; do echo "Number: $i" done # C-style for loop for ((i=0; i<5; i++)); do echo "Count: $i" done # Iterate over files for file in *.txt; do echo "Processing $file" done
While Loop:
count=1 while [[ $count -le 5 ]]; do echo "Count: $count" ((count++)) done # Reading file line by line while IFS= read -r line; do echo "Line: $line" done < "file.txt"
Until Loop:
attempt=1 until ping -c1 example.com &>/dev/null; do echo "Attempt $attempt: Waiting for connection..." ((attempt++)) sleep 1 done echo "Connection established!"
Loop Control:
break: Exit the loop
continue: Skip to next iteration
for i in {1..10}; do if [[ $i -eq 5 ]]; then continue fi if [[ $i -eq 8 ]]; then break fi echo "Number: $i" done
Functions
Basic Function
greet() { local name="$1" # Local variable echo "Hello, $name!" } greet "Alice" # Output: Hello, Alice! greet "Bob" # Output: Hello, Bob!
Returning Values
add() { local sum=$(( $1 + $2 )) echo "$sum" # Output the result } result=$(add 3 5) echo "3 + 5 = $result" # 3 + 5 = 8
Function Parameters
create_user() { local username="$1" local shell="${2:-/bin/bash}" # Default value echo "Creating user $username with shell $shell" # useradd "$username" -s "$shell" } create_user "Mohammad" create_user "Aman" "/bin/zsh"
Best Practices:
Use local for function variables to avoid side effects
Return status codes (0 for success) with return
Use echo to "return" data
Document functions with comments
Error Handling
Exit Codes
Every command returns an exit code (0 = success, non-zero = error):
mkdir /non_existent_dir if [[ $? -ne 0 ]]; then echo "Failed to create directory!" >&2 exit 1 fi
The set Command
Control script behavior:
set -e # Exit immediately if a command fails set -u # Treat unset variables as an error set -o pipefail # Fail if any command in a pipeline fails set -x # Print commands before execution (debugging)
Trapping Signals
cleanup() { echo "Cleaning up..." rm -f tempfile.txt } trap cleanup EXIT INT TERM # Run on exit or interrupt
Logging
log() { local message="$1" local level="${2:-INFO}" local timestamp=$(date +"%Y-%m-%d %H:%M:%S") echo "[$timestamp] [$level] $message" >> script.log } log "Starting script execution" log "Error encountered" "ERROR"
Advanced Topics
Arrays
# Indexed arrays fruits=("apple" "banana" "cherry") echo ${fruits[0]} # apple echo ${fruits[@]} # all elements echo ${#fruits[@]} # array length # Adding elements fruits+=("date") # Associative arrays (Bash 4+) declare -A user user["name"]="Sakir" user["age"]=20 echo "${user["name"]} is ${user["age"]} years old."
String Manipulation
str="Hello World" # Substrings echo ${str:0:5} # Hello echo ${str:6} # World # Replacement echo ${str/World/Everyone} # Hello Everyone echo ${str//l/L} # HeLLo WorLd (global replacement) # Case conversion echo ${str^^} # HELLO WORLD echo ${str,,} # hello world
Arithmetic Operations
a=5 b=3 # Using $(( )) echo $((a + b)) # 8 echo $((a * b)) # 15 echo $((a++)) # 5 (then a becomes 6) # Using let let "sum = a + b" echo $sum # 9 (if a is now 6) # Using expr (older syntax) echo $(expr $a + $b)
Process Substitution
# Compare outputs diff <(ls dir1) <(ls dir2) # Process multiple files paste <(cut -f1 file1) <(cut -f2 file2) > output.txt
Here Documents
cat <<EOF This is a multi-line text block that preserves formatting and variables like $HOME EOF # With variable expansion disabled cat <<'EOF' No variable expansion here $HOME EOF
Performance Tips
-
Avoid unnecessary subshells:
# Slow count=$(ls | wc -l) # Faster files=(*) count=${#files[@]}
-
Use built-in string operations instead of external commands:
# Instead of: uppercase=$(echo "$str" | tr '[:lower:]' '[:upper:]') # Use: uppercase="${str^^}"
-
Batch file operations:
# Instead of: for file in *.txt; do process "$file" done # Consider: find . -name "*.txt" -exec process {} \;
-
Use printf instead of echo for complex output:
printf "Name: %-20s Age: %3d\n" "$name" "$age"
Security Best Practices
-
Always quote variables:
rm "$file" # Safe rm $file # Dangerous (filename with spaces becomes multiple arguments)
-
Validate input:
if [[ ! "$input" =~ ^[a-zA-Z0-9_]+$ ]]; then echo "Invalid input" >&2 exit 1 fi
-
Use mktemp for temporary files:
tempfile=$(mktemp /tmp/script.XXXXXX) echo "Data" > "$tempfile"
-
Avoid eval - it can execute arbitrary code:
# Dangerous! eval "$user_input"
-
Run with least privileges:
# Use sudo only when necessary sudo chown root:root /important/file
Real-World Script Examples
System Backup Script
#!/bin/bash # backup.sh - Automated system backup set -euo pipefail BACKUP_DIR="/backups/$(date +%Y-%m-%d_%H-%M-%S)" LOG_FILE="/var/log/backup.log" mkdir -p "$BACKUP_DIR" log() { echo "[$(date +'%Y-%m-%d %H:%M:%S')] $1" | tee -a "$LOG_FILE" } log "Starting backup to $BACKUP_DIR" # Backup home directories for user in /home/*; do if [[ -d "$user" ]]; then username=$(basename "$user") log "Backing up $username" tar -czf "$BACKUP_DIR/$username.tar.gz" "$user" fi done # Backup system configurations log "Backing up system configurations" tar -czf "$BACKUP_DIR/etc.tar.gz" /etc log "Backup completed successfully"
Log Analyzer
#!/bin/bash # log_analyzer.sh - Analyze log files if [[ $# -eq 0 ]]; then echo "Usage: $0 <logfile> [error_level]" exit 1 fi LOG_FILE="$1" ERROR_LEVEL="${2:-ERROR}" if [[ ! -f "$LOG_FILE" ]]; then echo "Error: File $LOG_FILE not found" >&2 exit 1 fi echo "Analyzing $LOG_FILE for $ERROR_LEVEL messages" declare -A error_counts while IFS= read -r line; do if [[ "$line" =~ \[([^]]+)\] ]]; then timestamp="${BASH_REMATCH[1]}" date_part="${timestamp%% *}" if [[ "$line" =~ $ERROR_LEVEL ]]; then ((error_counts["$date_part"]++)) fi fi done < "$LOG_FILE" echo -e "\nError Count by Date:" for date in "${!error_counts[@]}"; do printf "%s: %d\n" "$date" "${error_counts[$date]}" done | sort
Cheat Sheet
Variable Manipulation
Syntax | Description |
---|---|
${var} | Variable value |
${var:-default} | Use default if var is unset |
${var:=default} | Set default if var is unset |
${#var} | Length of string |
${var:position:length} | Substring |
${var/pattern/repl} | Replace first match |
${var//pattern/repl} | Replace all matches |
Test Operators
Operator | Description |
---|---|
-e | File exists |
-f | Regular file |
-d | Directory |
-r | Readable |
-w | Writable |
-x | Executable |
-z | String is empty |
-n | String is not empty |
Arithmetic
Operator | Description |
---|---|
+ | Addition |
- | Subtraction |
* | Multiplication |
/ | Division |
% | Modulus |
** | Exponentiation |
Resources
Documentation
Tools
Books
Communities
Conclusion
Bash scripting is a must-have skill for developers and system administrators. Start with simple automation tasks and gradually incorporate advanced features as you gain confidence. Key takeaways:
Test thoroughly: Especially for scripts that modify files or systems
Add comments: Document your code for future you
Follow conventions: Make your scripts readable and maintainable
Share your work: Contribute to open source or publish your utilities
Next Steps:
Explore awk and sed for advanced text processing
Learn about cron for scheduling scripts
Dive into shell scripting frameworks like Bash Infinity
Happy scripting! 🐚💻
Discussion Prompt: What's your favorite Bash scripting trick? Share in the comments below! 👇
Top comments (1)
Hi, I tried this, but it does not work. Do you have any suggestions?