Shells like Bash (Bourne Again Shell), Zsh, and sh (Bourne Shell) interpret scripts. Bash is the most common (default on Linux/macOS) and offers advanced features (arrays, associative arrays, regex). Always target a specific shell for consistency—avoid relying on non-portable features if scripting for POSIX-compliant environments (use sh).
A shell script typically includes:
Example minimal structure:
#!/bin/bash
# Description: A simple script
echo "Hello, World!"
The shebang (#!) line tells the OS which interpreter to use. Always specify it explicitly to avoid unexpected behavior.
Bad:
#!/bin/sh # May use a minimal shell (e.g., dash) with limited features
Good:
#!/bin/bash # Explicitly use Bash for advanced features
For POSIX compliance (portable across shells like sh, dash):
#!/bin/sh
Prevent silent failures by enabling strict error handling with set options:
-e: Exit immediately if any command fails.-u: Treat unset variables as errors (avoids $undefined bugs).-o pipefail: Exit if any command in a pipeline fails (not just the last one).Example:
#!/bin/bash
set -euo pipefail # Enable strict mode
# Script will exit here if "file.txt" doesn't exist (due to -e)
cat "file.txt"
# Script will exit here if "undefined_var" is unset (due to -u)
echo "$undefined_var"
# Pipeline fails if "grep" fails (due to pipefail)
false | grep "pattern"
Exception: Allow commands to fail with || true:
grep "optional-pattern" file.txt || true # Don't exit if grep finds nothing
Unquoted variables cause word splitting and globbing (e.g., $file becomes file1 file2 if file="file*").
Bad:
file="my document.txt"
cat $file # Fails: "my" and "document.txt" are treated as separate files
Good:
file="my document.txt"
cat "$file" # Safely handles spaces/glob characters
Limit variable scope with local to avoid polluting the global namespace.
Example:
greet() {
local name="$1" # Local to the function
echo "Hello, $name!"
}
greet "Alice"
echo "$name" # Error: "name" is unset (good!)
Reserve uppercase variables for environment variables (e.g., PATH, HOME). Use lowercase for script variables to prevent conflicts.
Bad:
FILENAME="data.txt" # Risk of overwriting environment variables
Good:
filename="data.txt"
Functions improve reusability, readability, and testability. Use descriptive names and return codes for success/failure.
Example Function:
# Validate if a file exists and is readable
validate_file() {
local file="$1"
if [ ! -f "$file" ]; then
echo "Error: File '$file' not found." >&2 # Redirect error to stderr
return 1
elif [ ! -r "$file" ]; then
echo "Error: No read permissions for '$file'." >&2
return 1
fi
return 0 # Success
}
# Usage
if validate_file "data.txt"; then
echo "Processing 'data.txt'..."
cat "data.txt"
fi
Always validate user input (e.g., required arguments, file existence). Use getopts for parsing flags/options.
Example:
#!/bin/bash
set -euo pipefail
if [ $# -eq 0 ]; then
echo "Usage: $0 <input-file>" >&2
exit 1
fi
input_file="$1"
validate_file "$input_file" # Reuse the validate_file function above
getopts for FlagsHandle options like -v (verbose) or -o <output> with getopts:
#!/bin/bash
set -euo pipefail
output_file="output.txt"
verbose=0
# Parse flags: -o <file>, -v
while getopts "o:v" opt; do
case "$opt" in
o) output_file="$OPTARG" ;;
v) verbose=1 ;;
\?) echo "Invalid option: -$OPTARG" >&2; exit 1 ;;
:) echo "Option -$OPTARG requires an argument." >&2; exit 1 ;;
esac
done
if [ "$verbose" -eq 1 ]; then
echo "Verbose mode enabled. Output: $output_file"
fi
Subshells ($(...) or backticks) spawn new processes. Use built-in commands or avoid subshells when possible.
Bad:
# Slow: Spawns a subshell for each iteration
for file in $(ls *.txt); do # Also risky (ls output may have spaces)
cat "$file"
done
Good:
# Faster: No subshell, and safer (globbing handles spaces)
for file in *.txt; do
cat "$file"
done
Prefer built-ins (e.g., [[ ]] over [ ], (( )) for arithmetic) over external tools (e.g., expr).
Example:
# Slow: Uses external `expr`
count=$(expr 1 + 2)
# Faster: Bash built-in arithmetic
count=$((1 + 2))
Minimize file reads/writes. For example, append to a file once instead of in a loop:
Bad:
# Slow: Multiple I/O operations
for i in {1..1000}; do
echo "$i" >> numbers.txt # Opens/closes the file 1000 times
done
Good:
# Faster: Single I/O operation
{
for i in {1..1000}; do
echo "$i"
done
} > numbers.txt # Opens the file once
Explain why, not what. Focus on intent and complex logic.
Example:
#!/bin/bash
set -euo pipefail
# Clean up temporary files on script exit (critical for reliability)
trap 'rm -f "$temp_file"' EXIT
temp_file=$(mktemp) # Create a temporary file
Use 2/4-space indentation, and keep lines short (wrap at 80 chars).
Split large scripts into smaller, reusable functions or even separate files (sourced with source ./utils.sh).
Redirect output to a log file for auditing:
LOG_FILE="script_$(date +%Y%m%d).log"
exec > >(tee -a "$LOG_FILE") 2>&1 # Log stdout/stderr to file and console
echo "Script started at $(date)"
set -xEnable debugging to trace execution:
#!/bin/bash
set -euo pipefail
set -x # Print commands as they execute (debug mode)
rm -f "temp.txt"
touch "temp.txt"
set +x # Disable debugging
$file → "$file").set -e: Commands expected to fail (e.g., grep "optional" file) will exit the script. Use || true to bypass.for i in {1..5} (no parentheses).command || { echo "Failed"; exit 1; }).ls in Scripts: ls *.txt breaks with filenames containing spaces. Use globbing (*.txt) instead.Store lists of values with arrays (Bash 3+):
files=("file1.txt" "file2.txt" "file3.txt")
for file in "${files[@]}"; do # Note: Use "${array[@]}" to preserve spaces
echo "$file"
done
Use key-value pairs:
declare -A config=(
["user"]="admin"
["port"]="8080"
)
echo "User: ${config[user]}, Port: ${config[port]}"
Use trap to handle signals (e.g., SIGINT, EXIT) and clean up temporary files:
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT # Delete temp dir on exit
Pass output of a command as a “file” (avoids temporary files):
# Compare two command outputs without temp files
diff <(sort file1.txt) <(sort file2.txt)
Efficient shell scripting combines strict error handling, readability, and performance optimization. By adopting practices like strict mode (set -euo pipefail), variable quoting, function encapsulation, and input validation, you’ll write scripts that are robust, maintainable, and less prone to bugs.
Remember: The best scripts are those that are easy to debug, modify, and understand—even by your future self.