LUNAROPS · OPERATIONAL UPLINK 100% UPTIME 1,247d POSTS 893 JEFF.MOON@LUNAROPS.DEV UTC --:--:--

Shell Scripting Fundamentals for Linux

linuxbashscriptingautomationshell

Shell scripting automates repetitive tasks and ties together Linux tools. Here’s how to write effective scripts.

Script Basics

First Script

1
2
3
#!/bin/bash
# This is a comment
echo "Hello, World!"
1
2
3
4
5
6
7
# Make executable
chmod +x script.sh

# Run
./script.sh
# or
bash script.sh

Shebang

The first line tells which interpreter to use:

1
2
3
#!/bin/bash        # Bash
#!/bin/sh          # POSIX shell
#!/usr/bin/env bash   # Find bash in PATH (portable)

Variables

Defining Variables

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# No spaces around =
name="Alice"
count=42
path="/var/log"

# Command output
date_now=$(date)
files=$(ls -la)

# Arithmetic
sum=$((5 + 3))

Using Variables

1
2
3
echo "Hello, $name"
echo "Path is ${path}/file.log"   # Braces for clarity
echo "Count: ${count}"

Special Variables

Variable Meaning
$0 Script name
$1, $2, ... Arguments
$# Number of arguments
$@ All arguments (as separate strings)
$* All arguments (as one string)
$? Exit status of last command
$$ Current process ID
$! Last background process ID
1
2
3
4
5
#!/bin/bash
echo "Script: $0"
echo "First arg: $1"
echo "All args: $@"
echo "Count: $# arguments"

Environment Variables

1
2
3
4
5
6
7
8
# Export for child processes
export MY_VAR="value"

# Common environment variables
echo $HOME
echo $USER
echo $PATH
echo $PWD

Input and Output

Reading Input

1
2
3
4
5
6
7
8
9
# Read into variable
read -p "Enter name: " name
echo "Hello, $name"

# Read with timeout
read -t 5 -p "Quick, enter something: " input

# Silent (for passwords)
read -s -p "Password: " password

Output

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
# Standard output
echo "Hello"
printf "Name: %s, Age: %d\n" "$name" "$age"

# Standard error
echo "Error!" >&2

# Redirect to file
echo "Log entry" >> logfile.txt

# Here document
cat << EOF
This is a
multi-line
text block
EOF

Conditionals

if Statements

1
2
3
4
5
6
7
if [ condition ]; then
    commands
elif [ condition ]; then
    commands
else
    commands
fi

Test Operators

String Comparisons:

1
2
3
4
[ "$a" = "$b" ]    # Equal
[ "$a" != "$b" ]   # Not equal
[ -z "$a" ]        # Empty
[ -n "$a" ]        # Not empty

Numeric Comparisons:

1
2
3
4
5
6
[ "$a" -eq "$b" ]  # Equal
[ "$a" -ne "$b" ]  # Not equal
[ "$a" -lt "$b" ]  # Less than
[ "$a" -le "$b" ]  # Less or equal
[ "$a" -gt "$b" ]  # Greater than
[ "$a" -ge "$b" ]  # Greater or equal

File Tests:

1
2
3
4
5
6
7
[ -e "$file" ]     # Exists
[ -f "$file" ]     # Is regular file
[ -d "$file" ]     # Is directory
[ -r "$file" ]     # Is readable
[ -w "$file" ]     # Is writable
[ -x "$file" ]     # Is executable
[ -s "$file" ]     # Size > 0

Logical Operators:

1
2
3
[ condition1 ] && [ condition2 ]   # AND
[ condition1 ] || [ condition2 ]   # OR
[ ! condition ]                     # NOT

Modern Syntax [[ ]]

1
2
3
4
5
6
7
8
# Preferred in bash - more features, fewer surprises
if [[ "$string" == *pattern* ]]; then
    echo "Pattern match"
fi

if [[ "$a" -gt 5 && "$b" -lt 10 ]]; then
    echo "Both conditions"
fi

Case Statement

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
case "$option" in
    start)
        echo "Starting..."
        ;;
    stop)
        echo "Stopping..."
        ;;
    restart)
        echo "Restarting..."
        ;;
    *)
        echo "Unknown option"
        ;;
