Arrays, Special Variables, and Arithmetic

Summary: in this tutorial, you will learn learn bash arrays (indexed and associative), special shell variables, and integer arithmetic for scripting.

Arrays, Special Variables, and Arithmetic

Beyond simple string variables, Bash provides arrays for storing collections of data, special built-in variables that give you information about the script and its environment, and arithmetic capabilities for calculations. These features are essential building blocks for more sophisticated scripts.

Arrays

Bash supports two types of arrays: indexed arrays (numbered, like most languages) and associative arrays (key-value pairs, like dictionaries/maps).

Indexed Arrays

# Create an array
fruits=("apple" "banana" "cherry" "date" "elderberry")
 
# Create empty array
empty_array=()
 
# Add elements one by one
fruits[0]="apple"
fruits[1]="banana"
fruits[2]="cherry"
 
# Access elements (0-indexed)
echo ${fruits[0]}                    # apple
echo ${fruits[2]}                    # cherry
echo ${fruits[4]}                    # elderberry
 
# Access all elements
echo ${fruits[@]}                    # apple banana cherry date elderberry
echo ${fruits[*]}                    # apple banana cherry date elderberry
 
# Number of elements
echo ${#fruits[@]}                   # 5
 
# Length of a specific element
echo ${#fruits[0]}                   # 5 (length of "apple")
echo ${#fruits[2]}                   # 6 (length of "cherry")
 
# Append elements
fruits+=("fig")
fruits+=("grape" "honeydew")
echo ${#fruits[@]}                   # 8
 
# Modify an element
fruits[1]="blueberry"
echo ${fruits[1]}                    # blueberry
 
# Delete an element
unset fruits[3]                      # Removes "date"
echo ${fruits[@]}                    # apple blueberry cherry elderberry fig grape honeydew
# Note: index 3 is now empty, but indices don't shift
 
# Get all indices
echo ${!fruits[@]}                   # 0 1 2 4 5 6 7 (3 is missing!)
 
# Slice an array
echo ${fruits[@]:1:3}                # 3 elements starting at index 1
echo ${fruits[@]:2}                  # All elements from index 2 onward
 
# Copy an array
fruits_copy=("${fruits[@]}")
 

Looping Over Arrays

fruits=("apple" "banana" "cherry")
 
# Loop over values
for fruit in "${fruits[@]}"; do
    echo "Fruit: $fruit"
done
 
# Loop over indices
for i in "${!fruits[@]}"; do
    echo "Index $i: ${fruits[$i]}"
done
 
# Loop with counter
count=0
for fruit in "${fruits[@]}"; do
    echo "$count: $fruit"
    ((count++))
done
 
# C-style for loop
for ((i=0; i<${#fruits[@]}; i++)); do
    echo "fruits[$i] = ${fruits[$i]}"
done
 

🚫 Always quote array expansions!

Critical: Use "${array[@]}" (with double quotes) when iterating or passing arrays to functions. Without quotes, elements containing spaces will be split into multiple items:

files=("my document.txt" "your file.pdf" "data.csv")
 
# WRONG: Elements with spaces are split
for f in ${files[@]}; do
    echo "File: $f"
done
# Output:
# File: my
# File: document.txt
# File: your
# File: file.pdf
# File: data.csv
 
# RIGHT: Elements preserved correctly
for f in "${files[@]}"; do
    echo "File: $f"
done
# Output:
# File: my document.txt
# File: your file.pdf
# File: data.csv
 

Rule: "${array[@]}" is almost always what you want. Only use ${array[@]} unquoted if you specifically need word splitting.

Array Operations

# Sort an array
fruits=("banana" "apple" "cherry")
IFS=$'\n' sorted=($(sort <<<"${fruits[*]}"))
echo "${sorted[@]}"                  # apple banana cherry
 
# Remove duplicates
items=("a" "b" "a" "c" "b")
unique=($(printf "%s\n" "${items[@]}" | sort -u))
echo "${unique[@]}"                  # a b c
 
# Check if element exists
if [[ " ${fruits[@]} " =~ " apple " ]]; then
    echo "Apple found!"
fi
 
# Find index of element
for i in "${!fruits[@]}"; do
    if [[ "${fruits[$i]}" == "cherry" ]]; then
        echo "cherry is at index $i"
    fi
done
 
# Concatenate arrays
arr1=("a" "b")
arr2=("c" "d")
combined=("${arr1[@]}" "${arr2[@]}")
echo "${combined[@]}"                # a b c d
 
# Reverse an array
reverse() {
    local arr=("$@")
    local reversed=()
    for ((i=${#arr[@]}-1; i>=0; i--)); do
        reversed+=("${arr[$i]}")
    done
    echo "${reversed[@]}"
}
fruits=("apple" "banana" "cherry")
reversed=($(reverse "${fruits[@]}"))
echo "${reversed[@]}"                # cherry banana apple
 

Associative Arrays (Bash 4+)

Associative arrays use strings as keys instead of numbers:

# Declare an associative array (required!)
declare -A user
 
# Set key-value pairs
user[name]="Alice"
user[age]="30"
user[email]="alice@example.com"
user[city]="New York"
 
# Access values
echo ${user[name]}                   # Alice
echo ${user[email]}                  # alice@example.com
 
# Get all keys
echo ${!user[@]}                     # name age email city (order not guaranteed)
 
# Get all values
echo ${user[@]}                      # Alice 30 alice@example.com New York
 
# Number of elements
echo ${#user[@]}                     # 4
 
# Check if key exists
if [[ -v user[name] ]]; then
    echo "name key exists"
fi
 
# Delete a key
unset user[city]
echo ${#user[@]}                     # 3
 
# Create with initial values
declare -A colors=(
    [red]="#FF0000"
    [green]="#00FF00"
    [blue]="#0000FF"
    [yellow]="#FFFF00"
)
 
# Loop over keys and values
for key in "${!user[@]}"; do
    echo "$key: ${user[$key]}"
done
 

Practical use cases:

# Configuration settings
declare -A config=(
    [host]="localhost"
    [port]="5432"
    [database]="myapp"
    [username]="admin"
)
 
# Count occurrences
declare -A word_count
while read -r word; do
    ((word_count[$word]++))
done < file.txt
 
# Map file extensions to types
declare -A mime_types=(
    [txt]="text/plain"
    [html]="text/html"
    [json]="application/json"
    [pdf]="application/pdf"
)
file="document.pdf"
ext="${file##*.}"
echo "MIME type: ${mime_types[$ext]}"
 
# Cache results
declare -A cache
get_data() {
    local key=$1
    if [[ -v cache[$key] ]]; then
        echo "${cache[$key]}"
    else
        # Expensive operation
        local result=$(curl -s "https://api.example.com/$key")
        cache[$key]=$result
        echo "$result"
    fi
}
 

Special Variables

Bash provides several special read-only variables:

VariableDescriptionExample
$0Script name (or shell)./myscript.sh or bash
$1-$9Positional parameters (arguments 1-9)arg1, arg2, ...
${10}, ${11}, ...Arguments 10+ (braces required)arg10, arg11, ...
$#Number of arguments3
$@All arguments (as separate words)"$1" "$2" "$3"
$*All arguments (as one word)"$1 $2 $3"
$?Exit status of last command (0=success)0, 1, 127
$$PID of current shell12345
$!PID of last background process12346
$_Last argument of previous commandfile.txt
$-Current shell option flagshimBH
$PPIDParent process ID12344
$BASHPIDPID of current Bash process12345
$BASH_VERSIONBash version5.2.15(1)-release
$LINENOCurrent line number in script42
$RANDOMRandom integer (0-32767)17463
$SECONDSSeconds since shell started3542
$EPOCHSECONDSSeconds since Unix epoch (Bash 5+)1705325400

Positional Parameters (Script Arguments)

#!/bin/bash
# save as: demo_args.sh
 
echo "Script name: $0"
echo "First argument: $1"
echo "Second argument: $2"
echo "Third argument: $3"
echo "Number of arguments: $#"
echo "All arguments (\$@): $@"
echo "All arguments (\$*): $*"
echo "Process ID: $$"
echo "Last exit status: $?"
 
# Run it:
# ./demo_args.sh hello world foo
# Script name: ./demo_args.sh
# First argument: hello
# Second argument: world
# Third argument: foo
# Number of arguments: 3
# All arguments ($@): hello world foo
# All arguments ($*): hello world foo
# Process ID: 12345
# Last exit status: 0
 

$@ vs $* — Critical Difference

The difference matters when quoting:

#!/bin/bash
# Given arguments: "hello world" "foo bar"
 
# "$@" preserves each argument separately
func_at() {
    for arg in "$@"; do
        echo "Arg: [$arg]"
    done
}
 
# "$*" combines all arguments into one string
func_star() {
    for arg in "$*"; do
        echo "Arg: [$arg]"
    done
}
 
func_at "hello world" "foo bar"
# Output:
# Arg: [hello world]
# Arg: [foo bar]
 
func_star "hello world" "foo bar"
# Output:
# Arg: [hello world foo bar]
 

💡 Always use $@ for argument passing

When passing arguments to functions or other commands, always use "$@" (with double quotes). This correctly preserves:

  • Arguments with spaces
  • Empty arguments
  • Arguments with special characters
# RIGHT: Preserves arguments correctly
my_function() {
    other_command "$@"
}
 
# WRONG: Loses argument boundaries
my_function() {
    other_command $@
}
 

Exit Status ($?)

Every command returns an exit status (0 = success, non-zero = failure):

# Check if command succeeded
ls /existing/path
if [ $? -eq 0 ]; then
    echo "Success!"
fi
 
# Better: check directly
if ls /existing/path; then
    echo "Success!"
fi
 
# Common exit codes
command
echo $?    # 0 = success
#          # 1 = general error
#          # 2 = misuse of shell command
#          # 126 = command not executable
#          # 127 = command not found
#          # 128+n = command killed by signal n
#          # 130 = script terminated by Ctrl+C (128+2)
 
# Use in error handling
if ! mkdir /protected/directory 2>/dev/null; then
    echo "Failed to create directory (exit code: $?)"
    exit 1
fi
 

Other Useful Special Variables

# Random number (0-32767)
echo $RANDOM                         # 17463
dice_roll=$((RANDOM % 6 + 1))        # 1-6
 
# Seconds since shell started
start=$SECONDS
sleep 2
duration=$((SECONDS - start))
echo "Took $duration seconds"
 
# Current line number (useful for debugging)
echo "Debug on line $LINENO"
 
# Last argument of previous command
ls /var/log
cd $_                                 # cd /var/log
 
# PIDs
echo "Current shell PID: $$"
echo "Parent PID: $PPID"
sleep 100 &
echo "Background process PID: $!"
 

Arithmetic

Bash supports integer arithmetic through several mechanisms:

$((...)) — Arithmetic Expansion

The primary way to perform arithmetic:

# Basic operations
echo $((5 + 3))                      # 8
echo $((10 - 3))                     # 7
echo $((4 * 6))                      # 24
echo $((20 / 3))                     # 6 (integer division!)
echo $((20 % 3))                     # 2 (modulo/remainder)
echo $((2 ** 10))                    # 1024 (exponentiation)
 
# Using variables (no $ needed inside $((...)))
x=10
y=3
echo $((x + y))                      # 13
echo $((x * y))                      # 30
echo $((x / y))                      # 3
echo $((x % y))                      # 1
 
# Assignment inside arithmetic
((count = 0))
((count = count + 1))
echo $count                          # 1
 
# Increment / decrement operators
count=0
((count++))                          # count is now 1
((count++))                          # count is now 2
((++count))                          # count is now 3 (pre-increment)
((count--))                          # count is now 2
((--count))                          # count is now 1
 
# Compound assignment
num=10
((num += 5))                         # num = 15
((num -= 3))                         # num = 12
((num *= 2))                         # num = 24
((num /= 4))                         # num = 6
((num %= 4))                         # num = 2
 
# Bitwise operations
echo $((5 & 3))                      # 1 (AND)
echo $((5 | 3))                      # 7 (OR)
echo $((5 ^ 3))                      # 6 (XOR)
echo $((~5))                         # -6 (NOT)
echo $((5 << 2))                     # 20 (left shift)
echo $((20 >> 2))                    # 5 (right shift)
 
# Comparisons (return 1 for true, 0 for false)
echo $((5 > 3))                      # 1
echo $((5 < 3))                      # 0
echo $((5 == 5))                     # 1
echo $((5 != 3))                     # 1
echo $((5 >= 5))                     # 1
echo $((5 <= 3))                     # 0
 
# Logical operators
echo $((1 && 1))                     # 1 (AND)
echo $((1 && 0))                     # 0
echo $((1 || 0))                     # 1 (OR)
echo $((0 || 0))                     # 0
echo $((!0))                         # 1 (NOT)
echo $((!1))                         # 0
 
# Ternary operator
max=$((x > y ? x : y))
min=$((x < y ? x : y))
 
# Complex expressions
result=$(( (x + y) * 2 - z / 4 ))
 

ℹ️ Variables in arithmetic don't need $

Inside $((...)), you can reference variables without the $ prefix:

x=10
y=3
 
# These are equivalent:
echo $((x + y))
echo $(($x + $y))
 
# This is cleaner and recommended:
result=$((x + y))
 

However, you DO need $ when using the result outside arithmetic context.

let — Arithmetic Command

An alternative to ((...)):

let "a = 5 + 3"
let "b = a * 2"
echo $a $b                           # 8 16
 
let "a++"
echo $a                              # 9
 
# Multiple expressions
let "x = 5" "y = 10" "z = x + y"
echo $z                              # 15
 
# Without quotes (careful with spaces)
let a=5+3
let b=a*2
echo $a $b                           # 8 16
 

Note: ((...)) is generally preferred over let as it's more modern and cleaner.

Floating-Point Math with bc

Bash only supports integers. For floating-point, use bc:

# Basic floating-point
echo "scale=2; 10 / 3" | bc          # 3.33
echo "scale=4; 22 / 7" | bc          # 3.1428
 
# Scale sets decimal places
echo "scale=2; 5.5 * 3.2" | bc       # 17.60
echo "scale=0; 5.5 * 3.2" | bc       # 17 (integer)
 
# Mathematical functions (use -l for math library)
echo "scale=4; sqrt(2)" | bc -l      # 1.4142
echo "scale=4; s(1)" | bc -l         # 0.8414 (sine)
echo "scale=4; c(0)" | bc -l         # 1.0000 (cosine)
echo "scale=4; l(10)" | bc -l        # 2.3025 (natural log)
echo "scale=4; e(1)" | bc -l         # 2.7182 (e^1)
 
# Power
echo "2 ^ 10" | bc                   # 1024
echo "1.5 ^ 3" | bc                  # 3.375
 
# Complex expressions
echo "scale=2; (5.5 * 3.2) + (10 / 3)" | bc    # 21.10
 
# Using variables
price=19.99
quantity=5
tax=0.08
total=$(echo "scale=2; $price * $quantity * (1 + $tax)" | bc)
echo "Total: \$$total"               # Total: $107.95
 
# Comparison
result=$(echo "5.5 > 3.2" | bc)      # 1 (true)
result=$(echo "3.2 > 5.5" | bc)      # 0 (false)
 

Common bc patterns:

# Average
echo "scale=2; (15 + 22 + 33 + 41) / 4" | bc    # 27.75
 
# Percentage
echo "scale=2; (75 / 100) * 100" | bc           # 75.00
 
# Convert Fahrenheit to Celsius
fahrenheit=98.6
celsius=$(echo "scale=2; ($fahrenheit - 32) * 5 / 9" | bc)
echo "$fahrenheit°F = $celsius°C"               # 98.6°F = 37.00°C
 
# Round to nearest integer
value=3.7
rounded=$(echo "scale=0; ($value + 0.5) / 1" | bc)
echo $rounded                                    # 4
 

Arithmetic in Different Bases

# Hexadecimal (base 16)
echo $((16#FF))                      # 255
echo $((16#1A))                      # 26
echo $((16#DEAD))                    # 57005
 
# Octal (base 8)
echo $((8#77))                       # 63
echo $((8#100))                      # 64
 
# Binary (base 2)
echo $((2#1010))                     # 10
echo $((2#11111111))                 # 255
 
# Arbitrary base (2-64)
echo $((36#ZZ))                      # 1295 (base 36)
 
# Convert decimal to other bases
printf "%x\n" 255                    # ff (hexadecimal)
printf "%X\n" 255                    # FF (uppercase hex)
printf "%o\n" 255                    # 377 (octal)
printf "%d\n" 0xFF                   # 255 (hex to decimal)
 
# Binary conversion (requires extra work)
decimal_to_binary() {
    echo "obase=2; $1" | bc
}
decimal_to_binary 255                # 11111111
 

Exercises

🏋️ Exercise 1: Variable Basics

Q1: What's the output of each command?

name="Alice"
echo "Hello $name"
echo 'Hello $name'
echo Hello $name
echo "Hello ${name}Smith"
echo "Hello $nameSmith"
 
Show Solution

Hello Alice              # Double quotes: variable expanded
Hello $name              # Single quotes: literal text
Hello Alice              # No quotes: variable expanded
Hello AliceSmith         # ${name} required to separate from "Smith"
Hello                    # Looks for variable "nameSmith" which doesn't exist

Q2: Fix this broken assignment:

user name = "Alice Smith"
 
Show Solution
# Problem: spaces around = and space in variable name
 
# Fixed:
user_name="Alice Smith"
# Or:
username="Alice Smith"
 
# Key fixes:
# 1. Remove spaces around =
# 2. Use underscore instead of space in variable name
 

Q3: What's wrong with this code and how do you fix it?

filename="my document.txt"
rm $filename
 
Show Solution
# Problem: Unquoted variable with spaces is split into two arguments
# rm tries to delete "my" and "document.txt" separately
 
# Fixed:
filename="my document.txt"
rm "$filename"
 
# Explanation: Double quotes preserve the space as part of one argument
 
🏋️ Exercise 2: Parameter Expansion Challenge

Task 1: Given path="/var/log/nginx/access.log", extract:

  • Just the filename: access.log
  • Just the filename without extension: access
  • Just the extension: log
  • Just the directory: /var/log/nginx
Show Solution
path="/var/log/nginx/access.log"
 
filename="${path##*/}"                # access.log (remove everything up to last /)
basename="${filename%.*}"             # access (remove extension)
extension="${filename##*.}"           # log (remove everything up to last .)
directory="${path%/*}"                # /var/log/nginx (remove filename)
 
echo "File: $filename"
echo "Base: $basename"
echo "Ext: $extension"
echo "Dir: $directory"
 

Task 2: Given url="https://www.example.com/path/to/page.html", extract:

  • Protocol: https
  • Domain: www.example.com
  • Path: path/to/page.html
Show Solution
url="https://www.example.com/path/to/page.html"
 
protocol="${url%%://*}"               # https (remove everything from :// onward)
rest="${url#*://}"                    # www.example.com/path/to/page.html
domain="${rest%%/*}"                  # www.example.com (remove path)
path="${rest#*/}"                     # path/to/page.html (remove domain)
 
echo "Protocol: $protocol"
echo "Domain: $domain"
echo "Path: $path"
 

Task 3: Convert this filename to a URL-safe slug (lowercase, spaces to hyphens, remove special characters):


"My Awesome Document (Final).pdf"

Show Solution
filename="My Awesome Document (Final).pdf"
 
# Remove extension
slug="${filename%.*}"                 # My Awesome Document (Final)
 
# Replace spaces with hyphens
slug="${slug// /-}"                   # My-Awesome-Document-(Final)
 
# Remove parentheses
slug="${slug//[\(\)]/}"               # My-Awesome-Document-Final
 
# Convert to lowercase
slug="${slug,,}"                      # my-awesome-document-final
 
echo "$slug"                          # my-awesome-document-final
 
🏋️ Exercise 3: Arrays and Iteration

Task 1: Create an array of the 5 most common shell commands. Loop through and print each with its number.

Show Solution
commands=("ls" "cd" "grep" "find" "cat")
 
for i in "${!commands[@]}"; do
    echo "$((i+1)). ${commands[$i]}"
done
 
# Output:
# 1. ls
# 2. cd
# 3. grep
# 4. find
# 5. cat
 

Task 2: Create an associative array representing a server configuration (host, port, database, username). Loop through and print each setting.

Show Solution
declare -A config=(
    [host]="localhost"
    [port]="5432"
    [database]="myapp"
    [username]="admin"
)
 
echo "Server Configuration:"
for key in "${!config[@]}"; do
    echo "  $key: ${config[$key]}"
done
 
# Output:
# Server Configuration:
#   host: localhost
#   port: 5432
#   database: myapp
#   username: admin
 

Task 3: Given an array of filenames with spaces, safely copy each to a backup directory.

files=("my document.txt" "important data.csv" "report 2024.pdf")
 
Show Solution
files=("my document.txt" "important data.csv" "report 2024.pdf")
 
# Create backup directory
mkdir -p backup
 
# Copy each file (correctly handling spaces)
for file in "${files[@]}"; do
    if [ -f "$file" ]; then
        cp "$file" "backup/$file"
        echo "Backed up: $file"
    else
        echo "Not found: $file"
    fi
done
 
# Key: "${files[@]}" with double quotes preserves spaces in filenames
 
🏋️ Exercise 4: Arithmetic Challenge

Task 1: Calculate compound interest: You invest $1000 at 5% annual interest for 3 years. What's the final amount?

Formula: A = P(1 + r)^n

Show Solution
principal=1000
rate=0.05
years=3
 
# Bash doesn't do floating-point, so use bc
amount=$(echo "scale=2; $principal * (1 + $rate) ^ $years" | bc)
 
echo "Initial investment: \$$principal"
echo "Interest rate: ${rate}%"
echo "Years: $years"
echo "Final amount: \$$amount"
 
# Output: $1157.62
 

Task 2: Convert 98.6°F to Celsius. Formula: C = (F - 32) × 5/9

Show Solution
fahrenheit=98.6
 
# Using bc for floating-point
celsius=$(echo "scale=2; ($fahrenheit - 32) * 5 / 9" | bc)
 
echo "${fahrenheit}°F = ${celsius}°C"
 
# Output: 98.6°F = 37.00°C
 

Task 3: Calculate the total cost of an online order: 15 items at $24.99 each, with 8.5% sales tax and $12.50 shipping.

Show Solution
items=15
price=24.99
tax_rate=0.085
shipping=12.50
 
# Calculate subtotal
subtotal=$(echo "scale=2; $items * $price" | bc)
 
# Calculate tax
tax=$(echo "scale=2; $subtotal * $tax_rate" | bc)
 
# Calculate total
total=$(echo "scale=2; $subtotal + $tax + $shipping" | bc)
 
echo "Items: $items × \$$price = \$$subtotal"
echo "Tax (${tax_rate}%): \$$tax"
echo "Shipping: \$$shipping"
echo "Total: \$$total"
 
# Output:
# Items: 15 × $24.99 = $374.85
# Tax (0.085%): $31.86
# Shipping: $12.50
# Total: $419.21
 

Summary

Variables and the environment are the foundation of Bash scripting:

Variables:

  • Create with name=value (NO SPACES around =)
  • Access with $name or ${name}
  • Unset with unset name
  • Make read-only with readonly name=value

Quoting (CRITICAL):

  • "double quotes": Expand variables/commands, preserve spaces
  • 'single quotes': Treat everything as literal text
  • No quotes: Word splitting and glob expansion
  • $'ANSI': Enable escape sequences (\n, \t, etc.)
  • Default: Always quote variables ("$var")

Parameter Expansion:

  • Default values: ${var:-default}, ${var:=default}
  • String length: ${#var}
  • Substrings: ${var:offset:length}
  • Pattern removal: ${var#pattern}, ${var%pattern}
  • Substitution: ${var/pattern/replacement}
  • Case conversion: ${var^^}, ${var,,}

Environment:

  • Shell variables: Local to current shell
  • Environment variables: Inherited by child processes (use export)
  • Key variables: PATH, HOME, USER, EDITOR
  • View with: env, printenv, echo $VAR

Arrays:

  • Indexed: array=("a" "b" "c"), access with ${array[0]}
  • Associative: declare -A map=([key]="value")
  • Always quote expansions: "${array[@]}"

Arithmetic:

  • Integer math: $((expression))
  • Floating-point: echo "scale=2; expression" | bc
  • Operators: +, -, *, /, %, **, ++, --

Special Variables:

  • Arguments: $1, $2, ..., $#, "$@"
  • Exit status: $?
  • PIDs: $$, $!, $PPID
  • Random: $RANDOM, $SECONDS

Master these concepts and you'll have the foundation for powerful Bash scripts. In the next tutorial, you'll learn about pipes and redirection—how to chain commands together and control data flow.

Was this page helpful?
SR

Written by the ShellRAG Team

The ShellRAG editorial team writes practical, beginner-friendly Bash Shell tutorials with tested code examples and real-world use cases. Every article is technically reviewed for accuracy and updated regularly.

Learn more about us →