1.7 Shell Scripting Basics

Turn repeated terminal work into reliable Bash scripts

After access control, shell scripting gives you a way to inspect systems, process logs, and automate the small tasks that defenders repeat every day. This page starts with the basics, then builds toward conditions, loops, functions, and safer scripting habits.

1. What is Shell Scripting?

A shell script is a text file containing a sequence of commands that the shell (command-line interpreter) can execute. Instead of typing commands one by one, you write them in a file and run them all at once.

Why Learn Shell Scripting?

  • Automation: Repeat tasks without manual effort
  • Efficiency: Run multiple commands with one script
  • System Administration: Essential for managing Linux servers
  • DevOps: Foundation for CI/CD pipelines and deployments
  • Cybersecurity: Create tools, automate scans, process logs
Context: Most Linux systems use Bash (Bourne Again SHell) as the default shell. It's what this guide focuses on.

2. Getting Started

Your First Script

Let's create a simple "Hello World" script:

Step 1: Create the file

# Create a new file
nano hello.sh

Step 2: Write the script

#!/bin/bash
# My first shell script

echo "Hello, World!"
echo "Welcome to shell scripting!"

Step 3: Make it executable

# Add execute permission
chmod +x hello.sh

# Run the script
./hello.sh
The Shebang Line: The #!/bin/bash at the top (called a "shebang" or "hashbang") tells the system which interpreter to use. Always include it.

Script File Conventions

  • Use .sh extension (e.g., myscript.sh)
  • Always start with the shebang line
  • Add comments using #
  • Use descriptive names for your scripts

3. Variables & Scopes

Variables store data that your script can use. Think of them as labeled containers for information.

Declaring Variables

#!/bin/bash

# Declaring variables (No spaces around =)
name="lokii"
age=25
course="Shell Scripting"

# Using variables (prefix with $)
echo "Name: $name"
echo "Age: $age"
echo "Course: $course"
Important: No spaces around the = sign. name="value" works; name = "value" fails.

Variable Types

# Strings
greeting="Hello there!"

# Numbers
count=42

# Command output (command substitution)
current_date=$(date)
user=$(whoami)

echo "Today is: $current_date"
echo "Logged in as: $user"

Variable Scopes

Understanding variable scope is crucial for writing maintainable scripts:

Global Variables (Default)

#!/bin/bash

# Global variable - accessible everywhere in the script
GLOBAL_VAR="I'm global."

my_function() {
    echo "Inside function: $GLOBAL_VAR"
    GLOBAL_VAR="Modified inside function"  # Modifies the global.
}

my_function
echo "After function: $GLOBAL_VAR"
# Output: Modified inside function

Local Variables (with 'local' keyword)

#!/bin/bash

name="Global Alice"

greet() {
    local name="Local Bob"  # Only exists inside this function
    echo "Inside: $name"       # Prints: Local Bob
}

greet
echo "Outside: $name"  # Prints: Global Alice, unchanged

Environment Variables

#!/bin/bash

# Export makes variable available to child processes
export MY_API_KEY="secret123"

# Common environment variables
echo "Home: $HOME"
echo "User: $USER"
echo "Path: $PATH"
echo "Shell: $SHELL"
echo "PWD: $PWD"
Best Practice: Use local for function variables to avoid accidentally modifying global state. Use UPPERCASE for environment/global constants, lowercase for local variables.

Special Variables

# Script arguments
$0  # Script name
$1  # First argument
$2  # Second argument
$#  # Number of arguments
$@  # All arguments (as separate words)
$*  # All arguments (as single string)
$?  # Exit status of last command
$$  # Current process ID
$!  # PID of last background process

Example: Using Arguments

#!/bin/bash
# greet.sh - A greeting script

echo "Script name: $0"
echo "Hello, $1!"
echo "You passed $# arguments"

# Run with: ./greet.sh Alice

Variable Best Practices

  • Always quote variables: Use "$var" to prevent word splitting
  • Use braces for clarity: ${variable} especially in strings
  • Initialize variables: Set defaults with ${var:-default}
  • Check if set: Use ${var:?error message} to exit if unset
# Variable defaults and checks
name="${1:-Guest}"      # Use "Guest" if $1 is empty
file="${2:?File required}"  # Exit with error if $2 is empty
echo "Hello ${name}! Processing ${file}..."

4. User Input

Use the read command to get input from users:

#!/bin/bash

