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.
Table of Contents
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
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
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"
= 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"
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
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:
-
[[ ]] 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
[[ ]] 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
;;& 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
- 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
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"
- 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)
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
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
-
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.
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.
In bash, there must be No spaces around the = sign. The $ is only used when accessing the variable, not declaring it.
In bash's test brackets [ ], use -gt for "greater than", -lt for "less than", -eq for "equal", etc.
#!/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
$? contains the exit status of the last executed command. 0 means success, non-zero indicates an error.
#!/bin/bash
greet() {
echo "Hello, $1!"
}
# Usage
greet "Alice" # Outputs: Hello, Alice!
The
-s flag makes the input silent (invisible). You can combine it with -p for a prompt: read -sp "Password: " pass
#!/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
- while: Runs while the condition is true
- until: Runs until the condition becomes true (runs while false)
They are essentially opposites of each other.
#!/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.