Linux shell scripting is a powerful skill that enables users to automate repetitive tasks, manage system operations, and streamline workflows. Whether you’re a system administrator, developer, or hobbyist, understanding shell scripting can significantly boost your productivity. At its core, a shell script is a text file containing a sequence of commands that the shell (e.g., Bash, Zsh) executes. This blog will guide you from the fundamentals of shell scripting to advanced techniques, with practical examples and best practices to help you write efficient, reliable scripts.
Table of Contents
- What is a Shell Script?
- Getting Started: Your First Script
- Variables: Storing and Manipulating Data
- Input/Output and Redirection
- Control Structures: Making Decisions and Loops
- Functions: Reusable Code Blocks
- Advanced Topics
- Best Practices for Shell Scripting
- Real-World Example: A Backup Script
- Conclusion
- References
1. What is a Shell Script?
A shell script is a plain text file containing a series of commands interpreted by a Unix/Linux shell (e.g., Bash, the default on most Linux distributions). Instead of typing commands manually, you can automate tasks by writing them into a script and executing it.
Why Learn Shell Scripting?
- Automation: Automate repetitive tasks (e.g., backups, log rotation).
- System Administration: Manage users, files, and services efficiently.
- Customization: Create custom tools or shortcuts for your workflow.
2. Getting Started: Your First Script
What You Need
- A Linux terminal (e.g., GNOME Terminal, Konsole).
- A text editor (e.g.,
nano,vim, or VS Code).
Step 1: The Shebang Line
Every script starts with a shebang line (#!), which tells the system which shell to use. For Bash scripts, this is:
#!/bin/bash
Step 2: Write a Simple Script
Create a file named hello_world.sh and add:
#!/bin/bash
# This is a comment (ignored by the shell)
echo "Hello, World!" # Print text to the terminal
Step 3: Make the Script Executable
By default, text files aren’t executable. Use chmod to set executable permissions:
chmod +x hello_world.sh
Step 4: Run the Script
Execute the script with:
./hello_world.sh # Output: Hello, World!
Alternatively, run it directly with the shell (bypassing the shebang line):
bash hello_world.sh
3. Variables: Storing and Manipulating Data
Variables store data for reuse. Shell scripting has two types of variables:
System Variables
Predefined by the shell (e.g., $HOME, $USER, $PATH). Access them with $:
echo "Current user: $USER" # Output: Current user: john
echo "Home directory: $HOME" # Output: Home directory: /home/john
echo "PATH: $PATH" # Output: List of directories for executable files
User-Defined Variables
Create your own variables with name=value (no spaces around =):
name="Alice"
age=30
Access variables with $name or ${name} (curly braces avoid ambiguity):
echo "Name: $name, Age: $age" # Output: Name: Alice, Age: 30
echo "Hello, ${name}!" # Output: Hello, Alice!
String vs. Numeric Variables
- Strings: Enclose in quotes to handle spaces:
greeting="Hello, World" echo "${greeting}!" # Output: Hello, World! - Numbers: No quotes needed for arithmetic:
x=10 y=20 sum=$((x + y)) # Arithmetic expansion with $((...)) echo "Sum: $sum" # Output: Sum: 30
Read Input from Users
Use read to capture user input. Add -p for a prompt:
#!/bin/bash
read -p "Enter your name: " username # Prompt user for input
echo "Hello, $username!" # Output: Hello, [input]!
4. Input/Output and Redirection
Control where input comes from and output goes with redirection and pipes.
Basic Output with echo
echo prints text to the terminal. Use -e to enable escape characters (e.g., \n for newlines):
echo -e "Line 1\nLine 2" # Output:
# Line 1
# Line 2
Redirection
>: Overwrite a file with output (create if it doesn’t exist):echo "Hello" > output.txt # Write "Hello" to output.txt (overwrites old content)>>: Append to a file:echo "World" >> output.txt # Add "World" to output.txt (preserves old content)<: Read input from a file:while read line; do echo "Line: $line"; done < output.txt # Read output.txt line by line2>: Redirect errors (stderr) to a file:ls non_existent_file 2> errors.txt # Log "file not found" error to errors.txt&>: Redirect both stdout and stderr:command &> combined.log # Log all output (normal and errors) to combined.log
Pipes (|)
Chain commands with | to pass output of one command as input to another:
ls -l | grep ".sh" # List files, then filter for .sh scripts
cat output.txt | wc -l # Count lines in output.txt
5. Control Structures: Making Decisions and Loops
Control structures let you execute code conditionally or repeatedly.
If Statements
Check conditions and run code based on results. Syntax:
if [ condition ]; then
# Code to run if condition is true
elif [ another_condition ]; then
# Code if first condition is false, second is true
else
# Code if all conditions are false
fi
Common Conditions:
- Numeric:
-eq(equal),-ne(not equal),-lt(less than),-gt(greater than). - String:
=,!=(equality/inequality),-z(empty string),-n(non-empty string). - File:
-f(file exists),-d(directory exists),-x(executable).
Example 1: Check if a File Exists
#!/bin/bash
file="example.txt"
if [ -f "$file" ]; then
echo "$file exists."
else
echo "$file does NOT exist."
fi
Example 2: Compare Numbers
#!/bin/bash
age=18
if [ $age -lt 18 ]; then
echo "Minor"
elif [ $age -ge 18 ] && [ $age -lt 65 ]; then # Logical AND with &&
echo "Adult"
else
echo "Senior"
fi
Loops
For Loops
Iterate over a list of items (e.g., files, numbers, strings):
# Loop over strings
for fruit in apple banana cherry; do
echo "I like $fruit"
done
# Loop over numbers (C-style syntax)
for ((i=1; i<=5; i++)); do
echo "Count: $i"
done
# Loop over files in a directory
for file in *.sh; do
echo "Script: $file"
done
While Loops
Run code as long as a condition is true:
#!/bin/bash
count=1
while [ $count -le 5 ]; do
echo "Count: $count"
count=$((count + 1)) # Increment count
done
Until Loops
Run code until a condition is true (opposite of while):
#!/bin/bash
count=5
until [ $count -lt 1 ]; do
echo "Countdown: $count"
count=$((count - 1))
done
# Output: Countdown: 5, 4, 3, 2, 1
6. Functions: Reusable Code Blocks
Functions group code for reuse. Define them with:
function_name() {
# Code here
echo "Hello from $function_name"
}
Parameters in Functions
Access arguments with $1, $2, etc. (like script parameters):
greet() {
name=$1 # First argument
echo "Hello, $name!"
}
greet "Bob" # Output: Hello, Bob!
Return Values
Use return for numeric exit codes (0 = success, non-zero = error) or echo to return strings:
add() {
a=$1
b=$2
echo $((a + b)) # Echo result to "return" it
}
sum=$(add 5 3) # Capture output with $()
echo "Sum: $sum" # Output: Sum: 8
7. Advanced Topics
Command Substitution
Capture output of a command into a variable with $(command) or backticks `command`:
current_date=$(date +%Y-%m-%d) # Get date in YYYY-MM-DD format
echo "Today: $current_date" # Output: Today: 2024-05-20
file_count=$(ls -l | wc -l) # Count files in current directory
echo "Files: $file_count"
Arrays
Store multiple values in a single variable:
fruits=("apple" "banana" "cherry") # Declare array
# Access elements (indexes start at 0)
echo "First fruit: ${fruits[0]}" # Output: apple
echo "All fruits: ${fruits[@]}" # Output: apple banana cherry
# Loop through an array
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
Error Handling
set -e: Exit the script if any command fails (avoids silent errors).set -u: Treat undefined variables as errors (prevents typos).trap: Run cleanup commands (e.g., delete temp files) on exit or errors:#!/bin/bash set -euo pipefail # Exit on error, undefined var, or failed pipe temp_file="temp.txt" # Cleanup temp file on script exit (even if it fails) trap 'rm -f "$temp_file"' EXIT echo "Creating temp file..." touch "$temp_file" # ... rest of script ...
Debugging
Debug scripts with Bash options:
-x: Print commands and their arguments as they run (trace execution).-v: Print input lines as they’re read.-n: Check for syntax errors without running.
Example:
bash -x my_script.sh # Run script with debugging output
8. Best Practices for Shell Scripting
Follow these guidelines to write clean, maintainable scripts:
1. Use the Shebang Line
Always start with #!/bin/bash (or your shell of choice) to avoid ambiguity.
2. Comment Liberally
Explain why (not just what) your code does:
#!/bin/bash
# Backup script for /home directory
# Usage: ./backup.sh <destination>
3. Use Descriptive Variable/Function Names
Avoid vague names like x or temp. Use backup_dir or validate_input instead.
4. Quote Variables
Prevent word splitting for variables with spaces:
file="my file.txt"
cat "$file" # Correct: handles spaces
# cat $file # Wrong: splits into "my" and "file.txt"
5. Validate Input
Check for required arguments or valid file paths:
#!/bin/bash
if [ $# -eq 0 ]; then # $# = number of arguments
echo "Error: No destination provided."
echo "Usage: $0 <destination>" # $0 = script name
exit 1 # Exit with error code (non-zero)
fi
6. Use shellcheck
A tool to detect bugs and bad practices. Install it with sudo apt install shellcheck (Debian/Ubuntu) and run:
shellcheck my_script.sh
9. Real-World Example: A Backup Script
Let’s combine everything into a practical script that backs up a directory, logs activity, and cleans up old backups.
#!/bin/bash
set -euo pipefail
# Configuration
source_dir="/home/john/documents"
dest_dir="/mnt/backup"
log_file="$dest_dir/backup_log.txt"
retention_days=30 # Keep backups for 30 days
# Validate source directory exists
if [ ! -d "$source_dir" ]; then
echo "Error: Source directory $source_dir does not exist." | tee -a "$log_file"
exit 1
fi
# Create destination if it doesn't exist
mkdir -p "$dest_dir"
# Generate backup filename with timestamp
timestamp=$(date +%Y%m%d_%H%M%S)
backup_file="$dest_dir/backup_$timestamp.tar.gz"
# Log start time
echo "=== Backup started at $(date) ===" | tee -a "$log_file"
# Create backup with tar
echo "Creating backup: $backup_file" | tee -a "$log_file"
tar -czf "$backup_file" -C "$(dirname "$source_dir")" "$(basename "$source_dir")"
# Check if backup succeeded
if [ $? -eq 0 ]; then # $? = exit code of last command (0 = success)
echo "Backup completed successfully." | tee -a "$log_file"
else
echo "Error: Backup failed." | tee -a "$log_file"
exit 1
fi
# Clean up old backups
echo "Removing backups older than $retention_days days..." | tee -a "$log_file"
find "$dest_dir" -name "backup_*.tar.gz" -mtime +"$retention_days" -delete
# Log completion time
echo "=== Backup finished at $(date) ===" | tee -a "$log_file"
echo "----------------------------------------" | tee -a "$log_file"
10. Conclusion
Shell scripting is a cornerstone of Linux system administration and automation. From simple “Hello World” scripts to complex backup tools, it empowers you to save time and reduce errors. By mastering variables, control structures, functions, and best practices, you’ll be able to tackle a wide range of tasks.
Start small: automate one repetitive task (e.g., renaming files, cleaning logs) and build from there. With practice, you’ll write scripts that are efficient, reliable, and easy to maintain.