Bash
A powerful Unix shell and command language for scripting, automation, and system administration.
Bash
A powerful Unix shell and command language for scripting, automation, and system administration.
Overview
Bash (Bourne Again SHell) is the default shell on most Linux distributions and macOS. It provides a command-line interface for interacting with the operating system and a scripting language for automating tasks. Bash scripts are essential for DevOps workflows, system administration, and development tooling.
flowchart LR
A[Script Input] --> B[Variable Expansion]
B --> C[Command Substitution]
C --> D[Word Splitting]
D --> E[Glob Expansion]
E --> F[Command Execution]
F --> G[Exit Code]
Variables and Parameter Expansion
Variable Assignment and Usage
# Basic assignment (no spaces around =)
name="value"
count=42
# Using variables
echo "$name"
echo "${name}" # Braces for clarity or when needed
# Command substitution
current_date=$(date +%Y-%m-%d)
files=$(ls -la)
# Arithmetic
result=$((5 + 3))
((count++))
Parameter Expansion
# Default values
echo "${var:-default}" # Use default if var is unset or null
echo "${var:=default}" # Assign default if var is unset or null
echo "${var:+alternate}" # Use alternate if var is set and not null
echo "${var:?error msg}" # Exit with error if var is unset or null
# String length
echo "${#name}" # Length of variable
# Substring extraction
str="Hello World"
echo "${str:0:5}" # "Hello" - from position 0, length 5
echo "${str:6}" # "World" - from position 6 to end
echo "${str: -5}" # "World" - last 5 characters (note space)
# Pattern removal
file="/path/to/file.txt"
echo "${file#*/}" # "path/to/file.txt" - remove shortest match from start
echo "${file##*/}" # "file.txt" - remove longest match from start
echo "${file%/*}" # "/path/to" - remove shortest match from end
echo "${file%%/*}" # "" - remove longest match from end
# Pattern substitution
text="hello world"
echo "${text/world/universe}" # "hello universe" - replace first match
echo "${text//l/L}" # "heLLo worLd" - replace all matches
# Case modification (Bash 4+)
echo "${text^}" # "Hello world" - capitalise first char
echo "${text^^}" # "HELLO WORLD" - uppercase all
echo "${text,,}" # "hello world" - lowercase all
Special Variables
$0 # Script name
$1, $2... # Positional parameters
$# # Number of arguments
$@ # All arguments as separate words
$* # All arguments as single word
$$ # Current process ID
$? # Exit code of last command
$! # PID of last background process
$_ # Last argument of previous command
Control Structures
flowchart TD
A[Start] --> B{Condition}
B -->|True| C[Execute Block]
B -->|False| D[Skip/Else]
C --> E[Continue]
D --> E
E --> F[End]
If Statements
# Basic if
if [[ condition ]]; then
commands
fi
# If-else
if [[ "$name" == "admin" ]]; then
echo "Welcome, admin"
elif [[ "$name" == "guest" ]]; then
echo "Limited access"
else
echo "Unknown user"
fi
# Compound conditions
if [[ "$age" -ge 18 && "$age" -le 65 ]]; then
echo "Working age"
fi
if [[ "$status" == "active" || "$status" == "pending" ]]; then
echo "Process continues"
fi
# Negation
if [[ ! -f "$file" ]]; then
echo "File does not exist"
fi
Case Statements
case "$option" in
start)
echo "Starting service"
;;
stop)
echo "Stopping service"
;;
restart|reload)
echo "Restarting service"
;;
*)
echo "Unknown option: $option"
exit 1
;;
esac
# Pattern matching in case
case "$filename" in
*.txt)
echo "Text file"
;;
*.sh)
echo "Shell script"
;;
*.tar.gz|*.tgz)
echo "Compressed archive"
;;
esac
For Loops
# Iterate over list
for item in apple banana cherry; do
echo "$item"
done
# Iterate over files
for file in /var/log/*.log; do
echo "Processing $file"
done
# C-style for loop
for ((i=0; i<10; i++)); do
echo "Iteration $i"
done
# Iterate over array
fruits=("apple" "banana" "cherry")
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Iterate over range
for i in {1..10}; do
echo "$i"
done
# Iterate with step
for i in {0..100..10}; do
echo "$i"
done
While and Until Loops
# While loop
count=0
while [[ $count -lt 5 ]]; do
echo "Count: $count"
((count++))
done
# Read file line by line
while IFS= read -r line; do
echo "Line: $line"
done < input.txt
# Infinite loop with break
while true; do
read -p "Enter command: " cmd
if [[ "$cmd" == "quit" ]]; then
break
fi
echo "You entered: $cmd"
done
# Until loop (opposite of while)
until [[ -f /tmp/ready ]]; do
echo "Waiting for file..."
sleep 1
done
# Continue to skip iterations
for i in {1..10}; do
if [[ $((i % 2)) -eq 0 ]]; then
continue
fi
echo "Odd: $i"
done
Functions and Arguments
Function Definition
# Standard syntax
function greet {
echo "Hello, $1!"
}
# Alternative syntax (POSIX-compatible)
greet() {
echo "Hello, $1!"
}
# Call function
greet "World"
Function Arguments and Return Values
# Access arguments
process_data() {
local input="$1" # First argument
local output="$2" # Second argument
local count="$#" # Number of arguments
local all_args="$@" # All arguments
echo "Processing $input to $output"
echo "Total arguments: $count"
}
# Return values (exit codes)
is_valid() {
if [[ -f "$1" ]]; then
return 0 # Success
else
return 1 # Failure
fi
}
# Using return value
if is_valid "/etc/passwd"; then
echo "File exists"
fi
# Capture output
get_timestamp() {
date +%s
}
timestamp=$(get_timestamp)
echo "Timestamp: $timestamp"
Local Variables and Scope
outer_var="global"
my_function() {
local inner_var="local" # Only visible inside function
outer_var="modified" # Modifies global variable
echo "Inner: $inner_var"
echo "Outer: $outer_var"
}
my_function
echo "After function: $outer_var" # "modified"
echo "Inner: $inner_var" # Empty (out of scope)
Advanced Function Patterns
# Default argument values
greet() {
local name="${1:-Guest}"
echo "Hello, $name!"
}
# Passing arrays to functions
process_array() {
local -n arr=$1 # nameref (Bash 4.3+)
for item in "${arr[@]}"; do
echo "Item: $item"
done
}
my_array=("one" "two" "three")
process_array my_array
# Error handling in functions
safe_cd() {
cd "$1" || {
echo "Failed to change to $1" >&2
return 1
}
}
String Manipulation
Substring Operations
string="Hello, World!"
# Extract substring
echo "${string:0:5}" # "Hello"
echo "${string:7}" # "World!"
echo "${string: -6}" # "World!"
# String length
echo "${#string}" # 13
# Find and replace
echo "${string/World/Bash}" # "Hello, Bash!"
echo "${string//o/0}" # "Hell0, W0rld!"
# Remove prefix/suffix
filename="document.txt.bak"
echo "${filename%.bak}" # "document.txt"
echo "${filename%.*}" # "document.txt"
echo "${filename%%.*}" # "document"
echo "${filename#*.}" # "txt.bak"
echo "${filename##*.}" # "bak"
String Splitting
# Split by delimiter using IFS
string="apple,banana,cherry"
IFS=',' read -ra fruits <<< "$string"
for fruit in "${fruits[@]}"; do
echo "$fruit"
done
# Split path into array
path="/usr/local/bin"
IFS='/' read -ra parts <<< "$path"
# Split and process
data="name:John;age:30;city:London"
IFS=';' read -ra pairs <<< "$data"
for pair in "${pairs[@]}"; do
IFS=':' read -r key value <<< "$pair"
echo "$key = $value"
done
String Comparison and Testing
# Equality
if [[ "$str1" == "$str2" ]]; then
echo "Strings are equal"
fi
# Pattern matching
if [[ "$string" == *"pattern"* ]]; then
echo "Contains pattern"
fi
# Regex matching
if [[ "$email" =~ ^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$ ]]; then
echo "Valid email"
fi
# String length checks
if [[ -z "$string" ]]; then
echo "String is empty"
fi
if [[ -n "$string" ]]; then
echo "String is not empty"
fi
# Lexicographical comparison
if [[ "$str1" < "$str2" ]]; then
echo "$str1 comes before $str2"
fi
Array Operations
Array Declaration and Access
# Indexed arrays
fruits=("apple" "banana" "cherry")
numbers=(1 2 3 4 5)
# Access elements
echo "${fruits[0]}" # "apple"
echo "${fruits[-1]}" # "cherry" (last element)
# All elements
echo "${fruits[@]}" # All elements as separate words
echo "${fruits[*]}" # All elements as single word
# Array length
echo "${#fruits[@]}" # 3
# Array indices
echo "${!fruits[@]}" # 0 1 2
Array Modification
# Append elements
fruits+=("date")
fruits+=("elderberry" "fig")
# Modify element
fruits[1]="blueberry"
# Remove element
unset 'fruits[2]'
# Slice array
subset=("${fruits[@]:1:2}") # 2 elements starting from index 1
# Copy array
copy=("${fruits[@]}")
Associative Arrays (Bash 4+)
# Declare associative array
declare -A user
# Assign values
user[name]="John"
user[age]=30
user[city]="London"
# Access values
echo "${user[name]}"
# All keys
echo "${!user[@]}"
# All values
echo "${user[@]}"
# Iterate
for key in "${!user[@]}"; do
echo "$key: ${user[$key]}"
done
# Check if key exists
if [[ -v user[name] ]]; then
echo "Name is set"
fi
Array Iteration and Processing
# Iterate with index
fruits=("apple" "banana" "cherry")
for i in "${!fruits[@]}"; do
echo "$i: ${fruits[$i]}"
done
# Filter array
filtered=()
for fruit in "${fruits[@]}"; do
if [[ "${#fruit}" -gt 5 ]]; then
filtered+=("$fruit")
fi
done
# Join array elements
IFS=','
joined="${fruits[*]}"
unset IFS
echo "$joined" # "apple,banana,cherry"
# Search in array
contains() {
local needle="$1"
shift
for item in "$@"; do
if [[ "$item" == "$needle" ]]; then
return 0
fi
done
return 1
}
if contains "banana" "${fruits[@]}"; then
echo "Found banana"
fi
File Tests and Conditionals
File Test Operators
# File existence and type
[[ -e file ]] # Exists
[[ -f file ]] # Regular file
[[ -d dir ]] # Directory
[[ -L link ]] # Symbolic link
[[ -p pipe ]] # Named pipe
[[ -S socket ]] # Socket
[[ -b device ]] # Block device
[[ -c device ]] # Character device
# File permissions
[[ -r file ]] # Readable
[[ -w file ]] # Writable
[[ -x file ]] # Executable
[[ -u file ]] # SUID bit set
[[ -g file ]] # SGID bit set
[[ -k file ]] # Sticky bit set
# File properties
[[ -s file ]] # Size > 0
[[ -O file ]] # Owned by current user
[[ -G file ]] # Owned by current group
# File comparison
[[ file1 -nt file2 ]] # file1 newer than file2
[[ file1 -ot file2 ]] # file1 older than file2
[[ file1 -ef file2 ]] # Same inode (hard links)
Practical File Test Examples
# Check before operations
if [[ -f "$config_file" && -r "$config_file" ]]; then
source "$config_file"
else
echo "Cannot read config file" >&2
exit 1
fi
# Create directory if missing
if [[ ! -d "$output_dir" ]]; then
mkdir -p "$output_dir"
fi
# Check for required commands
for cmd in git docker kubectl; do
if ! command -v "$cmd" &> /dev/null; then
echo "Required command not found: $cmd" >&2
exit 1
fi
done
# Process only newer files
for file in *.txt; do
if [[ "$file" -nt "$last_processed" ]]; then
process_file "$file"
fi
done
Numeric Comparisons
# Integer comparison
[[ $a -eq $b ]] # Equal
[[ $a -ne $b ]] # Not equal
[[ $a -lt $b ]] # Less than
[[ $a -le $b ]] # Less than or equal
[[ $a -gt $b ]] # Greater than
[[ $a -ge $b ]] # Greater than or equal
# Arithmetic context (preferred for numbers)
if (( count > 10 )); then
echo "Count exceeds threshold"
fi
if (( value >= min && value <= max )); then
echo "Value in range"
fi
Command Substitution and Pipelines
flowchart LR
A[Command 1] -->|stdout| B[Command 2]
B -->|stdout| C[Command 3]
C -->|stdout| D[Output]
subgraph Pipeline
A
B
C
end
Command Substitution
# Modern syntax (preferred)
current_date=$(date +%Y-%m-%d)
file_count=$(ls -1 | wc -l)
# Nested substitution
backup_name="backup-$(hostname)-$(date +%Y%m%d).tar.gz"
# Capture multi-line output
files=$(find /var/log -name "*.log" -mtime -1)
# Use in conditionals
if [[ $(whoami) != "root" ]]; then
echo "This script requires root privileges"
exit 1
fi
# Arithmetic with command output
file_size=$(stat -c %s file.txt)
if (( file_size > 1000000 )); then
echo "Large file: $file_size bytes"
fi
Pipelines
# Basic pipeline
cat file.txt | grep "error" | sort | uniq -c
# Process substitution
diff <(sort file1.txt) <(sort file2.txt)
# Tee to split output
command | tee output.log | grep "pattern"
# Pipeline with while
find . -name "*.sh" | while read -r script; do
echo "Checking $script"
shellcheck "$script"
done
# xargs for parallel processing
find . -name "*.log" -print0 | xargs -0 -P4 gzip
# PIPESTATUS for exit codes
false | true | false
echo "${PIPESTATUS[@]}" # "1 0 1"
Here Documents and Strings
# Here document
cat << EOF
This is a multi-line
string with variable expansion: $HOME
EOF
# Here document without expansion
cat << 'EOF'
This text is literal: $HOME
EOF
# Here string
grep "pattern" <<< "$variable"
# Indented here document
cat <<- EOF
Indented content
Tabs are stripped
EOF
Redirection
# Output redirection
command > file.txt # Overwrite
command >> file.txt # Append
command 2> error.log # Stderr only
command &> all.log # Both stdout and stderr
command > out.log 2>&1 # Both (alternative)
# Input redirection
command < input.txt
# Discard output
command > /dev/null 2>&1
command &> /dev/null
# Multiple redirections
command > stdout.log 2> stderr.log
# File descriptors
exec 3> custom.log # Open FD 3 for writing
echo "Message" >&3 # Write to FD 3
exec 3>&- # Close FD 3
Exit Codes and Error Handling
Exit Codes
# Exit with specific code
exit 0 # Success
exit 1 # General error
exit 2 # Misuse of shell command
# Check last command's exit code
command
if [[ $? -eq 0 ]]; then
echo "Command succeeded"
else
echo "Command failed with code $?"
fi
# Chain based on exit code
command1 && command2 # Run command2 only if command1 succeeds
command1 || command2 # Run command2 only if command1 fails
command1 && command2 || command3
Error Handling Patterns
#!/bin/bash
# Strict mode
set -euo pipefail
# set -e: Exit on error
# set -u: Error on undefined variables
# set -o pipefail: Pipeline fails if any command fails
# Trap for cleanup
cleanup() {
echo "Cleaning up..."
rm -f "$temp_file"
}
trap cleanup EXIT
trap cleanup ERR
# Error function
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
# Usage
[[ -f "$config" ]] || die "Config file not found: $config"
# Try-catch pattern
{
risky_command
} || {
echo "Command failed, trying alternative..."
alternative_command
}
# Check command exists
require_command() {
command -v "$1" &> /dev/null || die "Required command not found: $1"
}
require_command docker
require_command kubectl
Trap Signals
# Trap multiple signals
trap 'echo "Interrupted"; exit 1' INT TERM
# Trap with cleanup
temp_dir=$(mktemp -d)
trap 'rm -rf "$temp_dir"' EXIT
# Common signals
# EXIT - Script exit (any reason)
# ERR - Command error (with set -e)
# INT - Ctrl+C
# TERM - Termination signal
# HUP - Hangup
# DEBUG - Before each command
# Ignore signal
trap '' INT # Ignore Ctrl+C
# Reset to default
trap - INT # Reset INT handler
Common Utilities
grep - Pattern Matching
# Basic search
grep "pattern" file.txt
# Common options
grep -i "pattern" file # Case insensitive
grep -r "pattern" directory # Recursive
grep -n "pattern" file # Show line numbers
grep -c "pattern" file # Count matches
grep -l "pattern" *.txt # List matching files
grep -L "pattern" *.txt # List non-matching files
grep -v "pattern" file # Invert match
grep -w "word" file # Match whole word
grep -A 3 "pattern" file # Show 3 lines after
grep -B 3 "pattern" file # Show 3 lines before
grep -C 3 "pattern" file # Show 3 lines context
# Extended regex
grep -E "pattern1|pattern2" file
grep -E "^start.*end$" file
# Multiple patterns
grep -e "pat1" -e "pat2" file
grep -f patterns.txt file # Patterns from file
sed - Stream Editor
# Substitute first occurrence
sed 's/old/new/' file
# Substitute all occurrences
sed 's/old/new/g' file
# Edit in place
sed -i 's/old/new/g' file
sed -i.bak 's/old/new/g' file # With backup
# Delete lines
sed '/pattern/d' file # Delete matching lines
sed '5d' file # Delete line 5
sed '5,10d' file # Delete lines 5-10
# Print specific lines
sed -n '5p' file # Print line 5
sed -n '5,10p' file # Print lines 5-10
sed -n '/pattern/p' file # Print matching lines
# Multiple operations
sed -e 's/a/b/' -e 's/c/d/' file
sed 's/a/b/; s/c/d/' file
# Address ranges
sed '/start/,/end/s/old/new/g' file
# Insert and append
sed '5i\New line before' file # Insert before line 5
sed '5a\New line after' file # Append after line 5
awk - Text Processing
# Print columns
awk '{print $1, $3}' file # Print 1st and 3rd columns
awk -F: '{print $1}' /etc/passwd # Custom delimiter
# Pattern matching
awk '/pattern/ {print}' file
awk '$3 > 100 {print $1, $3}' file
# Built-in variables
awk '{print NR, NF, $0}' file # Line number, field count, whole line
awk 'END {print NR}' file # Total line count
# Calculations
awk '{sum += $1} END {print sum}' file
awk '{sum += $1; count++} END {print sum/count}' file
# Field operations
awk -F: 'BEGIN {OFS=","} {print $1, $3}' /etc/passwd
# Conditionals
awk '{if ($3 > 50) print $1 " is large"}' file
# Format output
awk '{printf "%-20s %10d\n", $1, $2}' file
cut, sort, uniq
# cut - extract columns
cut -d: -f1 /etc/passwd # Field 1, delimiter :
cut -c1-10 file # Characters 1-10
cut -d, -f2,4 file.csv # Multiple fields
# sort
sort file # Alphabetical
sort -n file # Numeric
sort -r file # Reverse
sort -k2 file # By 2nd field
sort -t: -k3 -n /etc/passwd # Custom delimiter
sort -u file # Unique only
sort -h file # Human numeric (1K, 2M)
# uniq (requires sorted input)
sort file | uniq # Remove duplicates
sort file | uniq -c # Count occurrences
sort file | uniq -d # Show only duplicates
sort file | uniq -u # Show only unique
# Common combinations
cut -d: -f7 /etc/passwd | sort | uniq -c | sort -rn
awk '{print $1}' log | sort | uniq -c | sort -rn | head -10
Other Essential Utilities
# tr - translate characters
echo "hello" | tr 'a-z' 'A-Z' # Uppercase
echo "hello" | tr -d 'aeiou' # Delete characters
echo "hello" | tr -s ' ' # Squeeze repeats
# wc - word count
wc -l file # Line count
wc -w file # Word count
wc -c file # Byte count
# head and tail
head -n 20 file # First 20 lines
tail -n 20 file # Last 20 lines
tail -f file # Follow file updates
# tee - split output
command | tee output.txt # Display and save
command | tee -a output.txt # Append
# xargs - build commands
find . -name "*.tmp" | xargs rm
echo "a b c" | xargs -n1 echo # One arg per command
find . -print0 | xargs -0 cmd # Handle spaces
Debugging
Debug Options
#!/bin/bash
# Enable debugging
set -x # Print commands before execution (xtrace)
set -v # Print shell input lines (verbose)
# Disable debugging
set +x
set +v
# Debug specific section
set -x
problematic_code
set +x
# Enable via shebang
#!/bin/bash -x
# Enable via command line
bash -x script.sh
Strict Mode
#!/bin/bash
set -euo pipefail
# set -e: Exit immediately on error
# set -u: Treat unset variables as error
# set -o pipefail: Pipeline returns rightmost non-zero exit code
# Handle unset with defaults
echo "${undefined_var:-default}"
# Disable for specific commands
set +e
potentially_failing_command
result=$?
set -e
Debugging Techniques
# Print debug messages
debug() {
if [[ "${DEBUG:-0}" == "1" ]]; then
echo "DEBUG: $*" >&2
fi
}
# Usage
DEBUG=1 ./script.sh
# Trace function calls
PS4='+ ${BASH_SOURCE}:${LINENO}:${FUNCNAME[0]}: '
set -x
# Log to file
exec 2> debug.log # Redirect stderr
set -x
# Inspect variables
declare -p variable # Print variable declaration
echo "Array: ${array[*]}"
echo "Array length: ${#array[@]}"
# Interactive debugging
read -p "Press enter to continue..."
# Trap ERR for debugging
trap 'echo "Error at line $LINENO"' ERR
Common Debugging Patterns
# Validate script syntax
bash -n script.sh
# Check with shellcheck
shellcheck script.sh
# Step through script
trap 'read -p "[$BASH_SOURCE:$LINENO] $BASH_COMMAND?"' DEBUG
# Log all commands
log_cmd() {
echo "$(date '+%Y-%m-%d %H:%M:%S') $*" >> command.log
"$@"
}
log_cmd mkdir -p /tmp/test
log_cmd cp file.txt /tmp/test/
Interactive History
History expansion belongs to the interactive shell. None of it works in a script, where history is off: bash -c 'echo !!' prints !! verbatim.
Re-running and reusing
# Repeat the previous command
!!
# Repeat it as root, having forgotten the sudo
sudo !!
# Arguments of the previous command
!$ # last argument (Alt-. inserts it without expanding)
!^ # first argument
!* # all arguments
# Earlier commands
!ls # last command starting with 'ls'
!?error? # last command containing 'error'
!-3 # three commands back
!42 # command 42, as numbered by: history
# Repeat the previous command with one substitution
^600^644^ # first '600' in it becomes '644'
!?string? and ^old^new^ may drop their closing delimiter only at the end of the
line. Anything after it — a comment, a pipe — is swallowed as part of the search or
the replacement, so terminate them whenever the line continues.
Ctrl-R searches backwards through the history as you type; Ctrl-G abandons the search and leaves the line as it was.
Check the expansion before it runs
sudo !! runs whatever came last, as root, and the expansion happens as you press Return. histverify puts the expanded command on the prompt instead, so it runs only once you have read it and pressed Return a second time.
shopt -s histverify # in ~/.bashrc
$ echo verify me
verify me
$ !!
$ echo verify me # expansion lands on the prompt; Return runs it
Each expansion goes into the history as the command it expanded to, so a following !! repeats that, not the line you typed.
Quick Reference
Essential Patterns
| Pattern | Description |
|---|---|
${var:-default} |
Use default if unset |
${var:=default} |
Assign default if unset |
${#var} |
String/array length |
${var%pattern} |
Remove suffix |
${var#pattern} |
Remove prefix |
${var/old/new} |
Replace first match |
${var//old/new} |
Replace all matches |
[[ -f file ]] |
File exists |
[[ -d dir ]] |
Directory exists |
[[ -z "$var" ]] |
String is empty |
[[ -n "$var" ]] |
String is not empty |
$(command) |
Command substitution |
$((expr)) |
Arithmetic evaluation |
"${array[@]}" |
All array elements |
Script Template
#!/bin/bash
set -euo pipefail
# Script description
# Usage: script.sh [options] <arguments>
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
# Defaults
VERBOSE=0
OUTPUT_DIR="./output"
# Functions
usage() {
cat << EOF
Usage: $SCRIPT_NAME [options] <input>
Options:
-h, --help Show this help
-v, --verbose Verbose output
-o, --output Output directory
EOF
exit "${1:-0}"
}
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
cleanup() {
# Cleanup code here
:
}
main() {
trap cleanup EXIT
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
;;
-v|--verbose)
VERBOSE=1
shift
;;
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
*)
break
;;
esac
done
# Validate inputs
[[ $# -ge 1 ]] || usage 1
# Main logic here
echo "Processing: $1"
}
main "$@"
Common Issues and Solutions
Issue: Word Splitting and Glob Expansion
Problem: Variables containing spaces or special characters break commands.
# Wrong - breaks on spaces
for file in $(find . -name "*.txt"); do
rm $file
done
# Correct - use null delimiter
find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do
rm "$file"
done
# Or with proper quoting
while IFS= read -r file; do
rm "$file"
done < <(find . -name "*.txt")
Issue: Unset Variables Cause Silent Failures
Problem: Scripts continue with empty variables.
# Enable strict mode
set -u
# Or use parameter expansion with error
: "${REQUIRED_VAR:?Variable not set}"
# Check critical variables
[[ -n "${API_KEY:-}" ]] || die "API_KEY not set"
Issue: Exit Code Lost in Pipelines
Problem: Only the last command's exit code is captured.
# Enable pipefail
set -o pipefail
# Or capture all exit codes
command1 | command2 | command3
echo "Exit codes: ${PIPESTATUS[*]}"
Issue: Subshell Variable Scope
Problem: Variables modified in subshells don't persist.
# Wrong - count stays 0
count=0
cat file | while read line; do
((count++))
done
echo "$count" # 0
# Correct - use process substitution
count=0
while read line; do
((count++))
done < <(cat file)
echo "$count" # Correct value
Issue: Comparing Numbers vs Strings
Problem: Using wrong operators for comparison type.
# String comparison (use == and !=)
if [[ "$str" == "value" ]]; then
echo "Match"
fi
# Numeric comparison (use -eq, -lt, -gt, etc.)
if [[ $num -eq 42 ]]; then
echo "Match"
fi
# Or use arithmetic context for numbers
if (( num == 42 )); then
echo "Match"
fi
Issue: Here Document Indentation
Problem: Here documents include unwanted whitespace.
# Use <<- with tabs (not spaces)
cat <<- EOF
Line 1
Line 2
EOF
# Or dedent manually
cat << EOF | sed 's/^ //'
Line 1
Line 2
EOF
Issue: ! in a Double-Quoted String
Problem: An interactive shell reads ! followed by a word as a history expansion, so the line fails before it runs. Scripts are unaffected.
echo "hi!there" # bash: !there: event not found
echo 'hi!there' # fine - single quotes suppress expansion
echo "it works!" # fine - a trailing ! is not an event
echo "hi\!there" # prints hi\!there - inside double quotes the backslash survives
set +H # or turn expansion off for this shell
Related Topics
To expand your shell scripting and command-line skills, explore these related topics:
- Linux CLI - Essential commands, file operations, process management, and system utilities
- zsh - Advanced shell with powerful completion, themes, and plugins
- tmux - Terminal multiplexer for session management and persistent shells
- vim - Modal text editor commonly used for script editing
- Git - Version control for tracking script changes and collaboration
- sed and awk - Advanced text processing for complex transformations