Table of Contents
-
The Foundation: Script Structure & Essentials
- 1.1 Shebang Line
- 1.2 Script Permissions
- 1.3 Comments & Documentation
-
Variables: Declaring, Using, and Avoiding Pitfalls
- 2.1 Local vs. Global Variables
- 2.2 Quoting Variables to Prevent Word Splitting
-
User Input and Command-Line Arguments
- 3.1 Reading Input with
read - 3.2 Parsing Arguments with
getopts
- 3.1 Reading Input with
-
Control Flow: Conditionals and Loops
- 4.1 Conditional Statements (
if,case) - 4.2 Loops (
for,while,until)
- 4.1 Conditional Statements (
-
Functions: Modularizing Your Code
- 5.1 Defining Functions
- 5.2 Parameters and Return Values
-
Error Handling: Making Scripts Robust
- 6.1 Checking Exit Codes
- 6.2 Using
setfor Strict Mode - 6.3 Cleaning Up with
trap
-
- 7.1
set -xfor Tracing Commands - 7.2 Linting with
shellcheck
- 7.1
1. The Foundation: Script Structure & Essentials
A well-structured script is easier to read, debug, and maintain. Start with these basics:
1.1 Shebang Line
The shebang line (#!) tells the system which interpreter to use. For bash scripts, always use:
#!/bin/bash
Avoid #!/bin/sh unless your script is POSIX-compliant (sh lacks bash-specific features like arrays or [[ ]] conditionals).
1.2 Script Permissions
Make your script executable with chmod:
chmod +x my_script.sh
Run it with ./my_script.sh (or place it in ~/bin or /usr/local/bin for global access).
1.3 Comments & Documentation
Add comments to explain why (not just what) the code does. Use a header comment to describe the script’s purpose, author, and usage:
#!/bin/bash
# Purpose: Backup /home directory to an external drive
# Author: Jane Doe
# Usage: ./backup_home.sh <backup_drive_mount_point>
2. Variables: Declaring, Using, and Avoiding Pitfalls
Variables store data for reuse. Bash is weakly typed, so no declaration is needed—just assign a value.
2.1 Local vs. Global Variables
- Global variables: Visible everywhere in the script (default).
- Local variables: Limited to a function (declare with
local).
GLOBAL_VAR="I'm global" # Global by default
my_function() {
local LOCAL_VAR="I'm local" # Local to the function
echo "$LOCAL_VAR"
}
my_function # Output: I'm local
echo "$LOCAL_VAR" # Error: LOCAL_VAR is undefined (out of scope)
2.2 Quoting Variables to Prevent Word Splitting
Unquoted variables are split into words by whitespace, which can break scripts. Always quote variables with " to preserve spaces:
name="John Doe"
echo Hello, $name # Output: Hello, John Doe (works here, but risky!)
echo "Hello, $name" # Output: Hello, John Doe (safer)
# Risky example:
files="file1.txt file2.txt"
rm $files # Works, but if files contain spaces (e.g., "my file.txt"), this breaks!
rm "$files" # Tries to delete "file1.txt file2.txt" (one file) – wrong! Use arrays instead.
3. User Input and Command-Line Arguments
Scripts often need input from users or command-line arguments.
3.1 Reading Input with read
Use read to capture user input. Common flags:
-p: Prompt message.-s: Silent mode (for passwords).-n <num>: Read only<num>characters.
read -p "Enter your name: " name
echo "Hello, $name!"
read -s -p "Enter password: " password
echo -e "\nPassword received." # -e enables escape characters (e.g., \n)
3.2 Parsing Arguments with getopts
For scripts with flags (e.g., -h for help, -f <file> for a filename), use getopts to parse arguments cleanly:
#!/bin/bash
# Default values
file=""
verbose=0
# Parse options: -f (requires argument), -v (flag), -h (flag)
while getopts "f:vh" opt; do
case $opt in
f) file="$OPTARG" ;; # $OPTARG is the value after -f
v) verbose=1 ;;
h) echo "Usage: $0 -f <file> [-v]"; exit 0 ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
esac
done
if [ -z "$file" ]; then
echo "Error: -f <file> is required." >&2
exit 1
fi
[ $verbose -eq 1 ] && echo "Verbose mode enabled. File: $file"
4. Control Flow: Conditionals and Loops
4.1 Conditional Statements
Use if/elif/else for simple conditions and case for pattern matching.
if Statements
Use [ ] (POSIX) or [[ ]] (bash-specific, supports regex and pattern matching):
num=10
# POSIX-style [ ] (requires spaces around brackets and operators)
if [ "$num" -gt 5 ]; then
echo "$num is greater than 5"
elif [ "$num" -eq 5 ]; then
echo "$num is 5"
else
echo "$num is less than 5"
fi
# Bash-specific [[ ]] (supports pattern matching)
name="Alice"
if [[ "$name" == A* ]]; then # Matches names starting with "A"
echo "Name starts with A: $name"
fi
case Statements
Ideal for multiple fixed-value checks:
day="Monday"
case $day in
Monday|Tuesday|Wednesday|Thursday|Friday)
echo "Weekday" ;;
Saturday|Sunday)
echo "Weekend" ;;
*)
echo "Invalid day" ;;
esac
4.2 Loops
for Loops
Iterate over lists, ranges, or command output:
# Iterate over a list
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do # Use "${array[@]}" to handle spaces in elements
echo "I like $fruit"
done
# Iterate over a range (bash-specific)
for i in {1..5}; do
echo "Count: $i"
done
# Iterate over files (avoid parsing ls!)
for file in *.txt; do
echo "Processing $file"
done
while Loops
Repeat while a condition is true (e.g., read lines from a file):
# Read lines from a file (safer than for loop)
while IFS= read -r line; do # IFS= prevents trimming whitespace; -r preserves backslashes
echo "Line: $line"
done < "input.txt" # Redirect file into loop
5. Functions: Modularizing Your Code
Functions reduce redundancy and improve readability.
5.1 Defining Functions
Use function name { ... } or name() { ... } (POSIX-compliant):
greet() {
local name="$1" # $1 = first argument to the function
echo "Hello, $name!"
}
greet "Bob" # Output: Hello, Bob!
5.2 Parameters and Return Values
- Parameters: Access via
$1,$2, … (like script arguments). - Return values: Use
returnfor exit codes (0-255) orechoto return strings (capture with$(func)).
# Return exit code (0 = success, non-zero = error)
is_positive() {
local num="$1"
if [ "$num" -gt 0 ]; then
return 0 # Success
else
return 1 # Failure
fi
}
if is_positive 5; then
echo "5 is positive"
fi
# Return string via echo
get_greeting() {
local name="$1"
echo "Hello, $name"
}
greeting=$(get_greeting "Alice")
echo "$greeting" # Output: Hello, Alice
6. Error Handling: Making Scripts Robust
Scripts should fail gracefully and clean up resources.
6.1 Checking Exit Codes
Every command returns an exit code ($?): 0 = success, 1-255 = error. Use set -e to exit on any error, or check manually with if:
# Manual check
cp file1.txt backup/
if [ $? -ne 0 ]; then # $? = exit code of last command
echo "Error: cp failed" >&2 # >&2 redirects to stderr
exit 1
fi
# Shorter: check in if statement directly
if ! cp file1.txt backup/; then
echo "Error: cp failed" >&2
exit 1
fi
6.2 Using set for Strict Mode
Enable strict error checking with set options:
-e: Exit on any command failure.-u: Treat undefined variables as errors.-o pipefail: Exit if any command in a pipeline fails (not just the last one).
#!/bin/bash
set -euo pipefail # Strict mode
undefined_var # Error: undefined variable (due to -u)
grep "pattern" file.txt | head -n1 # Fails if grep fails (due to -o pipefail)
6.3 Cleaning Up with trap
Use trap to run commands on script exit (e.g., clean up temporary files):
#!/bin/bash
set -euo pipefail
TMP_FILE=$(mktemp) # Create temp file
# Define cleanup function
cleanup() {
rm -f "$TMP_FILE"
echo "Cleanup complete. Temp file removed."
}
# Run cleanup on EXIT (0), SIGINT (Ctrl+C), or SIGTERM
trap cleanup EXIT SIGINT SIGTERM
# Script logic here...
echo "Working with temp file: $TMP_FILE"
sleep 10 # Press Ctrl+C to test cleanup
7. Debugging Techniques
Debugging bash scripts can be tricky—use these tools:
7.1 set -x for Tracing Commands
Enable command tracing with set -x (disable with set +x). This shows each command as it runs:
#!/bin/bash
set -x # Enable tracing
name="Alice"
echo "Hello, $name"
set +x # Disable tracing
echo "Tracing off"
Output:
+ name=Alice
+ echo 'Hello, Alice'
Hello, Alice
+ set +x
Tracing off
7.2 Linting with shellcheck
shellcheck is a tool that flags errors and bad practices in bash scripts. Install it with sudo apt install shellcheck (Debian/Ubuntu) or brew install shellcheck (macOS).
Example:
# Save as bad_script.sh
greet() {
echo "Hello, $1"
}
greet # Missing argument
# Run shellcheck:
shellcheck bad_script.sh
Output:
In bad_script.sh line 4:
greet # Missing argument
^----^ SC2120: greet is called with no arguments. But function definition has 1 parameter.
8. Best Practices for Maintainable Scripts
- Use descriptive names: Avoid
script.sh; usebackup_home.sh. - Limit line length: Keep lines under 80 characters for readability.
- Avoid hard-coded values: Use variables for paths, filenames, or constants.
- Test incrementally: Test small sections before combining them.
- Version control: Track scripts in Git to revert changes if needed.
9. Advanced Tips: Arrays, Process Substitution, and More
Arrays
Store lists of values with arrays (bash 4+):
colors=("red" "green" "blue")
echo "First color: ${colors[0]}" # red
echo "All colors: ${colors[@]}" # red green blue
echo "Number of colors: ${#colors[@]}" # 3
# Associative arrays (key-value pairs)
declare -A user=(
[name]="John"
[age]=30
[city]="New York"
)
echo "Name: ${user[name]}, Age: ${user[age]}" # Name: John, Age: 30
Process Substitution
Use <(command) to treat command output as a file (avoids temporary files):
# Compare two command outputs without temp files
diff <(ls dir1) <(ls dir2) # Shows differences between ls outputs of dir1 and dir2
10. Conclusion
Effective bash scripting combines structure, clarity, and robustness. By mastering variables, control flow, error handling, and debugging, you can write scripts that automate tasks reliably and save time. Remember to use strict mode (set -euo pipefail), quote variables, and test rigorously. With practice, you’ll create scripts that are not just functional, but maintainable and scalable.