- Introduction to Shell Scripting
- Variables
- User Input
- Operators and Arithmetic
- Conditional Statements
- Loops
- Functions
- Arrays
- String Manipulation
- File Operations
- Command Line Arguments
- Exit Codes and Error Handling
- Best Practices
- Practical Examples
A shell script is a text file containing a series of commands that the shell executes sequentially. It automates repetitive tasks and combines multiple commands.
#!/bin/bash
# This is a comment
# Script: hello.sh
echo "Hello, World!"
echo "Today is $(date)"
echo "Current user: $USER"# Create the script
vim hello.sh
# Make it executable
chmod +x hello.sh
# Run the script
./hello.sh
# or
bash hello.sh#!/bin/bash # Use bash
#!/bin/sh # Use sh (POSIX)
#!/usr/bin/env bash # Find bash in PATH (portable)
#!/usr/bin/env python3 # For Python scripts# With execute permission
./script.sh
# Without execute permission
bash script.sh
sh script.sh
# Source (run in current shell)
source script.sh
. script.sh# Assign variable (NO spaces around =)
name="John"
age=25
file_path="/var/log/messages"
# Use variable (with $)
echo $name
echo ${name} # Preferred for clarity
echo "Hello, $name"
echo "Hello, ${name}!"
# Readonly variable
readonly PI=3.14159
PI=3.14 # Error: readonly variable
# Unset variable
unset name# Valid names
myVar="value"
my_var="value"
myVar2="value"
_myVar="value"
# Invalid names
2myVar="value" # Cannot start with number
my-var="value" # No hyphens
my var="value" # No spacesname="John Doe"
# Double quotes - variable expansion
echo "Hello, $name" # Hello, John Doe
# Single quotes - literal string
echo 'Hello, $name' # Hello, $name
# Backticks / $() - command substitution
echo "Date: $(date)"
echo "Date: `date`" # Old style, avoid
# Escape character
echo "Price: \$100" # Price: $100
echo "Tab:\tNewline:\n"$0 # Script name
$1 to $9 # Positional parameters
${10} # 10th parameter and beyond
$# # Number of arguments
$@ # All arguments (as separate words)
$* # All arguments (as single word)
$$ # Current process ID
$? # Exit status of last command
$! # PID of last background process
$_ # Last argument of previous command# Common environment variables
echo $HOME # User home directory
echo $USER # Current username
echo $PATH # Executable search path
echo $PWD # Current directory
echo $SHELL # Current shell
echo $HOSTNAME # System hostname
echo $RANDOM # Random number (0-32767)
echo $LINENO # Current line number
# Export variable (available to child processes)
export MY_VAR="value"
# List all environment variables
env
printenvname=""
# Default value (if unset or null)
echo ${name:-"default"} # Use default, don't change name
echo ${name:="default"} # Use default, set name to default
echo ${name:+"alternate"} # Use alternate if name is set
echo ${name:?"error msg"} # Display error if unset
# String length
str="Hello"
echo ${#str} # 5
# Substring
str="Hello World"
echo ${str:0:5} # Hello (start:length)
echo ${str:6} # World (from position 6)
echo ${str: -5} # World (last 5, note space)
# Pattern removal
file="/path/to/file.txt"
echo ${file#*/} # path/to/file.txt (remove first match)
echo ${file##*/} # file.txt (remove longest match)
echo ${file%/*} # /path/to (remove from end, first match)
echo ${file%%/*} # (empty - remove longest from end)
# Pattern replacement
str="hello world world"
echo ${str/world/there} # hello there world (first)
echo ${str//world/there} # hello there there (all)
echo ${str/#hello/hi} # hi world world (beginning)
echo ${str/%world/earth} # hello world earth (end)
# Case conversion (Bash 4+)
str="Hello World"
echo ${str,,} # hello world (lowercase)
echo ${str^^} # HELLO WORLD (uppercase)
echo ${str^} # Hello World (first char upper)# Basic input
echo "Enter your name:"
read name
echo "Hello, $name!"
# Prompt on same line
read -p "Enter your name: " name
# Silent input (passwords)
read -sp "Enter password: " password
echo
# Timeout
read -t 5 -p "Enter within 5 seconds: " answer
# Default value
read -p "Enter name [John]: " name
name=${name:-John}
# Read multiple values
read -p "Enter first and last name: " first last
echo "First: $first, Last: $last"
# Read into array
read -a colors -p "Enter colors: "
echo "First color: ${colors[0]}"
# Read specific number of characters
read -n 1 -p "Press any key..."
echo
# Read from file
while read line; do
echo "$line"
done < file.txt#!/bin/bash
PS3="Choose an option: " # Custom prompt
select option in "Start" "Stop" "Restart" "Quit"; do
case $option in
Start)
echo "Starting..."
;;
Stop)
echo "Stopping..."
;;
Restart)
echo "Restarting..."
;;
Quit)
echo "Goodbye!"
break
;;
*)
echo "Invalid option"
;;
esac
done# Using $(( ))
a=10
b=3
echo $((a + b)) # 13 (addition)
echo $((a - b)) # 7 (subtraction)
echo $((a * b)) # 30 (multiplication)
echo $((a / b)) # 3 (integer division)
echo $((a % b)) # 1 (modulus)
echo $((a ** b)) # 1000 (exponentiation)
# Assignment
((a++)) # Increment
((a--)) # Decrement
((a += 5)) # Add and assign
((a -= 5)) # Subtract and assign
((a *= 2)) # Multiply and assign
((a /= 2)) # Divide and assign
# Using let
let "result = a + b"
let a++
# Using expr (old style)
result=$(expr $a + $b)
result=$(expr $a \* $b) # Must escape *
# Using bc for floating point
echo "scale=2; 10/3" | bc # 3.33
result=$(echo "scale=2; 10/3" | bc)# Inside [[ ]] or [ ]
-eq # Equal
-ne # Not equal
-gt # Greater than
-ge # Greater than or equal
-lt # Less than
-le # Less than or equal
# Examples
if [[ $a -eq $b ]]; then
echo "Equal"
fi
if [[ $a -gt 5 ]]; then
echo "Greater than 5"
fi
# Inside (( )) - C-style
if (( a == b )); then echo "Equal"; fi
if (( a > 5 )); then echo "Greater"; fi
if (( a >= b )); then echo "Greater or equal"; fi
if (( a != b )); then echo "Not equal"; fi= # Equal (in [ ])
== # Equal (in [[ ]], preferred)
!= # Not equal
< # Less than (alphabetically)
> # Greater than
-z # String is empty
-n # String is not empty
# Examples
if [[ "$str1" == "$str2" ]]; then
echo "Strings are equal"
fi
if [[ -z "$str" ]]; then
echo "String is empty"
fi
if [[ -n "$str" ]]; then
echo "String is not empty"
fi
# Pattern matching (in [[ ]])
if [[ "$name" == J* ]]; then
echo "Starts with J"
fi
# Regex matching
if [[ "$email" =~ ^[a-z]+@[a-z]+\.[a-z]+$ ]]; then
echo "Valid email format"
fi-e file # File exists
-f file # Is regular file
-d file # Is directory
-L file # Is symbolic link
-r file # Is readable
-w file # Is writable
-x file # Is executable
-s file # File size > 0
-O file # Owned by current user
-G file # Owned by current group
file1 -nt file2 # file1 newer than file2
file1 -ot file2 # file1 older than file2
# Examples
if [[ -e "$file" ]]; then
echo "File exists"
fi
if [[ -d "$dir" ]]; then
echo "Is a directory"
fi
if [[ -f "$file" && -r "$file" ]]; then
echo "File exists and is readable"
fi# Inside [[ ]]
&& # AND
|| # OR
! # NOT
# Inside [ ]
-a # AND
-o # OR
! # NOT
# Examples
if [[ $a -gt 5 && $a -lt 10 ]]; then
echo "Between 5 and 10"
fi
if [[ $a -eq 1 || $a -eq 2 ]]; then
echo "One or Two"
fi
if [[ ! -f "$file" ]]; then
echo "File does not exist"
fi# Basic if
if [[ condition ]]; then
commands
fi
# if-else
if [[ condition ]]; then
commands
else
other_commands
fi
# if-elif-else
if [[ condition1 ]]; then
commands1
elif [[ condition2 ]]; then
commands2
elif [[ condition3 ]]; then
commands3
else
default_commands
fi
# Practical example
#!/bin/bash
read -p "Enter a number: " num
if [[ $num -gt 0 ]]; then
echo "Positive"
elif [[ $num -lt 0 ]]; then
echo "Negative"
else
echo "Zero"
ficase $variable in
pattern1)
commands1
;;
pattern2|pattern3)
commands2
;;
*)
default_commands
;;
esac
# Practical example
#!/bin/bash
read -p "Enter a fruit: " fruit
case $fruit in
apple|Apple)
echo "It's an apple"
;;
banana)
echo "It's a banana"
;;
[Oo]range)
echo "It's an orange"
;;
*)
echo "Unknown fruit"
;;
esac# Using && and ||
[[ $a -gt $b ]] && echo "a is greater" || echo "b is greater or equal"
# Using arithmetic
max=$(( a > b ? a : b ))# Basic for loop
for i in 1 2 3 4 5; do
echo "Number: $i"
done
# Range
for i in {1..10}; do
echo "Number: $i"
done
# Range with step
for i in {0..100..10}; do
echo "Number: $i"
done
# C-style for loop
for ((i=0; i<10; i++)); do
echo "Number: $i"
done
# Loop through files
for file in *.txt; do
echo "File: $file"
done
for file in /var/log/*; do
echo "Processing: $file"
done
# Loop through command output
for user in $(cat /etc/passwd | cut -d: -f1); do
echo "User: $user"
done
# Loop through array
colors=(red green blue)
for color in "${colors[@]}"; do
echo "Color: $color"
done
# Loop with index
for i in "${!colors[@]}"; do
echo "Index $i: ${colors[$i]}"
done# Basic while
count=1
while [[ $count -le 5 ]]; do
echo "Count: $count"
((count++))
done
# Infinite loop
while true; do
echo "Press Ctrl+C to stop"
sleep 1
done
# Read file line by line
while IFS= read -r line; do
echo "$line"
done < file.txt
# Process with while
while read -r user shell; do
echo "User: $user, Shell: $shell"
done < <(cat /etc/passwd | cut -d: -f1,7 | tr ':' ' ')
# While with command
while ping -c 1 google.com &>/dev/null; do
echo "Network is up"
sleep 5
done
echo "Network is down"# Until (opposite of while)
count=1
until [[ $count -gt 5 ]]; do
echo "Count: $count"
((count++))
done
# Wait for condition
until ping -c 1 server.example.com &>/dev/null; do
echo "Waiting for server..."
sleep 2
done
echo "Server is up!"# break - exit loop
for i in {1..10}; do
if [[ $i -eq 5 ]]; then
break
fi
echo $i
done
# continue - skip iteration
for i in {1..10}; do
if [[ $i -eq 5 ]]; then
continue
fi
echo $i
done
# break from nested loops
for i in {1..3}; do
for j in {1..3}; do
if [[ $j -eq 2 ]]; then
break 2 # Break out of 2 levels
fi
echo "$i, $j"
done
done# Style 1
function my_function {
echo "Hello from function"
}
# Style 2 (POSIX compatible)
my_function() {
echo "Hello from function"
}
# Calling function
my_functiongreet() {
echo "Hello, $1!" # First argument
echo "You are $2 years old" # Second argument
echo "All args: $@"
echo "Number of args: $#"
}
greet "John" 25# Return status (0-255)
is_even() {
if (( $1 % 2 == 0 )); then
return 0 # Success/true
else
return 1 # Failure/false
fi
}
if is_even 4; then
echo "4 is even"
fi
# Return string via echo
get_greeting() {
echo "Hello, $1!"
}
message=$(get_greeting "World")
echo "$message"
# Return via global variable
calculate() {
RESULT=$(( $1 + $2 ))
}
calculate 5 3
echo "Result: $RESULT"my_function() {
local name="John" # Local to function
global_var="Global" # Global
echo "Inside: $name"
}
my_function
echo "Outside: $name" # Empty
echo "Global: $global_var" # Global# Factorial
factorial() {
if [[ $1 -le 1 ]]; then
echo 1
else
local prev=$(factorial $(( $1 - 1 )))
echo $(( $1 * prev ))
fi
}
echo "5! = $(factorial 5)" # 120# Declare array
declare -a my_array
my_array=(one two three)
# Alternative
colors[0]="red"
colors[1]="green"
colors[2]="blue"
# Access elements
echo ${colors[0]} # First element
echo ${colors[-1]} # Last element (Bash 4+)
# All elements
echo ${colors[@]} # As separate words
echo ${colors[*]} # As single word
# Array length
echo ${#colors[@]}
# Array indices
echo ${!colors[@]}
# Add element
colors+=(yellow)
colors+=("light blue")
# Remove element
unset colors[1]
# Slice
echo ${colors[@]:1:2} # From index 1, 2 elements
# Loop through array
for color in "${colors[@]}"; do
echo "$color"
done
# Loop with index
for i in "${!colors[@]}"; do
echo "$i: ${colors[$i]}"
done# Declare
declare -A user
# Assign
user[name]="John"
user[age]=25
user[email]="john@example.com"
# Or inline
declare -A user=([name]="John" [age]=25 [email]="john@example.com")
# Access
echo ${user[name]}
# All keys
echo ${!user[@]}
# All values
echo ${user[@]}
# Check if key exists
if [[ -v user[name] ]]; then
echo "Name exists"
fi
# Loop
for key in "${!user[@]}"; do
echo "$key: ${user[$key]}"
donestr="Hello World"
# Length
echo ${#str} # 11
# Concatenation
str1="Hello"
str2="World"
str3="$str1 $str2" # Hello World
str3="${str1}${str2}" # HelloWorld
# Substring
echo ${str:0:5} # Hello
echo ${str:6} # World
echo ${str: -5} # World (note space)
# Find and replace
echo ${str/World/Universe} # Hello Universe
echo ${str//o/0} # Hell0 W0rld (all)
# Remove pattern
filename="document.txt"
echo ${filename%.txt} # document
echo ${filename##*.} # txt
# Case conversion (Bash 4+)
echo ${str,,} # hello world
echo ${str^^} # HELLO WORLD
# Check if contains substring
if [[ "$str" == *"World"* ]]; then
echo "Contains World"
fi
# Split string
IFS=',' read -ra parts <<< "a,b,c,d"
for part in "${parts[@]}"; do
echo "$part"
done# Read entire file
content=$(cat file.txt)
content=$(<file.txt)
# Read line by line
while IFS= read -r line; do
echo "$line"
done < file.txt
# Read with line numbers
n=1
while IFS= read -r line; do
echo "$n: $line"
((n++))
done < file.txt
# Read specific line
sed -n '5p' file.txt
# Read fields
while IFS=':' read -r user pass uid gid info home shell; do
echo "User: $user, Shell: $shell"
done < /etc/passwd# Write (overwrite)
echo "content" > file.txt
# Append
echo "more content" >> file.txt
# Multiple lines
cat > file.txt << EOF
Line 1
Line 2
Line 3
EOF
# Without variable expansion
cat > file.txt << 'EOF'
$HOME stays as literal
EOF
# Write from variable
echo "$content" > file.txt
printf "%s\n" "$content" > file.txt#!/bin/bash
file="$1"
if [[ ! -e "$file" ]]; then
echo "File does not exist"
exit 1
fi
if [[ -f "$file" ]]; then
echo "Regular file"
elif [[ -d "$file" ]]; then
echo "Directory"
elif [[ -L "$file" ]]; then
echo "Symbolic link"
fi
if [[ -r "$file" ]]; then echo "Readable"; fi
if [[ -w "$file" ]]; then echo "Writable"; fi
if [[ -x "$file" ]]; then echo "Executable"; fi
if [[ -s "$file" ]]; then echo "Not empty"; fi#!/bin/bash
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "All arguments: $@"
echo "Number of arguments: $#"
# Shift arguments
while [[ $# -gt 0 ]]; do
echo "Processing: $1"
shift
done#!/bin/bash
usage() {
echo "Usage: $0 [-v] [-f file] [-n number]"
exit 1
}
verbose=false
file=""
number=0
while getopts "vf:n:h" opt; do
case $opt in
v)
verbose=true
;;
f)
file="$OPTARG"
;;
n)
number="$OPTARG"
;;
h)
usage
;;
\?)
echo "Invalid option: -$OPTARG"
usage
;;
:)
echo "Option -$OPTARG requires an argument"
usage
;;
esac
done
# Shift past processed options
shift $((OPTIND - 1))
# Remaining arguments
echo "Remaining args: $@"
echo "Verbose: $verbose"
echo "File: $file"
echo "Number: $number"#!/bin/bash
# Using getopt (external command)
OPTS=$(getopt -o vf:n:h --long verbose,file:,number:,help -n "$0" -- "$@")
eval set -- "$OPTS"
verbose=false
file=""
number=0
while true; do
case "$1" in
-v|--verbose)
verbose=true
shift
;;
-f|--file)
file="$2"
shift 2
;;
-n|--number)
number="$2"
shift 2
;;
-h|--help)
echo "Usage: $0 [options]"
exit 0
;;
--)
shift
break
;;
*)
break
;;
esac
done
echo "Verbose: $verbose"
echo "File: $file"
echo "Number: $number"# Exit with code
exit 0 # Success
exit 1 # General error
exit 2 # Misuse of command
# Check exit code
command
if [[ $? -eq 0 ]]; then
echo "Success"
else
echo "Failed with code $?"
fi
# Common exit codes
# 0 - Success
# 1 - General error
# 2 - Misuse of command
# 126 - Permission denied
# 127 - Command not found
# 128+n - Killed by signal n#!/bin/bash
# Exit on error
set -e
# Exit on undefined variable
set -u
# Exit on pipe failure
set -o pipefail
# All together (recommended)
set -euo pipefail
# Trap errors
trap 'echo "Error on line $LINENO"; exit 1' ERR
# Cleanup on exit
cleanup() {
rm -f "$temp_file"
echo "Cleaned up"
}
trap cleanup EXIT
# Error handling function
die() {
echo "ERROR: $1" >&2
exit "${2:-1}"
}
# Usage
[[ -f "$file" ]] || die "File not found: $file" 2# Run with debug
bash -x script.sh
# Enable debug in script
set -x # Enable
set +x # Disable
# Debug section
set -x
commands_to_debug
set +x
# Print commands before execution
#!/bin/bash -x
# Verbose mode
set -v # Print input lines
set +v # Disable#!/bin/bash
#
# Script: script_name.sh
# Description: What this script does
# Author: Your Name
# Date: 2026-01-28
# Version: 1.0
#
set -euo pipefail
# Constants
readonly SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
readonly SCRIPT_NAME="$(basename "$0")"
# Global variables
LOG_FILE="/var/log/${SCRIPT_NAME}.log"
# Functions
log() {
echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*" | tee -a "$LOG_FILE"
}
die() {
log "ERROR: $1" >&2
exit "${2:-1}"
}
usage() {
cat << EOF
Usage: $SCRIPT_NAME [options]
Options:
-h, --help Show this help
-v, --verbose Enable verbose mode
EOF
exit 0
}
cleanup() {
# Cleanup code
log "Cleanup complete"
}
# Main
main() {
log "Starting $SCRIPT_NAME"
# Main logic here
log "Completed successfully"
}
# Traps
trap cleanup EXIT
trap 'die "Interrupted"' INT TERM
# Parse arguments
while [[ $# -gt 0 ]]; do
case "$1" in
-h|--help)
usage
;;
-v|--verbose)
set -x
shift
;;
*)
die "Unknown option: $1"
;;
esac
done
# Run main
main "$@"# Use lowercase for variables
my_variable="value"
# Use UPPERCASE for constants/environment
readonly MAX_RETRIES=3
export PATH
# Use meaningful names
user_count=10 # Good
uc=10 # Bad
# Quote variables
echo "$variable" # Good
echo $variable # Risky
# Use [[ ]] instead of [ ]
[[ -f "$file" ]] # Preferred
[ -f "$file" ] # POSIX, less features
# Use $(command) instead of backticks
result=$(ls -la) # Preferred
result=`ls -la` # Deprecated
# Check command existence
if command -v docker &>/dev/null; then
echo "Docker is installed"
fi
# Use functions for repeated code
# Use local variables in functions
# Add comments for complex logic
# Handle errors appropriately#!/bin/bash
set -euo pipefail
SOURCE_DIR="${1:-/home}"
BACKUP_DIR="${2:-/backup}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="backup_${DATE}.tar.gz"
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $*"; }
log "Starting backup of $SOURCE_DIR"
mkdir -p "$BACKUP_DIR"
tar -czf "${BACKUP_DIR}/${BACKUP_FILE}" "$SOURCE_DIR" 2>/dev/null
# Keep only last 7 backups
cd "$BACKUP_DIR"
ls -t backup_*.tar.gz | tail -n +8 | xargs -r rm -f
log "Backup complete: ${BACKUP_FILE}"
log "Size: $(du -h "${BACKUP_DIR}/${BACKUP_FILE}" | cut -f1)"#!/bin/bash
LOG_FILE="${1:-/var/log/messages}"
echo "=== Log Analysis for $LOG_FILE ==="
echo
echo "Error count by type:"
grep -i "error\|fail\|warning" "$LOG_FILE" 2>/dev/null | \
awk '{print $5}' | sort | uniq -c | sort -rn | head -10
echo
echo "Top 10 IPs (if applicable):"
grep -oE '\b([0-9]{1,3}\.){3}[0-9]{1,3}\b' "$LOG_FILE" 2>/dev/null | \
sort | uniq -c | sort -rn | head -10
echo
echo "Events by hour:"
awk '{print $3}' "$LOG_FILE" 2>/dev/null | cut -d: -f1 | \
sort | uniq -c | sort -k2n#!/bin/bash
echo "=== System Health Check ==="
echo "Date: $(date)"
echo "Hostname: $(hostname)"
echo
echo "=== CPU Usage ==="
top -bn1 | head -5
echo
echo "=== Memory Usage ==="
free -h
echo
echo "=== Disk Usage ==="
df -h | grep -v tmpfs
echo
echo "=== Top Processes ==="
ps aux --sort=-%mem | head -6
echo
echo "=== Failed Services ==="
systemctl --failed 2>/dev/null || echo "N/A"
echo
echo "=== Recent Errors ==="
journalctl -p err --since "1 hour ago" 2>/dev/null | tail -10 || \
tail -20 /var/log/messages | grep -i error| Syntax | Description |
|---|---|
${var:-default} |
Use default if unset |
${var:=default} |
Set default if unset |
${#var} |
String length |
${var:start:len} |
Substring |
${var/old/new} |
Replace first |
${var//old/new} |
Replace all |
${var#pattern} |
Remove prefix |
${var%pattern} |
Remove suffix |
| Operator | Description |
|---|---|
-eq, -ne |
Equal, not equal (int) |
-gt, -lt |
Greater, less than |
-ge, -le |
Greater/less or equal |
==, != |
String comparison |
-z, -n |
Empty, not empty |
-e, -f, -d |
Exists, file, directory |
-r, -w, -x |
Readable, writable, executable |
# If
if [[ cond ]]; then cmd; fi
# For
for i in list; do cmd; done
for ((i=0;i<10;i++)); do cmd; done
# While
while [[ cond ]]; do cmd; done
# Case
case $var in pat) cmd;; esac
# Function
func() { commands; }← Back to Index
Previous: Storage & Filesystems
Next: SELinux & Security