esac

Loops

for Loop

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# List
for item in apple banana cherry; do
    echo "$item"
done

# Range
for i in {1..5}; do
    echo "Number: $i"
done

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

# Files
for file in *.txt; do
    echo "Processing: $file"
done

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

while Loop

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
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 < file.txt

until Loop

1
2
3
4
5
count=0
until [ $count -ge 5 ]; do
    echo "Count: $count"
    ((count++))
done

Loop Control

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# Skip iteration
for i in {1..10}; do
    [ $i -eq 5 ] && continue
    echo $i
done

# Exit loop
for i in {1..10}; do
    [ $i -eq 5 ] && break
    echo $i
done

Functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
# Define function
greet() {
    local name="$1"    # Local variable
    echo "Hello, $name!"
    return 0           # Return status
}

# Call function
greet "Alice"

# Capture output
result=$(greet "Bob")
echo "$result"

# Check return status
if greet "Charlie"; then
    echo "Success"
fi

Error Handling

Exit Status

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
# Exit with status
exit 0    # Success
exit 1    # Error

# Check last command
if [ $? -ne 0 ]; then
    echo "Command failed"
fi

# Chain commands
command1 && command2    # Run command2 only if command1 succeeds
command1 || command2    # Run command2 only if command1 fails

Set Options

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash
set -e          # Exit on error
set -u          # Error on undefined variables
set -o pipefail # Pipeline fails if any command fails

# Combined
set -euo pipefail

# Debug mode
set -x          # Print commands before execution

Trap

1
2
3
4
5
6
7
8
9
# Cleanup on exit
cleanup() {
    rm -f /tmp/tempfile
    echo "Cleaned up"
}
trap cleanup EXIT

# Handle signals
trap 'echo "Interrupted!"; exit 1' INT TERM

Practical Examples

Backup Script

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
#!/bin/bash
set -euo pipefail

SOURCE="/home/user/documents"
DEST="/backup"
DATE=$(date +%Y%m%d_%H%M%S)
BACKUP_FILE="$DEST/backup_$DATE.tar.gz"

echo "Starting backup..."
tar -czf "$BACKUP_FILE" "$SOURCE"
echo "Backup created: $BACKUP_FILE"

# Remove backups older than 7 days
find "$DEST" -name "backup_*.tar.gz" -mtime +7 -delete
echo "Old backups cleaned"

Log Monitoring

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#!/bin/bash
LOG_FILE="/var/log/app.log"
PATTERN="ERROR"

tail -f "$LOG_FILE" | while read line; do
    if [[ "$line" == *"$PATTERN"* ]]; then
        echo "ALERT: $line"
        # Send notification
    fi
done

Argument Parsing

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#!/bin/bash

usage() {
    echo "Usage: $0 [-v] [-o output] input_file"
    exit 1
}

verbose=false
output=""

while getopts "vo:" opt; do
    case $opt in
        v) verbose=true ;;
        o) output="$OPTARG" ;;
        *) usage ;;
    esac
done

shift $((OPTIND-1))

if [ $# -eq 0 ]; then
    usage
fi

input_file="$1"

$verbose && echo "Processing: $input_file"

Service Health Check

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
#!/bin/bash

services=("nginx" "postgresql" "redis")

for service in "${services[@]}"; do
    if systemctl is-active --quiet "$service"; then
        echo "[OK] $service is running"
    else
        echo "[FAIL] $service is NOT running"
        systemctl start "$service"
    fi
done

Best Practices

  1. Always quote variables: "$variable"
  2. Use set -euo pipefail for safety
  3. Check command existence: command -v cmd >/dev/null
  4. Use functions for reusable code
  5. Add comments for complex logic
  6. Use shellcheck: shellcheck script.sh
  7. Handle errors gracefully
  8. Use meaningful variable names

Quick Reference

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
# Variables
var="value"
echo "$var"

# Conditionals
if [[ condition ]]; then
    commands
fi

# Loops
for item in list; do
    commands
done

# Functions
func() {
    local var="$1"
    echo "$var"
}

# Safety
set -euo pipefail

Shell scripting is the glue of Linux administration. Master it to automate everything.

Comments