Shell Script Introduction

beginner | 60 min read | 2025.12.02

What You’ll Learn in This Tutorial

  • Basic structure of shell scripts
  • Using variables
  • Conditional branching (if statements)
  • Loop processing (for, while)
  • Practical automation scripts

What is Shell Script? Why Learn It?

The Birth of Shell Scripts

The history of shell scripts dates back to 1971 with the Thompson shell (sh). In 1979, Stephen Bourne developed the Bourne Shell (sh), which became the foundation of modern shell scripting.

In 1989, Brian Fox developed Bash (Bourne Again Shell) as part of the GNU project. Bash is currently the most widely used shell.

“Shell scripts are the easiest way to automate repetitive tasks” - Ancient Unix wisdom

Why Learn Shell Scripts

  1. Automation: Automate repetitive tasks and save time
  2. System Administration: Essential for server management, deployment, backups
  3. CI/CD: Heavily used in GitHub Actions, Jenkins, etc.
  4. Portability: Works on almost all Unix-like systems

Bash and Other Shells

ShellCharacteristicsMain Uses
bashMost widespread. POSIX compatible + extensionsGeneral scripts
shPOSIX standard. Minimal featuresPortability-focused scripts
zshbash compatible + powerful completionInteractive shell
fishUser-friendlyInteractive shell
dashLightweight, fastSystem scripts

Best Practice: Use #!/bin/sh for portability, use #!/bin/bash when using Bash-specific features.

Your First Shell Script

Let’s start with “Hello World”.

hello.sh

#!/bin/bash

# This is my first shell script
echo "Hello, World!"

How to Run

# Grant execute permission
chmod +x hello.sh

# Execute
./hello.sh

# Or, explicitly specify bash
bash hello.sh

About Shebang

The first line #!/bin/bash is called the shebang, specifying which interpreter to use for this script.

#!/bin/bash     # Execute with bash
#!/bin/sh       # Execute with POSIX sh (more portable)
#!/usr/bin/env bash  # Find bash in PATH (recommended)

Official Documentation: GNU Bash Manual

Using Variables

Basic Variables

#!/bin/bash

# Variable definition (no spaces around =!)
name="John"
age=25

# Variable reference
echo "Name: $name"
echo "Age: ${age} years old"

# Assign command output to variable
current_date=$(date +%Y-%m-%d)
echo "Today's date: $current_date"

# Old style (backticks) - not recommended
old_style=`date +%Y-%m-%d`

Common Mistake: Spaces

# Wrong (spaces cause errors)
name = "John"    # command not found: name
name= "John"     # Runs "John" as command with empty variable

# Correct
name="John"

Variable Scope

#!/bin/bash

# Global variable
global_var="I am global"

function example() {
    # Local variable (only valid within function)
    local local_var="I am local"
    echo "$local_var"
    echo "$global_var"
}

example
echo "$local_var"   # Empty (not visible outside function)

Special Variables

#!/bin/bash

echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Number of arguments: $#"
echo "All arguments: $@"
echo "All arguments (string): $*"
echo "Exit status of last command: $?"
echo "Current process ID: $$"
echo "Background process PID: $!"

Execution example:

$ ./script.sh arg1 arg2 arg3
Script name: ./script.sh
First argument: arg1
Second argument: arg2
Number of arguments: 3
All arguments: arg1 arg2 arg3

Environment Variables

# Reference environment variables
echo "Home directory: $HOME"
echo "Username: $USER"
echo "Current directory: $PWD"
echo "Shell: $SHELL"
echo "Path: $PATH"

# Set environment variable (inherited by subprocesses)
export MY_VAR="some value"

# Temporarily set environment variable for command
DEBUG=true ./my_script.sh

Quote Differences

name="World"

# Double quotes: variables are expanded
echo "Hello, $name"    # Hello, World

# Single quotes: output as-is
echo 'Hello, $name'    # Hello, $name

# Escape with backslash
echo "Hello, \$name"   # Hello, $name

Conditional Branching (if statements)

Basic Syntax

#!/bin/bash

age=20

if [ $age -ge 20 ]; then
    echo "Adult"
elif [ $age -ge 13 ]; then
    echo "Teenager"
else
    echo "Child"
fi

Types of Test Syntax

# [ ] - Classic test syntax (POSIX compatible)
if [ $a -eq $b ]; then

# [[ ]] - Bash extended syntax (recommended)
if [[ $a == $b ]]; then

# (( )) - Arithmetic evaluation
if (( a > b )); then

