Skip to main content
Bash · lesson 4 of 22
Bash beginner Lesson 4 of 22

Bash Data Types

Learn how Bash handles strings, integers, indexed and associative arrays, namerefs and case attributes, with real output for every example.

By SkillByExample · Updated

Every example below is a complete script, run with bash 5.2 on Ubuntu 24.04.

Everything Is a String

Bash has no strict type system. Every variable holds a string, and the context decides how it is read: as a number inside arithmetic, as a path in a file test, as text everywhere else.

x=42
z=3.14
declare -p x z

first="Alice"
last="Smith"
full="${first} ${last}"
echo "$full has ${#full} characters"

message=$(cat <<'EOF'
Dear Alice,
Your account is active.
EOF
)
echo "$message"
$ bash strings.sh
declare -- x="42"
declare -- z="3.14"
Alice Smith has 11 characters
Dear Alice,
Your account is active.

declare -p shows how Bash stores a variable. The -- means “no attributes”: 42 and 3.14 are both plain strings. ${#var} gives the length in characters. A heredoc inside $(...) is the cleanest way to put a multi-line block into a variable.

Integers

declare -i gives a variable the integer attribute. After that, every assignment to it is evaluated as arithmetic.

declare -i count=10
count+=5
echo "declare -i: $count"

plain=10
plain+=5
echo "plain:      $plain"

count="abc"
echo "abc -> $count"
abc=7
count="abc * 2"
echo "abc * 2 -> $count"
$ bash integers.sh
declare -i: 15
plain:      105
abc -> 0
abc * 2 -> 14

Without the attribute, += appends text, so 10 plus 5 becomes 105. The last two lines are the surprise. Assigning "abc" doesn’t raise an error. Bash evaluates abc as an arithmetic expression, which treats it as a variable name. abc was unset, so the result was 0. Once abc=7, the same mechanism turns "abc * 2" into 14. declare -i evaluates whatever you assign to it, including text you never meant as a number.

For one-off arithmetic, $(( )) is clearer and needs no attribute:

a=10
b=3
echo "a + b  = $(( a + b ))"
echo "a / b  = $(( a / b ))"
echo "a % b  = $(( a % b ))"
echo "a ** b = $(( a ** b ))"
echo "7 / 2 via bc -l: $(echo '7 / 2' | bc -l)"
$ bash arith.sh
a + b  = 13
a / b  = 3
a % b  = 1
a ** b = 1000
7 / 2 via bc -l: 3.50000000000000000000

Bash arithmetic is integer-only, so 10 / 3 truncates to 3. For decimals, hand the expression to bc -l or awk.

Indexed Arrays

An indexed array stores an ordered list, such as servers to check or files to process.

fruits=("apple" "banana" "cherry")
echo "first: ${fruits[0]}  last: ${fruits[-1]}"
echo "count: ${#fruits[@]}  indices: ${!fruits[*]}"

fruits+=("date")
fruits[1]="blueberry"
unset 'fruits[2]'
echo "after edits: ${fruits[*]}"
echo "count: ${#fruits[@]}  indices: ${!fruits[*]}"
echo "fruits[2] is: [${fruits[2]}]"

letters=(a b c d e)
echo "slice 1:3 -> ${letters[*]:1:3}"
$ bash arrays.sh
first: apple  last: cherry
count: 3  indices: 0 1 2
after edits: apple blueberry date
count: 3  indices: 0 1 3
fruits[2] is: []
slice 1:3 -> b c d

unset removes an element but does not renumber the others. After the edits, the indices are 0 1 3, and fruits[2] is empty even though the array has three elements. Loop with "${fruits[@]}" (values) or "${!fruits[@]}" (real indices), never for i in $(seq 0 $((${#fruits[@]}-1))). Quote 'fruits[2]' in unset so the brackets can’t be taken as a filename glob.

Associative Arrays

Associative arrays use string keys, like a dictionary, and need declare -A.

declare -A config=(
  [host]="localhost"
  [port]="5432"
  [db]="myapp"
  [user]="app"
)

echo "port is ${config[port]}"
for key in "${!config[@]}"; do
  echo "$key = ${config[$key]}"
done

if [[ -v config[password] ]]; then echo "password set"; else echo "password not set"; fi
$ bash assoc.sh
port is 5432
db = myapp
port = 5432
host = localhost
user = app
password not set

The keys come back in hash order (db first), not in the order you wrote them. If order matters, pipe the loop through sort or keep a separate indexed array of keys. [[ -v array[key] ]] tests whether a key exists.

declare -A is not optional. Leave it out and Bash does something worse than fail:

user[name]="Alice"
user[role]="admin"
declare -p user
echo "name: ${user[name]}"
$ bash forgot-A.sh
declare -a user=([0]="admin")
name: admin

Without the declaration, user became an indexed array (-a). The subscripts name and role were evaluated as arithmetic, both unset variables, so both became index 0. The second write overwrote the first, and reading user[name] returns admin. There is no error message, just the wrong data.

Namerefs and Case Attributes

declare -n makes one variable an alias for another variable, named at runtime. This is how a function can work on whichever array you hand it.

declare -A server_a=([host]="web1" [port]="80")
declare -A server_b=([host]="web2" [port]="443")

print_server() {
  declare -n ref="$1"
  echo "Host: ${ref[host]}, Port: ${ref[port]}"
}

print_server server_a
print_server server_b
$ bash nameref.sh
Host: web1, Port: 80
Host: web2, Port: 443

-l and -u convert the value’s case on every assignment:

declare -l lower="MiXeD Case"
declare -u upper="MiXeD Case"
echo "$lower / $upper"
lower="NOW UPPER?"
echo "$lower"
$ bash case.sh
mixed case / MIXED CASE
now upper?

Type Summary

declare flagTypeExample
(none)Stringname="Alice"
-iInteger (assignments evaluated as arithmetic)declare -i count=0
-aIndexed arraydeclare -a list=()
-AAssociative arraydeclare -A map=()
-rReadonlydeclare -r MAX=100
-xExporteddeclare -x PATH="..."
-nNamerefdeclare -n ref=varname
-l / -uLowercase / uppercase on assigndeclare -l lower

Practice

  1. Sum nums=(4 8 15 16 23 42) into a declare -i total variable, then print the integer average.

    Solution
    nums=(4 8 15 16 23 42)
    declare -i total=0
    for n in "${nums[@]}"; do
      total+=n
    done
    echo "total=$total average=$(( total / ${#nums[@]} ))"
    $ bash p1.sh
    total=108 average=18

    total+=n works without $n because the right side is evaluated as arithmetic.

  2. Count how many times each word appears in red blue red green blue red with an associative array, and print the counts in alphabetical order.

    Solution
    declare -A seen
    for w in red blue red green blue red; do
      seen[$w]=$(( ${seen[$w]:-0} + 1 ))
    done
    for w in "${!seen[@]}"; do echo "$w ${seen[$w]}"; done | sort
    $ bash p2.sh
    blue 2
    green 1
    red 3
  3. After unset 'fruits[1]' leaves a gap, renumber the array so its indices are 0 1 2 again.

    Solution
    fruits=(apple banana cherry date)
    unset 'fruits[1]'
    echo "before: ${!fruits[*]}"
    fruits=("${fruits[@]}")
    echo "after:  ${!fruits[*]} -> ${fruits[1]}"
    $ bash p3.sh
    before: 0 2 3
    after:  0 1 2 -> cherry

    Re-assigning from "${fruits[@]}" rebuilds the array from its values, so the indices are packed again.

  4. Compute the decimal average of the same nums array to two places, without bc.

    Solution
    nums=(4 8 15 16 23 42)
    printf '%s\n' "${nums[@]}" | awk '{ s += $1 } END { printf "average=%.2f\n", s / NR }'
    $ bash p4.sh
    average=18.00

Frequently Asked Questions

Does Bash have real data types?
Not in the traditional sense. Every variable in Bash is a string by default. declare -i tells Bash to treat a variable as an integer for arithmetic, but it's still stored as a string internally.
What is the difference between indexed and associative arrays?
Indexed arrays use integer indices (0, 1, 2...). Associative arrays use string keys, like a hash map or dictionary. Associative arrays require declare -A.
Can Bash handle floating point numbers?
Not natively. Use the bc utility or awk for float arithmetic. Python is a better choice if your script heavily uses decimals.
What happens if I forget declare -A for an associative array?
Bash silently creates an indexed array instead. The string keys are evaluated as arithmetic, which usually means they become index 0, so every assignment overwrites the same slot and only the last value survives — with no error message.