Bash · 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 flag | Type | Example |
|---|---|---|
| (none) | String | name="Alice" |
-i | Integer (assignments evaluated as arithmetic) | declare -i count=0 |
-a | Indexed array | declare -a list=() |
-A | Associative array | declare -A map=() |
-r | Readonly | declare -r MAX=100 |
-x | Exported | declare -x PATH="..." |
-n | Nameref | declare -n ref=varname |
-l / -u | Lowercase / uppercase on assign | declare -l lower |
Practice
-
Sum
nums=(4 8 15 16 23 42)into adeclare -i totalvariable, 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=18total+=nworks without$nbecause the right side is evaluated as arithmetic. -
Count how many times each word appears in
red blue red green blue redwith 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 -
After
unset 'fruits[1]'leaves a gap, renumber the array so its indices are0 1 2again.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 -> cherryRe-assigning from
"${fruits[@]}"rebuilds the array from its values, so the indices are packed again. -
Compute the decimal average of the same
numsarray to two places, withoutbc.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