7

I have an array list in a bash script, and a variable var. I know that $var appears in ${list[@]}, but have no easy way of determining its index. I'd like to remove it from list.

This answer achieves something very close to what I need, except that list retains an empty element where $var once was. Note, e.g.:

$ list=(one two three)
$ var="two"
$ list=( "${list[@]/$var}" )
$ echo ${list[@]}
one three
$ echo ${#list[@]}
3

The same thing happens if I use delete=( "$var" ) and replace $var for $delete in the third line. Also, doing list=( "${list[@]/$var/}" ) makes no difference either. (I'll note that, experimenting with the comment to that answer, I managed to match only whole words using list=( "${list[@]/%$var}" ), omitting the #.)

I also saw this answer proposing a nice trick to keep track of index and use unset, but that is unfeasible in my case. Finally, the same issue also appeared here, except that OP was satisfied with the result and probably didn't run into the problem empty elements create for me later on in my script, when I iterate through list. I tried to negate that problem by using expansion as follows, without any apparent effect:

for item in "${list[@]}"; do
  if [ -n ${item:+'x'} ];then
    ...
  fi
done

It's the same when I do [ ${#item} > 0 ], and I'm running out of ideas. Suggestions?

EDIT:

I have no understanding of why this happens, but @l0b0's comment made me notice something. Using the above preamble, I get:

$ for item in "${list[@]}"; do echo "Here!"; done
Here!
Here!
Here!

but:

$ for item in ${list[@]}; do echo "Here!"; done
Here!
Here!

I'm not sure I can omit the quotes in my script, though, as items are considerably more complicated there (file names and paths, both containing spaces and odd characters).

Community
  • 1
  • 1
Jonathan Y.
  • 526
  • 1
  • 5
  • 13
  • Regarding your edit, I am unable to reproduce it. I get three instances of `Here!` for both runs. – jaypal singh May 05 '14 at 03:08
  • @JS웃 very odd. It's completely reproducible on my machine (running trusty). – Jonathan Y. May 05 '14 at 10:27
  • 1
    The reason your edit gets two instances in the second case is that the list `("one" "" "three")` gets expanded by the shell to `for item in one three` instead of `for item in "one" "" "three"` – David Burström Nov 23 '15 at 12:34
  • @JoshuaGoldberg have you noticed that that question was the first link I provided, then explained why it doesn't suffice? (To wit, the empty element.) – Jonathan Y. Nov 01 '16 at 18:56
  • @JonathanY., I'm very sorry, I did miss that... although it might be more of a limitation of the bulk of the answers there, rather than the question. (I posted a somewhat hacky answer there that doesn't happen to leave the blank slot.) I'm on the fence about whether it would be more helpful to people looking for answers to mark dup or just leave linked. – Joshua Goldberg Nov 01 '16 at 21:24
  • @JoshuaGoldberg (i) the reason I think it matters that this question remain open is that when using a `for ... in` loop would fail on an empty element; (ii) as to linking, marking as duplicate runs the risk of the question being closed; (iii) and finally, I don't think the close mechanism is meant / should be used just to make the link more outright apparent. – Jonathan Y. Nov 01 '16 at 21:32

3 Answers3

8

You can delete an element from existing array though the whole process isn't very straightforward and may appear like a hack.

#!/bin/bash

list=( "one" "two" "three" "four" "five" )
var1="two"
var2="four"

printf "%s\n" "Before:"
for (( i=0; i<${#list[@]}; i++ )); do 
    printf "%s = %s\n" "$i" "${list[i]}"; 
done

for (( i=0; i<${#list[@]}; i++ )); do 
    if [[ ${list[i]} == $var1 || ${list[i]} == $var2 ]]; then
        list=( "${list[@]:0:$i}" "${list[@]:$((i + 1))}" )
        i=$((i - 1))
    fi
done

printf "\n%s\n" "After:"
for (( i=0; i<${#list[@]}; i++ )); do 
    printf "%s = %s\n" "$i" "${list[i]}"; 
done

This script outputs:

Before:
0 = one
1 = two
2 = three
3 = four
4 = five

After:
0 = one
1 = three
2 = five

Key part of the script is:

list=( "${list[@]:0:$i}" "${list[@]:$((i + 1))}" )

Here we re-construct your existing array by specifying the index and length to remove the element from array completely and re-order the indices.

Massey101
  • 346
  • 1
  • 9
jaypal singh
  • 74,723
  • 23
  • 102
  • 147
  • This post is incorrect. If you use `var1="two" var2="three"` it will fail because the indexes are shifted while you're looping over the array. So it will be index 1, remove "two", then go index 2, which is now "four" thus skipping testing three. To correct this i=$((i - 1)) should be inserted in the if statement. – Massey101 Jun 11 '18 at 01:28
7

If you want to delete the array element & shift the indices, you can use answer by l0b0 or JS웃.

However, if you don't want to shift the indices, you can use below script-let: (Particularly useful for associative arrays)

$ list=(one two three)
$ delete_me=two
$ for i in ${!list[@]};do
    if [ "${list[$i]}" == "$delete_me" ]; then
        unset list[$i]
    fi 
done

$ for i in ${!list[@]};do echo "$i = ${list[$i]}"; done
0 = one
2 = three

If you want to shift the indices to make them continuous, re-construct the array as this:

$ list=("${list[@]}")
$ for i in ${!list[@]};do echo "$i = ${list[$i]}"; done
0 = one
1 = three
anishsane
  • 20,270
  • 5
  • 40
  • 73
2

If you want to remove by value and shift the indexes I think you have to create a new array:

list=(one two three)
new_list=() # Not strictly necessary, but added for clarity
var="two"
for item in ${list[@]}
do
    if [ "$item" != "$var" ]
    then
        new_list+=("$item")
    fi
done
list=("${new_list[@]}")
unset new_list

Test:

$ echo "${list[@]}"
one three
$ echo "${#list[@]}"
2
l0b0
  • 55,365
  • 30
  • 138
  • 223
  • Thanks! However, as I might need to go through this process many times in one run, I'd really prefer to avoid this approach if at all possible. (Not that I expect the complexity of what I'm doing to matter in any real way, but there's the principle of the thing :)) – Jonathan Y. May 04 '14 at 23:59