# Basic input
echo "What is your name?"
read username
echo "Hello, $username!"

# Input with prompt (-p flag)
read -p "Enter your age: " age
echo "You are $age years old"

# Silent input for passwords (-s flag)
read -sp "Enter password: " password
echo  # New line after hidden input
echo "Password received!"

# Input with timeout (-t flag)
read -t 5 -p "Quick! Enter something (5 sec): " quick_input
Tip: Combine flags. read -sp creates a silent prompt (great for passwords).

5. Conditionals (if/else) & Modern Syntax

Make decisions in your scripts using if statements:

Basic Syntax

#!/bin/bash

age=18

if [ $age -ge 18 ]; then
    echo "You are an adult"
else
    echo "You are a minor"
fi

Modern [[ ]] vs Traditional [ ]

Modern bash provides [[ ]] - a more powerful and safer alternative:

Key Differences:
- [[ ]] is a bash keyword (not a command like [ ])
- No word splitting - no need to quote variables inside
- Supports &&, || directly inside
- Pattern matching with == and regex with =~
- Safer: won't break with empty variables
#!/bin/bash

name="Alice"
empty_var=""

# Traditional [ ] - quote variables.
if [ "$empty_var" = "" ]; then
    echo "Empty (traditional)"
fi

# Modern [[ ]] - No quoting needed, safer
if [[ $empty_var == "" ]]; then
    echo "Empty (modern)"
fi

# Logical operators inside [[ ]]
if [[ $name == "Alice" && $age -ge 18 ]]; then
    echo "Adult Alice!"
fi

# Pattern matching with wildcards
if [[ $name == A* ]]; then
    echo "Name starts with A"
fi

# Regex matching with =~
email="[email protected]"
if [[ $email =~ ^[a-zA-Z0-9]+@[a-zA-Z]+\.[a-z]+$ ]]; then
    echo "Valid email format"
fi
Best Practice: Use [[ ]] for all string and pattern tests in bash. Use [ ] only when POSIX compatibility is required (e.g., /bin/sh scripts).

if / elif / else

#!/bin/bash

read -p "Enter your score: " score

if [[ $score -ge 90 ]]; then
    echo "Grade: A"
elif [[ $score -ge 80 ]]; then
    echo "Grade: B"
elif [[ $score -ge 70 ]]; then
    echo "Grade: C"
else
    echo "Grade: F"
fi

Comparison Operators

Numeric Comparisons

# Works in both [ ] and [[ ]]
[ $a -eq $b ]  # Equal
[ $a -ne $b ]  # Not equal
[ $a -gt $b ]  # Greater than
[ $a -lt $b ]  # Less than
[ $a -ge $b ]  # Greater or equal
[ $a -le $b ]  # Less or equal

# Modern alternative for numbers: (( ))
if (( a > b )); then echo "a is greater"; fi
if (( a >= 10 && a <= 20 )); then echo "10-20 range"; fi

String Comparisons

# In [[ ]] - preferred
[[ $str1 == $str2 ]]   # Equal (use == in [[ ]])
[[ $str1 != $str2 ]]   # Not equal
[[ $str1 < $str2 ]]    # Alphabetically less
[[ $str1 > $str2 ]]    # Alphabetically greater
[[ -z $str ]]         # String is empty
[[ -n $str ]]         # String is not empty

# In [ ] - use = and quote strings
[ "$str1" = "$str2" ]

File Tests

# Works in both [ ] and [[ ]]
[[ -e "filename" ]]   # File exists
[[ -f "filename" ]]   # Is a regular file
[[ -d "dirname" ]]    # Is a directory
[[ -r "filename" ]]   # File is readable
[[ -w "filename" ]]   # File is writable
[[ -x "filename" ]]   # File is executable
[[ -s "filename" ]]   # File is not empty
[[ -L "filename" ]]   # Is a symbolic link
[[ $f1 -nt $f2 ]]     # f1 is newer than f2
[[ $f1 -ot $f2 ]]     # f1 is older than f2

Logical Operators

# Inside [[ ]] - cleaner syntax
if [[ $a -gt 5 && $b -lt 10 ]]; then echo "Both true"; fi
if [[ $a -gt 5 || $b -lt 10 ]]; then echo "One is true"; fi
if [[ ! -f $file ]]; then echo "Not a file"; fi

# With [ ] - use -a, -o, or external &&, ||
if [ $a -gt 5 ] && [ $b -lt 10 ]; then echo "OK"; fi