Best Practice: Use [[ ]] when using Bash. It supports pattern matching and regular expressions.

Numeric Comparison Operators

OperatorMeaningExample
-eqequal[ $a -eq $b ]
-nenot equal[ $a -ne $b ]
-ltless than[ $a -lt $b ]
-leless or equal[ $a -le $b ]
-gtgreater than[ $a -gt $b ]
-gegreater or equal[ $a -ge $b ]

String Comparison

#!/bin/bash

str="hello"

# String comparison (quoting is important!)
if [ "$str" = "hello" ]; then
    echo "Strings match"
fi

# Check for empty string
if [ -z "$str" ]; then
    echo "String is empty"
fi

# Check if not empty
if [ -n "$str" ]; then
    echo "String is not empty"
fi

# Pattern matching with [[ ]]
if [[ "$str" == h* ]]; then
    echo "Starts with h"
fi

# Regex matching (Bash 3.0+)
if [[ "$str" =~ ^[a-z]+$ ]]; then
    echo "Lowercase only"
fi

File/Directory Tests

#!/bin/bash

# File existence check
if [ -f "config.txt" ]; then
    echo "config.txt exists"
fi

# Directory existence check
if [ -d "logs" ]; then
    echo "logs directory exists"
fi

# File or directory exists
if [ -e "path" ]; then
    echo "path exists"
fi

# Readable
if [ -r "file.txt" ]; then
    echo "Readable"
fi

# Writable
if [ -w "file.txt" ]; then
    echo "Writable"
fi

# Executable
if [ -x "script.sh" ]; then
    echo "Executable"
fi

# File is not empty
if [ -s "file.txt" ]; then
    echo "File is not empty"
fi

Logical Operators

# AND
if [ $a -gt 0 ] && [ $a -lt 10 ]; then
    echo "0 < a < 10"
fi

# OR
if [ $a -eq 0 ] || [ $a -eq 1 ]; then
    echo "a is 0 or 1"
fi

# NOT
if [ ! -f "file.txt" ]; then
    echo "file.txt does not exist"
fi

# [[ ]] allows && and || inside
if [[ $a -gt 0 && $a -lt 10 ]]; then
    echo "0 < a < 10"
fi

case Statement

case statements are convenient for multiple conditions:

#!/bin/bash

fruit="apple"

case $fruit in
    apple)
        echo "It's an apple"
        ;;
    banana|orange)
        echo "It's a banana or orange"
        ;;
    *)
        echo "Unknown fruit"
        ;;
esac

Loop Processing

for Loop

#!/bin/bash

# Process list sequentially
for fruit in apple banana orange; do
    echo "Fruit: $fruit"
done

# Numeric range (Bash extension)
for i in {1..5}; do
    echo "Count: $i"
done

# With step
for i in {0..10..2}; do
    echo "Even: $i"
done

# C-style for
for ((i=0; i<5; i++)); do
    echo "Index: $i"
done

# Process files
for file in *.txt; do
    echo "File: $file"
done

# Process command output
for user in $(cat users.txt); do
    echo "User: $user"
done

while Loop

#!/bin/bash

