Linux Basic β Manipulating Arrays Practice Exercises
Manipulating Arrays β Practice Exercises
Five exercises covering indexed arrays, associative arrays, slicing, sorting, and the array-vs-string trap.
Exercise 1 β Word Counter
Goal: Read a stream of words on stdin (one per line) and print each unique word with its count, sorted by count descending then by word ascending.
Setup:
cat > /tmp/words.txt <<'EOF'
apple
banana
apple
cherry
banana
apple
date
cherry
EOF
π‘ Show Answer
declare -A count
while read -r word; do
[[ -z "$word" ]] && continue
(( count[$word]++ ))
done < /tmp/words.txt
# Print "<count> <word>" then sort by count desc, word asc
for k in "${!count[@]}"; do
printf '%d %s\n' "${count[$k]}" "$k"
done | sort -k1,1rn -k2,2
# 3 apple
# 2 banana
# 2 cherry
# 1 date
sort -k1,1rn -k2,2 sorts primarily by column 1 reverse-numeric, secondarily by column 2 alphabetic. The 1,1 and 2,2 bounds tell sort exactly which fields to use (without them, it sorts to end-of-line).
Pure-awk one-liner (same output):
awk '{c[$1]++} END {for (k in c) printf "%d %s\n", c[k], k}' /tmp/words.txt \
| sort -k1,1rn -k2,2
Exercise 2 β Deduplicate Preserving Order
Goal: Given an array with duplicates, produce a new array containing each element once, in the order of first occurrence.
Input: (apple banana apple cherry banana date apple)
Expected: (apple banana cherry date)
π‘ Show Answer
items=(apple banana apple cherry banana date apple)
declare -A seen
unique=()
for x in "${items[@]}"; do
if [[ ! -v seen[$x] ]]; then
seen[$x]=1
unique+=("$x")
fi
done
printf '%s\n' "${unique[@]}"
# apple
# banana
# cherry
# date
sort -u would reorder. To preserve insertion order you need an associative-array set + parallel ordered list. This is a fundamental shell pattern β memorise it.
Exercise 3 β Latest Version
Goal: Given a list of version strings (some with v prefix, some without, some with pre-release suffixes), find the highest version using version-aware sorting.
Input: (1.2.3 v0.9 1.10.0 v1.2.3-rc1 2.0.0 v1.2.3 0.9.5)
π‘ Show Answer
versions=(1.2.3 v0.9 1.10.0 v1.2.3-rc1 2.0.0 v1.2.3 0.9.5)
mapfile -t sorted < <(printf '%s\n' "${versions[@]}" | sort -V)
printf '%s\n' "${sorted[@]}"
# v0.9
# 0.9.5
# 1.2.3
# v1.2.3-rc1
# v1.2.3
# 1.10.0
# 2.0.0
latest=${sorted[-1]}
echo "Latest: $latest"
# Latest: 2.0.0
sort -V understands that 1.10.0 > 1.2.3 (lexically the opposite), and that 1.2.3-rc1 < 1.2.3 (pre-release comes before final).
Exercise 4 β Build a Command Dynamically
Goal: Write a function fetch that calls curl with optional flags. The function takes a URL and reads three optional env vars: AUTH_TOKEN, TIMEOUT, VERBOSE. If set, add the appropriate curl flags:
AUTH_TOKENβ-H "Authorization: Bearer $AUTH_TOKEN"TIMEOUTβ--max-time "$TIMEOUT"VERBOSE=1β-v
Demonstrate by echoing the command instead of running it, so we can see the args.
π‘ Show Answer
fetch() {
local url=$1
local cmd=(curl)
[[ -n "${AUTH_TOKEN:-}" ]] && cmd+=(-H "Authorization: Bearer $AUTH_TOKEN")
[[ -n "${TIMEOUT:-}" ]] && cmd+=(--max-time "$TIMEOUT")
[[ "${VERBOSE:-0}" == "1" ]] && cmd+=(-v)
cmd+=("$url")
# For demonstration, print one arg per line
printf 'Args:\n'
printf ' [%s]\n' "${cmd[@]}"
# Real call would be:
# "${cmd[@]}"
}
AUTH_TOKEN="secret with spaces" TIMEOUT=30 VERBOSE=1 fetch "https://example.com/api"
Output:
Args:
[curl]
[-H]
[Authorization: Bearer secret with spaces]
[--max-time]
[30]
[-v]
[https://example.com/api]
Each curl flag and value is one array element β quoting of the bearer-token string is preserved end-to-end. This is the right way to build commands dynamically.
Exercise 5 β Largest 5 Files
Goal: Find the 5 largest regular files under a directory and store their paths in an array. Handle filenames with spaces. Print each as "<size> <path>".
Setup:
mkdir -p /tmp/sizes && cd /tmp/sizes
for i in {1..20}; do
dd if=/dev/zero of="file $i.bin" bs=1024 count=$((RANDOM % 100 + 1)) 2>/dev/null
done
π‘ Show Answer
cd /tmp/sizes
# NUL-delimited capture handles any filename
mapfile -d '' -t paths < <(find . -maxdepth 1 -type f -print0)
# Sort by size descending, take top 5
mapfile -t top5 < <(
for p in "${paths[@]}"; do
sz=$(stat -f '%z' "$p" 2>/dev/null || stat -c '%s' "$p")
printf '%d %s\n' "$sz" "$p"
done | sort -rn | head -5
)
printf '%s\n' "${top5[@]}"
# 102400 ./file 7.bin
# 98304 ./file 14.bin
# β¦
Two key techniques:
- NUL-delimited capture β
find -print0 | mapfile -d ''is the safest way to handle arbitrary filenames - Cross-platform
statβ Linux usesstat -c '%s', macOS/BSD usesstat -f '%z'; try one, fall back to the other
For one-shot use, the all-in-one pipeline is fine too:
find . -maxdepth 1 -type f -printf '%s %p\n' 2>/dev/null \
| sort -rn | head -5
But -printf is GNU-only. The stat approach is portable.
Whatβs Next?
Arrays handle data structures. The next chapter focuses on the loops that consume them β looping over arrays, directory listings, command output, globs, and find β¦ -print0 | xargs.
Continue β Iterating Through Lists