4

I would like to make a function that takes a bash array like this one:

a=("element zero" "element one" "element two")

and removes one element like "element one" and leaves a the array like this:

a=("element zero" "element two")

such that echo $a[1] will print out element two and not zero.

I've seen several attempts at this, but haven't found one that did it cleanly or without breaking elements that have spaces into multiple elements. Or just setting the element to be blank (i.e. not shifting the indexes of subsequent array elements).

0

2 Answers 2

11
# initial state
a=( "first element" "second element" "third element" )

# to remove
unset a[0]

# to reindex, such that a[0] is the old a[1], rather than having the array
# start at a[1] with no a[0] entry at all
a=( "${a[@]}" )

# to print the array with its indexes, to check its state at any stage
declare -p a

...now, for a function, if you have bash 4.3, you can use namevars to do this without any eval at all:

remove() {
  local -n _arr=$1      # underscore-prefixed name to reduce collision likelihood
  local idx=$2
  unset _arr[$idx]      # remove the undesired item
  _arr=( "${_arr[@]}" ) # renumber the indexes
}

For older versions of bash, it's a bit stickier:

remove() {
  local cmd
  unset "$1[$2]"
  printf -v cmd '%q=( "${%q[@]}" )' "$1" "$1" && eval "$cmd"
}

The use of printf with %q format strings is a bit of paranoia -- it makes it harder for maliciously chosen values (in this case, variable names) to perform actions of their choice, as opposed to simply failing with no effect.


All that said -- it's better if you don't renumber your arrays. If you leave off the renumbering step, such that after deleting entry a[1] you simply have a sparse array with no content at that index (which is different from an empty string at that index -- bash "arrays" are actually stored as linked lists or hash tables [in the associative case], not as arrays at all, so sparse arrays are memory-efficient), the delete operation is much faster.

This doesn't break your ability to iterate over your arrays if you retrieve ask the array for its keys rather than supplying them externally, as in:

for key in "${!a[@]}"; do
  value="${a[$key]}"
  echo "Entry $key has value $value"
done
Sign up to request clarification or add additional context in comments.

9 Comments

Is the order of keys in "${!a[@]}" guaranteed to still be numerically increasing after elements have been unset? (I mean, to the extent that bash carries guarantees :) )
For a non-associative array, yes.
Charles, for your non-function example, after unset a[0] and reindex I get declare -a a='([0]="second" [1]="element" [2]="third" [3]="element")' Is there something needed to prevent word splitting on reindex? That is the output of declare -p a
@CharlesDuffy testing, I find you need a=( "${a[@]}" ) to prevent word splitting.
@DavidC.Rankin, indeed, one does. Where do I say, imply, or demonstrate anything contrary?
|
0
remove() { 
    eval "$1=( \"\${$1[@]:0:$2}\" \"\${$1[@]:$(($2+1))}\" )" 
}

Can be called like this:

a=("element zero" "element one" "element two")
remove a 1
echo ${a[@]}  #element zero element two
echo ${a[1]}  #element two

This will leave blank array elements.

a=("element zero" "element one" "" "element three")
remove a 1
echo ${a[@]}  #element zero element two
echo ${a[1]}  #
echo ${a[2]}  #element three

This will flatten unset elements in sparse arrays.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.