Linux Basic β€” Manipulating Arrays Practice Exercises

3 minute read

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:

  1. NUL-delimited capture β€” find -print0 | mapfile -d '' is the safest way to handle arbitrary filenames
  2. Cross-platform stat β€” Linux uses stat -c '%s', macOS/BSD uses stat -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

Updated: