Table of Contents
- Basics of Bash Scripting
- Variables and Data Types
- Input/Output Handling
- Control Structures: Conditionals and Loops
- Functions for Reusability
- Error Handling and Debugging
- Working with Files and Directories
- Process Management
- Automation Tasks: Cron, Backups, and Log Rotation
- Security Best Practices
- Advanced Tips and Tools
- Conclusion
- References
1. Basics of Bash Scripting
A bash script is a text file containing a sequence of commands executed by the bash shell. Let’s start with the absolute fundamentals.
Shebang Line
Every script should start with a “shebang” line to specify the interpreter:
#!/bin/bash
This tells the system to run the script with /bin/bash (not the default sh, which may be a limited shell like dash).
Making Scripts Executable
To run a script, you must make it executable with chmod:
chmod +x script.sh # Grant execute permission
./script.sh # Run the script (./ is required if the script is in the current directory)
Hello World Example
The simplest script:
#!/bin/bash
echo "Hello, System Administration!"
2. Variables and Data Types
Variables store data for reuse. Bash is weakly typed, so you don’t declare types (e.g., string, integer).
Declaring Variables
Assign values without spaces around =:
name="SysAdmin"
age=30
Accessing Variables
Use $ to reference a variable:
echo "Name: $name" # Output: Name: SysAdmin
echo "Age: ${age} years" # Curly braces avoid ambiguity (e.g., ${age}years vs $ageyears)
Special Variables
Bash provides built-in variables for common tasks:
| Variable | Purpose | Example |
|---|---|---|
$0 | Script name | echo "Script: $0" → Script: ./script.sh |
$1, $2 | Positional arguments (1st, 2nd input) | ./script.sh arg1 arg2 → $1=arg1 |
$@ | All arguments (as separate strings) | for arg in "$@"; do echo $arg; done |
$# | Number of arguments | echo "Args: $#" → Args: 2 (for 2 inputs) |
$? | Exit code of the last command (0 = success) | ls non_existent_file; echo $? → 2 (error) |
$$ | Process ID (PID) of the script | echo "PID: $$" → PID: 12345 |
Environment Variables
Global variables like PATH, HOME, or USER are set by the system. Use export to make a variable available to child processes:
export BACKUP_DIR="/var/backups" # Now accessible to commands run by the script
3. Input/Output Handling
Bash scripts interact with input (stdin), output (stdout), and errors (stderr).
Standard Streams
- stdin (0): Input (e.g., keyboard, piped data).
- stdout (1): Normal output (default: terminal).
- stderr (2): Error output (default: terminal).
Redirection
Control where output/errors go:
| Operator | Action | Example |
|---|---|---|
> | Overwrite stdout to a file | echo "Log" > output.log |
>> | Append stdout to a file | echo "New log" >> output.log |
2> | Redirect stderr to a file | ls non_existent 2> errors.log |
&> | Redirect both stdout and stderr | command &> combined.log |
Pipes (|)
Send stdout of one command to stdin of another:
ps aux | grep "nginx" # Find nginx processes (ps → grep)
Here Documents
Embed multi-line input directly in the script:
cat << EOF > config.txt
server {
port 80;
host localhost;
}
EOF
4. Control Structures: Conditionals and Loops
Control structures let you execute code conditionally or repeatedly.
Conditionals (if-else)
Check conditions with if, elif, and else. Use [ ] (POSIX) or [[ ]] (bash-specific, supports patterns/regex).
Example: Check if a File Exists
file="/data/backup.tar.gz"
if [[ -f "$file" ]]; then # -f = regular file exists
echo "$file exists."
elif [[ -d "$file" ]]; then # -d = directory exists
echo "$file is a directory."
else
echo "$file not found."
fi
Case Statements
Simplify multi-condition checks:
read -p "Enter action (start/stop/restart): " action
case $action in
start)
echo "Starting service..."
;;
stop)
echo "Stopping service..."
;;
restart)
echo "Restarting service..."
;;
*) # Default case (any other input)
echo "Invalid action: $action"
exit 1 # Exit with error code 1
;;
esac
Loops
for Loops
Iterate over lists (files, arguments, ranges):
# Loop through files in /var/log
for logfile in /var/log/*.log; do
echo "Processing $logfile"
done
# Loop through numbers 1-5
for i in {1..5}; do
echo "Count: $i"
done
while Loops
Run until a condition fails:
count=1
while [[ $count -le 3 ]]; do # -le = less than or equal
echo "Loop $count"
((count++)) # Increment count
done
until Loops
Run until a condition succeeds (opposite of while):
count=5
until [[ $count -eq 0 ]]; do # Stop when count=0
echo "Countdown: $count"
((count--))
done
5. Functions for Reusability
Functions group code into reusable blocks, improving readability and reducing redundancy.
Defining Functions
Two syntaxes:
# Method 1
greet() {
echo "Hello, $1!" # $1 = first argument to the function
}
# Method 2 (bash-specific)
function log_message {
local timestamp=$(date +"%Y-%m-%d %H:%M:%S") # local = variable scoped to function
echo "[$timestamp] $1"
}
Calling Functions
greet "Admin" # Output: Hello, Admin!
log_message "Backup started" # Output: [2024-05-20 14:30:00] Backup started
Return Values
Bash functions return exit codes (0-255) with return, or output values via echo (captured with command substitution):
add_numbers() {
return $(( $1 + $2 )) # Return sum as exit code (limited to 0-255)
}
sum=$(echo $(( $1 + $2 )) ) # Better: use command substitution for values >255
echo "Sum: $sum"
6. Error Handling and Debugging
Robust scripts must handle errors gracefully. Here’s how to make your scripts resilient.
Exit on Error (set -e)
Add set -e at the top of your script to exit immediately if any command fails:
#!/bin/bash
set -e # Exit on error
cd /non/existent/dir # Fails → script exits here
echo "This line never runs"
Treat Unset Variables as Errors (set -u)
set -u prevents silent failures from typos in variable names:
#!/bin/bash
set -u
echo "Name: $nam" # Typo: $nam instead of $name → script exits with error
Trap Signals
Use trap to run commands on signals (e.g., script exit, Ctrl+C):
#!/bin/bash
# Cleanup temporary files on exit
cleanup() {
echo "Cleaning up temp files..."
rm -f /tmp/temp.txt
}
trap cleanup EXIT # Run cleanup when script exits (normal or error)
trap 'echo "Aborted!"; exit 1' SIGINT # Handle Ctrl+C (SIGINT)
Debugging Tools
set -x: Enable trace mode to print commands before execution:#!/bin/bash set -x # Trace on echo "Debugging..." set +x # Trace offshellcheck: A linter for bash scripts (install withsudo apt install shellcheck):shellcheck script.sh # Identifies syntax errors and bad practices
7. Working with Files and Directories
Sysadmins spend 90% of their time managing files. Here’s how to automate file operations.
File/Directory Basics
| Task | Command/Example |
|---|---|
| Create directory | mkdir -p /data/logs ( -p creates parent dirs) |
| Delete directory | rm -rf /old/data ( -r recursive, -f force) |
| Copy files | cp -a /src /dest ( -a archive: preserve permissions) |
| Move/rename files | mv file.txt /new/location/newname.txt |
Checking File Properties
Use conditional operators to validate files:
| Operator | Purpose | Example |
|---|---|---|
-f $file | Is $file a regular file? | if [[ -f "data.txt" ]]; then ... |
-d $dir | Is $dir a directory? | if [[ -d "/data" ]]; then ... |
-x $file | Is $file executable? | if [[ -x "/usr/bin/script.sh" ]]; then ... |
-s $file | Is $file non-empty? | if [[ -s "log.txt" ]]; then ... |
Searching for Files
Use find to locate files by name, size, or modification time:
# Find all .log files modified in the last 7 days, larger than 100MB
find /var/log -type f -name "*.log" -mtime -7 -size +100M -exec ls -lh {} \;
File Permissions
Modify permissions with chmod and ownership with chown:
chmod 600 secret.txt # Read/write for owner only (600)
chmod +x script.sh # Add execute permission
chown www-data:www-data /var/www # Set owner:group to www-data
8. Process Management
Automate monitoring and controlling running processes.
Checking Processes
ps aux: List all processes (user, PID, CPU/memory usage).pgrep: Find PIDs by name:pgrep nginx # Returns PID of nginx (if running)
Killing Processes
kill PID: Send a termination signal (SIGTERM) to a process.pkill nginx: Kill all processes namednginx.kill -9 PID: Force-kill (SIGKILL, cannot be ignored).
Service Monitoring Example
Restart a service if it’s down:
#!/bin/bash
service="nginx"
if ! pgrep "$service" >/dev/null; then # >/dev/null suppresses output
echo "$service is down! Restarting..."
systemctl start "$service"
else
echo "$service is running."
fi
9. Automation Tasks
Bash scripting shines for repetitive tasks like backups, log rotation, and scheduled jobs.
Cron Jobs (Scheduling)
Use cron to run scripts at fixed intervals. Edit cron jobs with crontab -e:
Cron Syntax:
* * * * * command_to_run
| | | | |
| | | | +-- Day of week (0=Sun, 6=Sat)
| | | +---- Month (1-12)
| | +------ Day of month (1-31)
| +-------- Hour (0-23)
+---------- Minute (0-59)
Example: Daily Backup at 2 AM
# Add to crontab -e
0 2 * * * /path/to/backup.sh >> /var/log/backup.log 2>&1
Backup Script Example
Use tar or rsync for backups:
#!/bin/bash
set -euo pipefail # Exit on error, unset var, or pipe failure
backup_dir="/backups"
source="/data"
timestamp=$(date +"%Y%m%d_%H%M%S")
archive="$backup_dir/backup_$timestamp.tar.gz"
# Create backup
echo "Creating backup: $archive"
tar -czf "$archive" "$source"
# Delete backups older than 30 days
find "$backup_dir" -name "backup_*.tar.gz" -mtime +30 -delete
Log Rotation
Prevent log files from filling disks with logrotate (system tool) or a custom script:
Custom Log Rotation Script:
#!/bin/bash
log_file="/var/log/app.log"
# Rotate if log is >100MB
if [[ $(du -m "$log_file" | cut -f1) -gt 100 ]]; then
mv "$log_file" "$log_file.old"
gzip "$log_file.old" # Compress old log
touch "$log_file" # Create new log file
chmod 644 "$log_file"
systemctl restart app # Restart app to use new log
fi
10. Security Best Practices
Secure scripts prevent data breaches and system compromise.
Input Validation
Sanitize user input to avoid injection attacks:
read -p "Enter username: " user
# Allow only letters, numbers, and underscores
if [[ ! "$user" =~ ^[a-zA-Z0-9_]+$ ]]; then
echo "Invalid username!"
exit 1
fi
Least Privilege
Avoid running scripts as root. Use sudo for specific commands instead:
# Bad: Run entire script as root
# Good: Use sudo for critical commands only
sudo chown www-data:www-data /var/www # Specific sudo call
Avoid Hardcoded Secrets
Store credentials in environment variables or secure vaults (e.g., HashiCorp Vault), not in scripts:
# Bad: Hardcoded password
# password="secret123"
# Good: Use environment variable
password="$DB_PASSWORD" # Set via export DB_PASSWORD="secret123"
11. Advanced Tips
Take your scripts to the next level with these pro techniques.
Arrays
Store lists of values:
servers=("server1" "server2" "server3")
echo "First server: ${servers[0]}" # Output: server1
echo "All servers: ${servers[@]}" # Output: server1 server2 server3
Associative Arrays (Dictionaries)
Store key-value pairs (bash 4+):
declare -A services=(
["web"]="nginx"
["db"]="mysql"
)
echo "Web service: ${services[web]}" # Output: nginx
Arithmetic Operations
Use $(( )) or bc for math:
sum=$(( 2 + 2 ))
echo "Sum: $sum" # Output: 4
# Floating-point math with bc
average=$(echo "scale=2; (10 + 20)/2" | bc) # scale=2 = 2 decimal places
Command Substitution
Capture command output into variables:
disk_usage=$(df -h / | awk 'NR==2 {print $5}') # Get root disk usage (e.g., 30%)
echo "Disk usage: $disk_usage"
12. Conclusion
Bash scripting is the backbone of system administration automation. From backups to log rotation, from process monitoring to security hardening, these skills will save you hours of manual work and reduce human error.
Start small: write a script to clean up logs, then a backup script, then a cron job to run it. Use shellcheck to refine your code, and never stop experimenting. With practice, you’ll build a library of scripts that make you a more efficient, effective sysadmin.