Linux Basic — Array Slicing & Copying

3 minute read

Array Slicing & Copying

You learned ${var:offset:length} for strings. The same syntax works on arrays — but the offset is element index, not byte. This page covers slicing, deep-copying, and merging.


Slicing — ${arr[@]:offset:length}

arr=(red green blue yellow purple orange)

echo "${arr[@]:0:3}"        # red green blue        (first 3)
echo "${arr[@]:2:2}"        # blue yellow           (2 starting at index 2)
echo "${arr[@]:3}"          # yellow purple orange  (from 3 to end)
echo "${arr[@]: -2}"        # purple orange         (last 2 — space required)
echo "${arr[@]: -3:2}"      # yellow purple         (2 starting 3 from end)

Note:

  • ${arr[@]:1:2} returns 2 elements starting at index 1
  • Space before - is required to disambiguate from ${var:-default}
  • Negative offset works in Bash 4.2+

To assign the slice back to a new array, wrap in ():

first_half=( "${arr[@]:0:3}" )
second_half=( "${arr[@]:3}" )
echo "${first_half[@]}"     # red green blue
echo "${second_half[@]}"    # yellow purple orange

Slicing Positional Parameters

$@ and $* slice exactly the same way:

set -- a b c d e
echo "${@:2}"               # b c d e          (from index 2)
echo "${@:2:3}"             # b c d            (3 from index 2)
echo "${@: -1}"             # e                (last)
echo "${@:1:$#-1}"          # a b c d          (all but last)

Useful idiom — “command and its args”:

mycmd_with_logging() {
    local cmd=$1
    local args=( "${@:2}" )
    echo ">>> Running: $cmd ${args[*]}"
    "$cmd" "${args[@]}"
}

mycmd_with_logging echo hello world

Copying Arrays — Deep, Always

Bash has no reference semantics for arrays (without nameref). Copying means cloning every element:

src=(apple banana cherry)

# ✅ Correct deep copy — note the quoted expansion + parens
dst=( "${src[@]}" )

dst[0]=apricot
echo "${src[@]}"            # apple banana cherry    (unchanged)
echo "${dst[@]}"            # apricot banana cherry

The quoting matters: dst=( ${src[@]} ) (unquoted) would word-split each element on $IFS, breaking entries with spaces:

src=("hello world" "foo bar")
dst_bad=( ${src[@]} )       # ❌
dst_ok=( "${src[@]}" )      # ✅
printf 'bad : %s\n' "${dst_bad[@]}"
# bad : hello
# bad : world
# bad : foo
# bad : bar
printf 'ok  : %s\n' "${dst_ok[@]}"
# ok  : hello world
# ok  : foo bar

Copying Associative Arrays

There’s no equally clean one-liner. You need to iterate:

declare -A src=([host]=example.com [port]=8080)
declare -A dst

for k in "${!src[@]}"; do
    dst[$k]=${src[$k]}
done

Bash 5.1+ has declare -A dst=( "${src[@]@K}" ) using the @K transformation, but that’s a recent addition. The loop above works everywhere.


Concatenation

Indexed arrays merge via += or explicit reconstruction:

a=(1 2 3)
b=(4 5 6)

# Concat via +=
combined=( "${a[@]}" )
combined+=( "${b[@]}" )

# Or in one shot
combined=( "${a[@]}" "${b[@]}" )

echo "${combined[@]}"        # 1 2 3 4 5 6

Associative arrays don’t merge with +=. Loop the second into the first:

declare -A first=([a]=1 [b]=2)
declare -A second=([b]=20 [c]=30)

for k in "${!second[@]}"; do
    first[$k]=${second[$k]}
done

# first now: [a]=1 [b]=20 [c]=30      ← "second" wins on conflicts

Passing Arrays to Functions

You can’t pass a Bash array as a single argument — only its expansion. Two clean patterns:

Pattern A — Expand and Capture

sum() {
    local total=0
    for n in "$@"; do
        total=$((total + n))
    done
    echo "$total"
}

nums=(10 20 30)
sum "${nums[@]}"             # 60

The function receives the elements as $1, $2, … — most idiomatic.

Pattern B — Pass by Name (nameref)

print_array() {
    local -n ref=$1           # nameref to caller's array
    for x in "${ref[@]}"; do
        echo "[$x]"
    done
}

fruits=(apple banana cherry)
print_array fruits           # pass the *name*

local -n (Bash 4.3+) makes ref an alias for the caller’s variable. Useful when you need to modify the caller’s array, or pass an associative array.


A Real Example — Filter Then Slice

all_users=(alice bob carol dan eve frank)

# Keep users starting with a-c
filtered=()
for u in "${all_users[@]}"; do
    [[ "${u:0:1}" == [a-c] ]] && filtered+=("$u")
done

# Take first 2 of the filtered set
top2=( "${filtered[@]:0:2}" )

echo "${top2[@]}"            # alice bob

Filter via a loop into a new array, then slice — composable and clear.


Common Mistakes

❌ Wrong ✅ Right Issue
dst=$src (no parens) dst=( "${src[@]}" ) $src is just ${src[0]}
dst=( ${src[@]} ) (unquoted) dst=( "${src[@]}" ) unquoted re-splits on $IFS
for x in ${arr[*]} for x in "${arr[@]}" * joins; @ separates
assoc_copy=( "${src[@]}" ) iterate keys + reassign assoc copy needs key/value loop
${arr[@]:-1} for “last” ${arr[@]: -1} (space) :- is default operator

Next → Sorting Arrays

Updated: