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:
| Variable | Description | Example |
|---|---|---|
$0 | Script name (or shell) | ./myscript.sh or bash |
$1-$9 | Positional parameters (arguments 1-9) | arg1, arg2, ... |
${10}, ${11}, ... | Arguments 10+ (braces required) | arg10, arg11, ... |
$# | Number of arguments | 3 |
$@ | 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 shell | 12345 |
$! | PID of last background process | 12346 |
$_ | Last argument of previous command | file.txt |
$- | Current shell option flags | himBH |
$PPID | Parent process ID | 12344 |
$BASHPID | PID of current Bash process | 12345 |
$BASH_VERSION | Bash version | 5.2.15(1)-release |
$LINENO | Current line number in script | 42 |
$RANDOM | Random integer (0-32767) | 17463 |
$SECONDS | Seconds since shell started | 3542 |
$EPOCHSECONDS | Seconds 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
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
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
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
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
$nameor${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.
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 →