count=1
while [ $count -le 5 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

# Read file line by line (recommended pattern)
while IFS= read -r line; do
    echo "Line: $line"
done < input.txt

# Infinite loop
while true; do
    echo "Press Ctrl+C to stop"
    sleep 1
done

until Loop

#!/bin/bash

count=1
until [ $count -gt 5 ]; do
    echo "Count: $count"
    count=$((count + 1))
done

Loop Control

# break to exit loop
for i in {1..10}; do
    if [ $i -eq 5 ]; then
        break
    fi
    echo $i
done

# continue to next iteration
for i in {1..5}; do
    if [ $i -eq 3 ]; then
        continue
    fi
    echo $i
done

Function Definition

#!/bin/bash

# Function definition (two ways)
function greet() {
    local name=$1
    echo "Hello, ${name}!"
}

# Or
greet2() {
    echo "Hello, $1!"
}

# Function calls
greet "John"
greet2 "World"

# Return values (exit status)
is_even() {
    if (( $1 % 2 == 0 )); then
        return 0  # Success (even)
    else
        return 1  # Failure (odd)
    fi
}

if is_even 4; then
    echo "4 is even"
fi

# Use echo to return values
add() {
    echo $(( $1 + $2 ))
}

result=$(add 3 5)
echo "3 + 5 = $result"

Practical: Backup Script

Let’s create a practical backup script using what we’ve learned.

backup.sh

#!/bin/bash
#
# Automatic backup script
# Usage: ./backup.sh [source_dir] [backup_dir]
#

set -euo pipefail  # Exit immediately on error

# Configuration
SOURCE_DIR="${1:-./src}"
BACKUP_DIR="${2:-./backups}"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_NAME="backup_$DATE.tar.gz"
RETENTION_DAYS=7

# Log function
log() {
    echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"
}

# Check/create backup directory
if [ ! -d "$BACKUP_DIR" ]; then
    log "Creating backup directory..."
    mkdir -p "$BACKUP_DIR"
fi

# Check source directory
if [ ! -d "$SOURCE_DIR" ]; then
    log "Error: $SOURCE_DIR not found"
    exit 1
fi

# Execute backup
log "Starting backup: $SOURCE_DIR -> $BACKUP_DIR/$BACKUP_NAME"
tar -czf "$BACKUP_DIR/$BACKUP_NAME" "$SOURCE_DIR"

if [ $? -eq 0 ]; then
    log "Complete: $BACKUP_DIR/$BACKUP_NAME"
    log "Size: $(du -h "$BACKUP_DIR/$BACKUP_NAME" | cut -f1)"
else
    log "Error: Backup failed"
    exit 1
fi

# Delete old backups
log "Deleting old backups (${RETENTION_DAYS}+ days old)..."
deleted=$(find "$BACKUP_DIR" -name "backup_*.tar.gz" -mtime +$RETENTION_DAYS -delete -print | wc -l)
log "Files deleted: $deleted"

log "All done!"

Script Best Practices

  1. Use set -euo pipefail

    • -e: Exit immediately on error
    • -u: Error on undefined variables
    • -o pipefail: Detect errors in pipes
  2. Always quote variables: "$variable"

  3. Use functions: Improve reusability and readability

  4. Include log output: For debugging and monitoring

Error Handling

#!/bin/bash

# Define error handling with trap
cleanup() {
    echo "Running cleanup..."
    # Delete temporary files, etc.
}

trap cleanup EXIT  # Execute on script exit
trap 'echo "Error occurred: line $LINENO"' ERR

# To ignore errors
command_that_might_fail || true

# Alternative processing on error
config_file="config.txt"
if [ -f "$config_file" ]; then
    source "$config_file"
else
    echo "Config file not found. Using defaults."
    default_value="fallback"
fi

Debugging Tips

# Run entire script in debug mode
bash -x script.sh

# Enable debugging within script
set -x  # Start debugging
# ... code to debug ...
set +x  # End debugging

# Exit immediately on error (recommended)
set -e

# Error on undefined variables (recommended)
set -u

# Detect pipe errors (recommended)
set -o pipefail

# Common combination
set -euo pipefail

Static Analysis with ShellCheck

ShellCheck is a static analysis tool that detects problems in shell scripts.

# Installation
# macOS
brew install shellcheck

# Ubuntu/Debian
sudo apt install shellcheck

# Usage
shellcheck script.sh

Example problems ShellCheck detects:

# Warning: Missing variable quotes
echo $USER  # SC2086: Double quote to prevent globbing

# Fixed
echo "$USER"

ShellCheck usage is also recommended in the Google Shell Style Guide.

Common Patterns

Argument Validation

#!/bin/bash

if [ $# -lt 2 ]; then
    echo "Usage: $0 <source> <destination>"
    exit 1
fi

source=$1
destination=$2

Setting Default Values

# Use default if variable is unset or empty
name="${1:-World}"
echo "Hello, $name"

# Use default only if variable is unset
name="${1-World}"

Safe Temporary File Creation

#!/bin/bash

# Use mktemp (recommended)
temp_file=$(mktemp)
temp_dir=$(mktemp -d)

# Delete on exit
trap "rm -rf $temp_file $temp_dir" EXIT

echo "Temp file: $temp_file"
echo "Temp directory: $temp_dir"

Getting User Input

#!/bin/bash

# Read input
read -p "Enter your name: " name
echo "Hello, $name"

# Password input (hidden)
read -sp "Password: " password
echo ""

# With timeout
read -t 5 -p "Enter within 5 seconds: " input || echo "Timeout"

Next Steps

After mastering shell script basics, proceed to the next steps:

  • Regular expressions with grep, sed, awk
  • Scheduled execution with cron jobs
  • More advanced automation (Ansible, Makefile, etc.)

Official Documentation

Style Guides & Best Practices

Learning Resources

Cheat Sheets

← Back to list