Table of Contents
- Understanding the Basics: Shebang and Execution
- Mastering Variables and Quoting
- Conditional Statements: Making Decisions in Scripts
- Loops: Automating Repetitive Tasks
- Command Substitution: Capturing Output
- Handling Arguments Like a Pro
- Functions: Reusable Code Blocks
- Error Handling: Writing Robust Scripts
- Input/Output Redirection and Here-Documents
- Advanced Tips: Arrays, Debugging, and
getopts - Best Practices for Clean, Maintainable Scripts
- Conclusion
- References
1. Understanding the Basics: Shebang and Execution
Every bash script starts with a shebang line, which tells the system which interpreter to use. Without it, your script may run in a different shell (e.g., sh), leading to unexpected behavior.
Shebang Line
The shebang line is always the first line of the script:
#!/bin/bash
This explicitly specifies that the script should run with bash, not the default sh (which may lack bash-specific features like arrays or advanced conditionals).
Making Scripts Executable
To run a script, you need to make it executable with chmod:
chmod +x my_script.sh
Then execute it using:
./my_script.sh # Run from the current directory
Example: Hello World Script
#!/bin/bash
echo "Hello, Automation!"
Save this as hello.sh, run chmod +x hello.sh, then ./hello.sh—you’ll see Hello, Automation! printed to the terminal.
2. Mastering Variables and Quoting
Variables let you store and manipulate data in scripts. Proper quoting ensures your script handles spaces, special characters, and expansions correctly.
Declaring Variables
Variables are declared without spaces around the = sign:
name="Alice"
age=30
To access a variable, prefix it with $:
echo "Name: $name, Age: $age" # Output: Name: Alice, Age: 30
Quoting: Single vs. Double Quotes
-
Double quotes (
" "): Allow variable expansion and command substitution.
Example:echo "Hello, $name!" # Output: Hello, Alice! -
Single quotes (
' '): Treat text as a literal (no expansion).
Example:echo 'Hello, $name!' # Output: Hello, $name! (no variable expansion)
Pro Tip: Use double quotes for most cases to preserve spaces in variables (e.g., file="my document.txt"), and single quotes when you need to avoid expansion (e.g., grep 'error$' log.txt).
3. Conditional Statements: Making Decisions in Scripts
Conditionals let your script act based on logic (e.g., “if a file exists, back it up”). Bash supports if-else blocks and case statements.
if-else Statements
The basic syntax uses [ ] (POSIX-compliant) or [[ ]] (bash-specific, more powerful):
if [ condition ]; then
# Code to run if true
elif [ another_condition ]; then
# Code if first condition is false, second is true
else
# Code if all conditions are false
fi
Common Conditions:
-f file: True iffileexists and is a regular file.-d dir: True ifdirexists and is a directory.-z string: True ifstringis empty.$a -eq $b: True ifaequalsb(numeric comparison).$a == $b: True ifaequalsb(string comparison, use[[ ]]for pattern matching).
Example: Check if a File Exists
#!/bin/bash
file="data.txt"
if [ -f "$file" ]; then
echo "$file exists. Backing up..."
cp "$file" "$file.bak"
else
echo "$file not found. Creating it..."
touch "$file"
fi
case Statements
Use case for multiple condition checks (cleaner than nested if-else):
case $variable in
pattern1)
# Code for pattern1
;;
pattern2|pattern3)
# Code for pattern2 or pattern3
;;
*)
# Default case (matches anything else)
;;
esac
Example: Simple Menu
#!/bin/bash
echo "Choose an option: [1] Backup, [2] Restore, [3] Exit"
read choice
case $choice in
1)
echo "Starting backup..."
# Add backup logic here
;;
2)
echo "Starting restore..."
# Add restore logic here
;;
3)
echo "Exiting..."
exit 0
;;
*)
echo "Invalid option!"
;;
esac
4. Loops: Automating Repetitive Tasks
Loops let you repeat actions (e.g., process all files in a directory or read lines from a log). Bash supports for, while, and until loops.
for Loops
Iterate over a list of items (files, numbers, etc.):
# Iterate over files
for file in *.txt; do
echo "Processing $file..."
# Add logic (e.g., grep, sed) here
done
# Iterate over numbers (Bash 4.0+)
for i in {1..5}; do
echo "Count: $i"
done
while Loops
Run code as long as a condition is true (great for reading input):
# Read lines from a file until EOF
while IFS= read -r line; do
echo "Line: $line"
done < input.txt # Redirect file into the loop
# Infinite loop (use Ctrl+C to exit)
while true; do
echo "Press Ctrl+C to stop..."
sleep 1
done
until Loops
Run code until a condition is true (opposite of while):
count=0
until [ $count -eq 3 ]; do
echo "Count: $count"
count=$((count + 1)) # Increment count
done
# Output: Count: 0, Count: 1, Count: 2
5. Command Substitution: Capturing Output
Command substitution lets you store the output of a command in a variable. Use $(command) (preferred) or backticks `command`.
Example: Capture Current Date
current_date=$(date +%Y-%m-%d) # Format: YYYY-MM-DD
echo "Today is $current_date" # Output: Today is 2024-05-20
# Or with backticks (older syntax)
current_time=`date +%H:%M:%S`
echo "Current time: $current_time"
Pro Tip: Use $(...) instead of backticks—they’re easier to nest and read:
# Nested substitution: Get the size of the largest .log file
largest_log_size=$(du -h *.log | sort -rh | head -n 1 | awk '{print $1}')
echo "Largest log file size: $largest_log_size"
6. Handling Arguments Like a Pro
Scripts often need input from users (e.g., ./backup.sh /data). Bash provides special variables to access these arguments:
| Variable | Description |
|---|---|
$0 | Name of the script (e.g., ./backup.sh). |
$1, $2... | Positional arguments (e.g., $1 is the first argument). |
$@ | All arguments as a list (e.g., "$@" preserves spaces in arguments). |
$# | Number of arguments. |
$? | Exit status of the last command (0 = success, non-zero = error). |
Example: Script with Arguments
#!/bin/bash
# Usage: ./greet.sh "Name" "Title"
name="$1"
title="$2"
if [ $# -ne 2 ]; then # Check if exactly 2 arguments are provided
echo "Error: Usage - $0 <Name> <Title>"
exit 1 # Exit with error code 1
fi
echo "Hello, $title $name! Welcome."
Run it with ./greet.sh "Doe" "Dr."—output: Hello, Dr. Doe! Welcome..
7. Functions: Reusable Code Blocks
Functions let you group code for reuse, making scripts modular and easier to maintain.
Defining Functions
function greet {
echo "Hello, $1!" # $1 is the first argument to the function
}
# Or shorter syntax (no "function" keyword)
greet() {
echo "Hello, $1!"
}
Using Functions
Call functions like any command, passing arguments:
greet "Alice" # Output: Hello, Alice!
# Function with return value (via echo, since bash functions return exit codes)
add() {
echo $(( $1 + $2 ))
}
sum=$(add 5 3) # Capture output with command substitution
echo "Sum: $sum" # Output: Sum: 8
8. Error Handling: Writing Robust Scripts
A good script doesn’t crash silently—it handles errors gracefully. Here are key techniques:
set -e: Exit on Error
Add set -e at the top of your script to exit immediately if any command fails (avoids running broken code):
#!/bin/bash
set -e # Exit on any error
cp important.txt backup/ # If this fails, script exits here
echo "Backup successful!" # Only runs if cp succeeded
set -u: Catch Undefined Variables
Use set -u to exit if the script uses an undefined variable (prevents silent failures):
#!/bin/bash
set -u
echo "Hello, $name!" # Error: name is undefined (script exits)
trap: Clean Up on Exit
Use trap to run commands (e.g., clean up temporary files) when the script exits, even if it errors:
#!/bin/bash
temp_file=$(mktemp) # Create a temporary file
# Clean up temp_file on exit (0 = success, 1 = error, etc.)
trap 'rm -f "$temp_file"' EXIT
# Script logic here (e.g., write to temp_file)
echo "Temporary data" > "$temp_file"
9. Input/Output Redirection and Here-Documents
Bash lets you redirect command input/output to files or other commands, and create multi-line text blocks with here-documents.
Redirection Operators
| Operator | Description |
|---|---|
command > file | Overwrite file with command’s output. |
command >> file | Append command’s output to file. |
command 2> error.log | Redirect errors (stderr) to error.log. |
command &> output.log | Redirect both stdout and stderr to output.log. |
command < input.txt | Read input from input.txt instead of the terminal. |
Example: Logging Output and Errors
#!/bin/bash
# Log all output (stdout + stderr) to script.log
./long_running_task.sh &> script.log
Here-Documents
Use << to pass multi-line input to a command (e.g., write a config file):
# Write to a file
cat > config.ini << EOF
[Server]
Port=8080
Host=localhost
EOF
# Pass to a command (e.g., ssh)
ssh user@server << EOF
echo "Remote command 1"
echo "Remote command 2"
EOF
EOF is a delimiter (can be any word); lines between << EOF and EOF are passed as input.
10. Advanced Tips: Arrays, Debugging, and getopts
Arrays (Bash 4.0+)
Arrays store lists of values (e.g., filenames, IPs):
#!/bin/bash
fruits=("apple" "banana" "cherry")
# Access an element (0-based index)
echo "First fruit: ${fruits[0]}" # Output: apple
# Loop over all elements
for fruit in "${fruits[@]}"; do
echo "Fruit: $fruit"
done
# Add an element
fruits+=("date")
Debugging with set -x
Add set -x to the script to print each command before running it (great for debugging):
#!/bin/bash
set -x # Enable debugging
name="Alice"
echo "Hello, $name!" # Script prints: + name=Alice, + echo 'Hello, Alice!'
getopts: Handle Flags and Options
Use getopts to parse flags like -v (verbose) or -f file (advanced argument handling):
#!/bin/bash
verbose=0
file=""
# Parse options: -v (no arg), -f (requires arg)
while getopts "vf:" opt; do
case $opt in
v) verbose=1 ;;
f) file="$OPTARG" ;; # $OPTARG holds the argument for -f
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
esac
done
if [ $verbose -eq 1 ]; then
echo "Verbose mode enabled. File: $file"
fi
Run with ./script.sh -v -f data.txt—output: Verbose mode enabled. File: data.txt.
11. Best Practices for Clean, Maintainable Scripts
To write scripts that are easy to read and maintain:
- Add Comments: Explain why (not just what) the code does.
- Use Descriptive Names: Variables like
backup_dirinstead ofb. - Test with
shellcheck: A tool to catch syntax errors and bad practices (shellcheck.net). - Avoid Hard-Coded Paths: Use variables (e.g.,
data_dir="/var/data"). - Limit Line Length: Keep lines under 80 characters for readability.
- Test Thoroughly: Test edge cases (missing files, invalid arguments).
12. Conclusion
Bash scripting is a superpower for Linux automation. By mastering variables, conditionals, loops, error handling, and advanced features like arrays and getopts, you can turn tedious tasks into one-click solutions. Start small—automate a daily task like file backups or log rotation—and gradually tackle more complex projects.
Remember: The best scripts are simple, well-documented, and robust. With practice, you’ll be writing scripts that save you time and impress your team!