Example: Modern File Check Script

#!/bin/bash

read -p "Enter filename: " filename

if [[ -e $filename ]]; then
    if [[ -d $filename ]]; then
        echo "$filename is a directory"
    elif [[ -f $filename ]]; then
        echo "$filename is a regular file"
        [[ -r $filename ]] && echo "  - Readable"
        [[ -w $filename ]] && echo "  - Writable"
        [[ -x $filename ]] && echo "  - Executable"
    fi
else
    echo "$filename does not exist"
fi

6. Case Statements

The case statement is cleaner than multiple if-elif chains for pattern matching:

Basic Syntax

#!/bin/bash

read -p "Enter a fruit: " fruit

case $fruit in
    apple)
        echo "Red and crunchy!"
        ;;
    banana)
        echo "Yellow and sweet!"
        ;;
    orange|lemon)
        echo "Citrus fruit!"
        ;;
    *)
        echo "Unknown fruit"
        ;;
esac

Pattern Matching in Case

#!/bin/bash

read -p "Enter a filename: " file

case $file in
    *.txt)
        echo "Text file"
        ;;
    *.sh)
        echo "Shell script"
        ;;
    *.jpg|*.png|*.gif)
        echo "Image file"
        ;;
    [0-9]*)
        echo "Starts with a number"
        ;;
    *)
        echo "Other file type"
        ;;
esac

Menu Example

#!/bin/bash

echo "=== System Menu ==="
echo "1) Show disk usage"
echo "2) Show memory usage"
echo "3) Show uptime"
echo "4) Exit"

read -p "Choose option: " choice

case $choice in
    1) df -h ;;
    2) free -h ;;
    3) uptime ;;
    4) echo "Goodbye!"; exit 0 ;;
    *) echo "Invalid option"; exit 1 ;;
esac
Tip: Use ;;& instead of ;; to fall through to the next case (bash 4+), or ;& to fall through and execute the next case's commands.

7. Loops & Nested Loops

Loops let you repeat commands multiple times.

For Loop

#!/bin/bash

# Loop through a list
for fruit in apple banana orange; do
    echo "I like $fruit"
done

# Loop through numbers (brace expansion)
for i in {1..5}; do
    echo "Count: $i"
done

# With step: {start..end..step}
for i in {0..10..2}; do
    echo "Even: $i"  # 0, 2, 4, 6, 8, 10
done

# Loop through files
for file in *.txt; do
    echo "Processing: $file"
done

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

While Loop

#!/bin/bash

count=1

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

# Reading file line by line (safe method)
while IFS= read -r line; do
    echo "Line: $line"
done < myfile.txt

# Infinite loop with break condition
while true; do
    read -p "Enter 'quit' to exit: " input
    [[ $input == "quit" ]] && break
done

Until Loop

#!/bin/bash

count=1

# Runs UNTIL condition becomes true
until [[ $count -gt 5 ]]; do
    echo "Count: $count"
    ((count++))
done

Nested Loops

Loops inside loops - useful for 2D operations, matrices, and combinations:

#!/bin/bash

# Multiplication table
for i in {1..5}; do
    for j in {1..5}; do
        result=$((i * j))
        printf "%4d" $result
    done
    echo  # New line
done

