In bash, after `arr=(a b c); unset 'arr[1]'`, what do `${#arr[@]}` and `${!arr[@]}` report, and why is a C-style loop from 0 to `${#arr[@]}-1` the wrong way to iterate an array?
answer
- holes are allowed
- count is not the highest index
- unset does not renumber
- iterate the indices, not a counter
basics
~20 sBash arrays are sparse: unset removes index 1 without renumbering, so ${#arr[@]} is 2 while the live indices from ${!arr[@]} are 0 and 2. A counter loop bounded by the count would read index 1, which no longer exists, and never reach index 2.
solid answer
~40 sAn indexed array in bash is a map from integers to values, not a contiguous vector. `unset 'arr[1]'` deletes that one subscript and leaves a hole: `${#arr[@]}` counts the *existing* elements and gives 2, while `${!arr[@]}` lists the actual subscripts, `0 2`. Element `c` is still at index 2 — nothing shifts down. A loop like `for ((i=0; i<${#arr[@]}; i++))` therefore visits 0 and 1: index 1 expands to the empty string, and index 2 is never visited at all. Iterate `for x in "${arr[@]}"` when you only need values, or `for i in "${!arr[@]}"` when you need subscripts too. Note also that `arr+=(d)` appends after the *highest* index, so it lands at 3 and the hole stays.
code
bash · 16 linesarr=(a b c)
unset 'arr[1]'
echo "${#arr[@]}" # 2
echo "${!arr[@]}" # 0 2
printf '[%s]\n' "${arr[@]}" # [a] [c]
arr+=(d)
echo "${!arr[@]}" # 0 2 3 (append goes after the highest index)
for i in "${!arr[@]}"; do
printf '%s=%s\n' "$i" "${arr[i]}"
done
arr=("${arr[@]}") # compact: indices become 0 1 2
echo "${!arr[@]}" # 0 1 2go deeper
Iterate with for x in "${arr[@]}" rather than with a numeric counter, and remember that ${#arr[@]} is how many elements exist, not the largest index.
Explain that a bash array is a map from integers to values, so unset leaves a hole and nothing is renumbered; show ${!arr[@]} as the authoritative list of live subscripts.
Point out where sparseness arises naturally in real scripts — subscripting by pid, id or line number, or filtering in place — and note that every count-based assumption downstream becomes wrong once it does.
When a script is subscripting by identifier and worrying about holes, recognise it has outgrown the shell's single flat list type, and call the boundary at which the logic should move to a language with real collections.
## Arrays are sparse by design Bash's indexed array is closer to a dictionary keyed by integers than to a C array. Subscripts need not start at 0, need not be contiguous, and have no fixed upper bound within the shell's arithmetic range. You can create a hole two ways: ```bash arr=(a b c) unset 'arr[1]' # delete a subscript other=() other[5]=x # assign a distant subscript ``` After the first, the array holds `[0]=a` and `[2]=c`. After the second, `other` has exactly one element, at subscript 5. Quote the argument to `unset`. Unquoted, `unset arr[1]` is subject to pathname expansion, and if a file named `arr1` happens to exist in the current directory the word is replaced by that filename and you unset the wrong variable. `unset 'arr[1]'` is unambiguous. ## What each expansion reports ```bash arr=(a b c) unset 'arr[1]' echo "${#arr[@]}" # 2 -> number of existing elements echo "${!arr[@]}" # 0 2 -> the subscripts that exist echo "${arr[2]}" # c -> nothing was renumbered echo "${arr[1]}" # -> empty, the subscript is gone printf '[%s]\n' "${arr[@]}" # [a] [c] -> two words, holes skipped ``` The key asymmetry: `${#arr[@]}` is a **count**, not a maximum subscript. In a dense array they differ by exactly one, which is why the confusion survives so long — the buggy loop is correct until the first hole appears. Expanding `"${arr[@]}"` skips the holes entirely, producing one word per existing element. So value-only iteration is always safe. ## Why the counter loop breaks ```bash for ((i = 0; i < ${#arr[@]}; i++)); do printf '%s\n' "${arr[i]}" done ``` With the hole present, `${#arr[@]}` is 2, so `i` takes 0 and 1. Index 0 gives `a`. Index 1 does not exist and expands to the empty string — under `set -u` it does not even error, because a missing array subscript is treated as empty rather than unset in most bash versions. Index 2, holding `c`, is never visited. The result is a loop that silently processes the wrong number of items and emits a blank line, which is exactly the sort of defect that survives code review. The two correct forms: ```bash for x in "${arr[@]}"; do ...; done # values only for i in "${!arr[@]}"; do ... "${arr[i]}"; done # subscripts and values ``` Inside `${arr[i]}` the subscript is an arithmetic context, so the bare name `i` is correct there — no `$` needed. ## Where holes come from in real scripts Rarely from an explicit `unset`. More often from: - filtering in place: deleting entries that failed validation while iterating; - using an id or a line number as the subscript, e.g. `results[$pid]=ok`, which is naturally sparse; - an incremental assignment such as `arr[$n]=v` where `n` skips values. Once any of those has happened, every count-based assumption in the rest of the script is suspect. ## Appending and compacting `arr+=(d)` appends after the highest existing subscript, so on `[0]=a [2]=c` the new element lands at 3, giving indices `0 2 3`. The hole is not filled and not closed. When you genuinely want a dense array again, rebuild it from the values: ```bash arr=("${arr[@]}") ``` The quoted expansion emits the existing elements in subscript order as separate words, and the compound assignment renumbers them from 0. This is the standard compaction idiom and it is safe for values containing spaces, precisely because the expansion is quoted. ## Summary Treat `${#arr[@]}` as a count and `${!arr[@]}` as the authoritative index list, never derive one from the other, and iterate over expansions rather than over a manufactured counter.
- How do you compact a sparse array so the indices are contiguous again?Reassign it from its own quoted expansion: `arr=("${arr[@]}")`. The quoted [@] emits the existing elements in subscript order as separate words, and the compound assignment renumbers them from 0. Because the expansion is quoted, elements containing spaces survive intact.
- Why should the argument to unset be quoted, as in `unset 'arr[1]'`?Unquoted, the word `arr[1]` is a valid globbing pattern, so pathname expansion can replace it with a matching filename such as `arr1` in the current directory — and you then unset a different variable, or nothing at all. Quoting stops the pattern from being expanded. ShellCheck warns about this case too.
saying these in an interview costs you the question
- Assuming indices are always contiguous from zero
- Treating ${#arr[@]} as the highest index
- Expecting unset to shift later elements down
- Looping with a counter bounded by the element count
- Leaving unset arr[1] unquoted so a glob can rewrite it