# Nested with different iterable types
for dir in /home/*; do
    [[ -d $dir ]] || continue
    echo "Directory: $dir"
    for file in "$dir"/*.txt; do
        [[ -f $file ]] && echo "  File: $file"
    done
done

Loop Control

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

# continue - skip to next iteration
for i in {1..5}; do
    [[ $i -eq 3 ]] && continue
    echo "$i"  # Prints 1,2,4,5 (skips 3)
done

# break N - break out of N levels of nested loops
for i in {1..3}; do
    for j in {1..3}; do
        [[ $i -eq 2 && $j -eq 2 ]] && break 2  # Exit both loops
        echo "i=$i, j=$j"
    done
done
Loop Best Practices:
- Use while IFS= read -r for reading files to preserve whitespace
- Quote variables inside loops: "$file" not $file
- Check if glob matches files: [[ -f $file ]] before processing
- Use (( )) for arithmetic in loop conditions

8. Functions & Scope

Functions let you organize code into reusable blocks.

Defining Functions

#!/bin/bash

# Method 1: Using function keyword (bash-specific)
function greet() {
    echo "Hello, World!"
}

# Method 2: POSIX-compatible (recommended)
say_goodbye() {
    echo "Goodbye!"
}

# Calling functions
greet
say_goodbye

Functions with Parameters

#!/bin/bash

greet_user() {
    echo "Hello, $1!"
    echo "You are $2 years old"
    echo "All args: $@"
    echo "Arg count: $#"
}

# Call with arguments
greet_user "Alice" 25
greet_user "Bob" 30

Function Scope

Understanding variable scope inside functions is important:

#!/bin/bash

global_var="I'm global"

demo_scope() {
    local local_var="I'm local"    # Only exists in this function
    global_var="Modified"         # Modifies the global
    new_var="Created in func"    # Creates a new global

    echo "Inside: local_var=$local_var"
}

demo_scope
echo "Outside: global_var=$global_var"   # Modified
echo "Outside: new_var=$new_var"         # Accessible
echo "Outside: local_var=$local_var"     # Empty
Important: Always use local for function variables to avoid accidentally creating/modifying globals. Without local, variables leak to global scope.

Nested Functions

#!/bin/bash

outer_function() {
    local outer_var="outer"

    # Define inner function
    inner_function() {
        local inner_var="inner"
        echo "Inner: outer_var=$outer_var, inner_var=$inner_var"
    }

    inner_function
    echo "Outer: outer_var=$outer_var"
}

outer_function
# inner_function  # Error: only exists inside outer_function

Return Values

#!/bin/bash

# Functions return exit codes (0-255)
is_even() {
    (( $1 % 2 == 0 )) && return 0 || return 1
}

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

# For string output, use echo and capture
get_greeting() {
    echo "Hello, $1!"
}

message=$(get_greeting "Alice")
echo "$message"

# Return multiple values via global or array
get_user_info() {
    _name="$1"
    _age="$2"
}

get_user_info "Alice" 25
echo "Name: $_name, Age: $_age"
Function Best Practices:
- Always use local for variables inside functions
- Use POSIX syntax func() { } for portability
- Document function parameters with comments
- Return 0 for success, non-zero for errors
- Use command substitution $(func) to capture output

9. Arrays

Arrays store multiple values in a single variable.

#!/bin/bash

# Declaring arrays
fruits=("apple" "banana" "orange" "grape")

# Accessing elements (0-indexed)
echo "First fruit: ${fruits[0]}"
echo "Third fruit: ${fruits[2]}"

# All elements
echo "All fruits: ${fruits[@]}"

# Array length
echo "Total: ${#fruits[@]}"

# Add element
fruits+=("mango")

# Loop through array
for fruit in "${fruits[@]}"; do
    echo "Fruit: $fruit"
done

# Loop with index
for i in "${!fruits[@]}"; do
    echo "Index $i: ${fruits[$i]}"
done

10. String Manipulation

Bash provides powerful built-in string manipulation features:

String Length & Substring

#!/bin/bash

str="Hello, World!"

# String length
echo "Length: ${#str}"  # 13

# Substring: ${string:start:length}
echo "${str:0:5}"   # Hello
echo "${str:7}"     # World! (from position 7)
echo "${str: -6}"   # World! (last 6 chars, note space)

Search & Replace

str="hello_world_hello"

# Replace first occurrence
echo "${str/hello/hi}"   # hi_world_hello

# Replace all occurrences
echo "${str//hello/hi}"  # hi_world_hi

# Delete pattern (replace with nothing)
echo "${str//_/}"        # helloworldhello

Pattern Removal

path="/home/user/docs/file.txt"

# Remove from beginning (shortest #, longest ##)
echo "${path#*/}"    # home/user/docs/file.txt
echo "${path##*/}"   # file.txt (basename)

# Remove from end (shortest %, longest %%)
echo "${path%/*}"    # /home/user/docs (dirname)
echo "${path%%/*}"   # (empty, removes longest)

# Get extension
file="document.tar.gz"
echo "${file##*.}"   # gz

Case Conversion (Bash 4+)

str="Hello World"

# Lowercase
echo "${str,,}"   # hello world
echo "${str,}"    # hello World (first char)

# Uppercase
echo "${str^^}"   # HELLO WORLD
echo "${str^}"    # Hello World (first char)
Tip: These parameter expansions are much faster than calling external tools like sed, awk, or tr for simple string operations.

11. Arithmetic Operations

Bash supports integer arithmetic with various methods:

Arithmetic Expansion $(( ))

#!/bin/bash

a=10
b=3

echo "Add: $((a + b))"      # 13
echo "Subtract: $((a - b))" # 7
echo "Multiply: $((a * b))" # 30
echo "Divide: $((a / b))"   # 3 (integer division)
echo "Modulo: $((a % b))"   # 1
echo "Power: $((a ** 2))"   # 100

# Compound assignments
((a++))     # Increment
((a--))     # Decrement
((a += 5))  # Add and assign
((a *= 2))  # Multiply and assign

let Command

let "result = 5 + 3"
let "result++"
echo "$result"  # 9

expr Command (POSIX)

# Older method, but POSIX-compatible
result=$(expr 5 + 3)
result=$(expr $a \* $b)  # Must escape *

Floating Point with bc

# Bash only does integers. Use bc for decimals.
result=$(echo "scale=2; 10 / 3" | bc)
echo "$result"  # 3.33

# More complex calculations
pi=$(echo "scale=10; 4*a(1)" | bc -l)  # Pi approximation
Note: Bash arithmetic only works with integers. For floating-point math, use bc, awk, or Python.

12. Exit Codes

Every command returns an exit code. 0 means success, anything else indicates an error.

#!/bin/bash

# Check last command's exit code
ls /some/directory
echo "Exit code: $?"

# Exit with custom code
if [ ! -f "config.txt" ]; then
    echo "Error: config.txt not found."
    exit 1
fi

echo "Config loaded successfully."
exit 0
Common Exit Codes:
- 0 - Success
- 1 - General error
- 2 - Misuse of command
- 126 - Permission denied
- 127 - Command not found
- 130 - Script terminated by Ctrl+C

13. Practice Tests

Test your knowledge with these exercises. Click "Show Answer" to check your response.

1. What does the shebang line "#!/bin/bash" do?
  1. Starts a comment
  2. Tells the system which interpreter to use
  3. Declares a variable
  4. Imports a library
Answer: B
The shebang (#!/bin/bash) tells the operating system which interpreter should execute the script. Without it, the system might not know how to run your script.
2. Which is the correct way to declare a variable in bash?
  1. name = "John"
  2. $name="John"
  3. name="John"
  4. var name = "John"
Answer: C
In bash, there must be No spaces around the = sign. The $ is only used when accessing the variable, not declaring it.
3. What operator checks if a number is greater than another in bash?
  1. >
  2. -gt
  3. greater
  4. ==
Answer: B
In bash's test brackets [ ], use -gt for "greater than", -lt for "less than", -eq for "equal", etc.
4. Write a script that prints numbers 1 to 5 using a for loop.
Answer:
#!/bin/bash
for i in {1..5}; do
    echo "$i"
done
Or using C-style:
#!/bin/bash
for ((i=1; i<=5; i++)); do
    echo "$i"
done
5. What does $? return?
  1. The current process ID
  2. The number of arguments
  3. The exit status of the last command
  4. A random number
Answer: C
$? contains the exit status of the last executed command. 0 means success, non-zero indicates an error.
6. Write a function that takes a name as an argument and prints "Hello, [name]!"
Answer:
#!/bin/bash

greet() {
    echo "Hello, $1!"
}

# Usage
greet "Alice"  # Outputs: Hello, Alice!
7. How do you read silent input (like a password) from user?
  1. read -p password
  2. read -s password
  3. read --hidden password
  4. read -silent password
Answer: B
The -s flag makes the input silent (invisible). You can combine it with -p for a prompt: read -sp "Password: " pass
8. Write a script that checks if a file exists and prints whether it's a file or directory.
Answer:
#!/bin/bash

read -p "Enter path: " path

if [ -e "$path" ]; then
    if [ -f "$path" ]; then
        echo "$path is a file"
    elif [ -d "$path" ]; then
        echo "$path is a directory"
    fi
else
    echo "$path does not exist"
fi
9. What's the difference between a while loop and an until loop?
Answer:
- while: Runs while the condition is true
- until: Runs until the condition becomes true (runs while false)

They are essentially opposites of each other.
10. Create a script that counts from 1 to 10, but skips the number 5.
Answer:
#!/bin/bash

for i in {1..10}; do
    if [ $i -eq 5 ]; then
        continue
    fi
    echo "$i"
done
The continue statement skips the rest of the loop body and jumps to the next iteration.
Practice complete. You've completed the Shell Scripting Basics quiz! Keep practicing by writing your own scripts. The best way to learn is by doing.