从 Bash 数组中移除一个元素

196
我需要在bash shell中从数组中删除一个元素。 通常我会这样做:
array=("${(@)array:#<element to remove>}")

不幸的是,我想要删除的元素是一个变量,所以我无法使用上一个命令。 以下是一个例子:

array+=(pluto)
array+=(pippo)
delete=(pluto)
array( ${array[@]/$delete} ) -> but clearly doesn't work because of {}

有什么想法吗?


1
哪个shell?你的例子看起来像是zsh - chepner
1
在Bash中,array=( ${array[@]/$delete} )按预期工作。你是否只是错过了= - Ken Sharp
2
@Ken,这不是想要的 - 它会从每个字符串中删除任何匹配项,并在数组中留下空字符串,其中它匹配整个字符串。 - Toby Speight
22个回答

285
以下内容在bashzsh中按照您期望的方式工作:
$ array=(pluto pippo)
$ delete=pluto
$ echo ${array[@]/$delete}
pippo
$ array=( "${array[@]/$delete}" ) #Quotes when working with strings

如果需要删除多个元素:

...
$ delete=(pluto pippo)
for del in ${delete[@]}
do
   array=("${array[@]/$del}") #Quotes when working with strings
done

注意事项

这种技术实际上是从元素中删除与$delete匹配的前缀,而不一定是整个元素。

更新

要真正删除一个确切的项,您需要遍历数组,将目标与每个元素进行比较,并使用unset来删除一个确切的匹配项。

array=(pluto pippo bob)
delete=(pippo)
for target in "${delete[@]}"; do
  for i in "${!array[@]}"; do
    if [[ ${array[i]} = $target ]]; then
      unset 'array[i]'
    fi
  done
done
请注意,如果您这样做,并且其中一个或多个元素被移除,则索引将不再是整数的连续序列。
$ declare -p array
declare -a array=([0]="pluto" [2]="bob")

简单的事实是,数组并不是为可变数据结构设计的。它们主要用于在单个变量中存储项目列表,无需浪费字符作为分隔符(例如,存储可能包含空格的字符串列表)。

如果间隙是一个问题,那么你需要重建数组来填补这些间隙:

for i in "${!array[@]}"; do
    new_array+=( "${array[i]}" )
done
array=("${new_array[@]}")
unset new_array

72
请注意:$ array=(sun sunflower)表示创建一个包含sun sunflower元素的数组。 $ delete=(sun)表示创建一个包含sun元素的数组,称为"delete"。 ${array[@]/$delete}表示从array数组中删除delete数组中的元素,并返回剩余的元素。在这种情况下,删除了sun元素,所以结果是flower - bernstein
19
请注意,这实际上是在进行替换操作,因此如果数组类似于 (pluto1 pluto2 pippo),那么最终会得到 (1 2 pippo) - haridsv
7
在for循环中使用时要小心,因为删除元素后,你将会得到一个空元素。为了保险起见,你可以像下面这样操作:for element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done这段代码会遍历数组中的元素并打印出非空元素,从而避免出现空元素的情况。 - Joel B
10
注意:这可能会将对应的值设置为“无”,但元素仍将保留在数组中。 - phil294
7
为了重新创建数组,因为间隙必须消失,只需执行以下操作:arr=("${arr[@]}") - SOUser
显示剩余14条评论

46

你可以建立一个新数组,没有不需要的元素,然后将其赋值回旧数组。这在bash中有效:


array=(pluto pippo)
new_array=()
for value in "${array[@]}"
do
    [[ $value != pluto ]] && new_array+=($value)
done
array=("${new_array[@]}")
unset new_array

这将产生:

echo "${array[@]}"
pippo

26

如果您知道值的位置,这是取消设置值的最直接方法。

$ array=(one two three)
$ echo ${#array[@]}
3
$ unset 'array[1]'
$ echo ${array[@]}
one three
$ echo ${#array[@]}
2

6
尝试输入echo ${array[1]},你会得到空字符串。要得到three,你需要输入echo ${array[2]}。因此,在 Bash 数组中,unset 不是正确的删除元素机制。 - rashok
@rashok,${array[1]+x}是空字符串,所以array[1]未设置。 unset不会更改其余元素的索引。不需要为unset引用参数。销毁数组元素的方法在Bash手册中有描述。 - jarno
1
@rashok 我不认为有什么问题。你不能假设 ${array[1]} 存在,只因为数组大小是2。如果你想要索引,检查 ${!array[@]} - Daniel C. Sobral
3
你可以通过以下方式更新/刷新索引: array=(${array[*]}) - FullStack Alex
如果索引是需要扩展的变量,则需要引用unset _is_参数。(这不是数组特定的事情,只是处理扩展顺序。)我意识到这不是给定示例的一部分;但是,为了未来的读者而提及它。 - Ti Strga
@FullStackAlex 这会破坏包含shell元字符或$IFS中分隔符的成员。重新索引一个稀疏的、按数字索引的数组并保留原始值的通用方法是 array=("${array[@]}") - Walf

10
本回答特定于从大型数组中删除多个值的情况,其中性能很重要。

最受欢迎的解决方案是(1)对数组进行模式替换,或者(2)遍历数组元素。第一种方法速度快,但只能处理具有不同前缀的元素,第二种方法的复杂度为O(n*k),n为数组大小,k为要删除的元素数量。关联数组是相对较新的功能,在问题最初发布时可能并不常见。

对于精确匹配的情况,当n和k很大时,可以将性能从O(n*k)提高到O(n+k*log(k))。实际上,假设k远小于n,则O(n)更可取。大部分加速都基于使用关联数组来识别要删除的项目。

性能(n-数组大小,k-要删除的值)。性能以用户时间的秒数衡量。

   N     K     New(seconds) Current(seconds)  Speedup
 1000   10     0.005        0.033             6X
10000   10     0.070        0.348             5X
10000   20     0.070        0.656             9X
10000    1     0.043        0.050             -7%

如预期,current 解决方案的时间复杂度为 N*K 线性,而 fast 解决方案在 K 的实际线性情况下具有更低的常数。当 k=1 时,由于额外设置的原因,fast 解决方案稍微比 current 解决方案慢一些。

'Fast' 解决方案:array=输入列表,delete=要删除的值列表。

        declare -A delk
        for del in "${delete[@]}" ; do delk[$del]=1 ; done
                # Tag items to remove, based on
        for k in "${!array[@]}" ; do
                [ "${delk[${array[$k]}]-}" ] && unset 'array[k]'
        done
                # Compaction
        array=("${array[@]}")

与最高票答案中的 current 解决方案进行基准测试。

    for target in "${delete[@]}"; do
        for i in "${!array[@]}"; do
            if [[ ${array[i]} = $target ]]; then
                unset 'array[i]'
            fi
        done
    done
    array=("${array[@]}")

8

以下是使用mapfile的一行代码解决方案:

$ mapfile -d $'\0' -t arr < <(printf '%s\0' "${arr[@]}" | grep -Pzv "<regexp>")

例子:

$ arr=("Adam" "Bob" "Claire"$'\n'"Smith" "David" "Eve" "Fred")

$ echo "Size: ${#arr[*]} Contents: ${arr[*]}"

Size: 6 Contents: Adam Bob Claire
Smith David Eve Fred

$ mapfile -d $'\0' -t arr < <(printf '%s\0' "${arr[@]}" | grep -Pzv "^Claire\nSmith$")

$ echo "Size: ${#arr[*]} Contents: ${arr[*]}"

Size: 5 Contents: Adam Bob David Eve Fred

这种方法允许通过修改/交换grep命令来实现很大的灵活性,并且不会在数组中留下任何空字符串。


1
请使用 printf '%s\n' "${array[@]}" 代替那个丑陋的 IFS/echo 方法。 - gniourf_gniourf
请注意,此方法无法处理包含换行符的字段。 - gniourf_gniourf
@Socowi,你错了,至少在bash 4.4.19上是这样的。-d $'\0' 完全正常工作,而没有参数的 -d 则不行。 - Niklas Holm
啊,我搞混了。抱歉。我的意思是:-d $'\0'-d $'\0 something'或者只有-d ''是一样的。 - Socowi
使用 $'\0' 更加清晰,但并不会有什么影响。 - Niklas Holm
第一个-P不符合POSIX标准(也不支持BSD grep),请考虑改用基本正则表达式而非扩展正则表达式的-E。其次,尽管这种方式更加灵活,但以下命令支持另一个数组删除项的情况,就像其他答案中那样: mapfile -d $'\\0' -t arr2 < <(printf '%s\\0' "${arr[@]}" | grep -Ezvw "${delete[@]/#/-e}") - Joshua Skrzypek

5

仅提供部分答案

要删除数组中的第一个项

unset 'array[0]'

删除数组中的最后一项。
unset 'array[-1]'

@gniourf_gniourf,使用unset的参数时不需要使用引号。 - jarno
7
如果当前目录下有一个名为array0的文件,由于array[0]是通配符,因此在执行unset命令之前,它将首先被扩展为array0。请一定使用以下引用:@jarno: these quotes MUST be used: if you have a file named array0 in the current directory, then since array[0] is glob, it will first be expanded to array0 before the unset command. - gniourf_gniourf
@gniourf_gniourf 你是正确的。这应该在Bash参考手册中进行更正,目前它说“unset name[subscript]会破坏索引为subscript的数组元素”。 - jarno

3
这是一个(可能非常与bash有关的)使用bash变量间接引用和unset的小函数;它是一种通用解决方案,不涉及文本替换或丢弃空元素,并且在引用/空格等方面没有任何问题。
delete_ary_elmt() {
  local word=$1      # the element to search for & delete
  local aryref="$2[@]" # a necessary step since '${!$2[@]}' is a syntax error
  local arycopy=("${!aryref}") # create a copy of the input array
  local status=1
  for (( i = ${#arycopy[@]} - 1; i >= 0; i-- )); do # iterate over indices backwards
    elmt=${arycopy[$i]}
    [[ $elmt == $word ]] && unset "$2[$i]" && status=0 # unset matching elmts in orig. ary
  done
  return $status # return 0 if something was deleted; 1 if not
}

array=(a 0 0 b 0 0 0 c 0 d e 0 0 0)
delete_ary_elmt 0 array
for e in "${array[@]}"; do
  echo "$e"
done

# prints "a" "b" "c" "d" in lines

使用delete_ary_elmt ELEMENT ARRAYNAME这样的方式使用它,不需要任何$符号。对于前缀匹配,请将== $word替换为== $word*; 对于不区分大小写的匹配,请使用${elmt,,} == ${word,,}; 等等,无论是bash[[支持的任何内容。

它的工作原理是确定输入数组的索引,并反向迭代它们(以便删除元素不会破坏迭代顺序)。要获取索引,您需要通过名称访问输入数组,可以通过bash变量间接访问实现x=1; varname=x; echo ${!varname} # prints "1"

不能像aryname=a; echo "${$aryname[@]}那样按名称访问数组,这会导致错误。您不能执行aryname=a; echo "${!aryname[@]}",这将给出变量aryname的索引(虽然它不是数组)。起作用的是aryref="a[@]"; echo "${!aryref}",它将打印数组a的元素,保留shell单词引用和空格,就像echo "${a[@]}"一样。但是,这仅适用于打印数组的元素,而不适用于打印其长度或索引(aryref="!a[@]"aryref="#a[@]""${!!aryref}""${#!aryref}", 它们都失败)。

因此,我通过bash间接引用将原始数组复制到其名称,并从副本中获取索引。要反向迭代索引,我使用C-style for循环。我还可以通过访问索引${!arycopy[@]}并使用tac将它们反转来完成,这是一个将输入行顺序反转的cat

没有变量间接访问的函数解决方案可能需要涉及eval,这在该情况下可能安全也可能不安全(我不能确定)。


1
这几乎完美地运作了,但它没有重新声明传递到函数中的初始数组,因此,虽然该初始数组的值缺失了,但是它的索引也被弄乱了。这意味着您对同一数组进行的下一次delete_ary_elmt调用将无法正常工作(或将删除错误的内容)。例如,在你所粘贴的内容之后,尝试运行delete_ary_elmt "d" array,然后重新打印数组。您会看到错误的元素被删除了。那么删除最后一个元素也永远不会起作用了。 - Scott
我们如何修复Scott指出的问题?我在对delete函数的连续调用中遇到了这个问题。 - hermit.crab

2

使用 unset

如果想要删除数组中的某个元素,我们可以使用 unset,然后将其复制到另一个数组中。但在这种情况下,仅仅使用 unset 是不够的。因为 unset 并没有真正删除该元素,它只是将该索引位置上的值设置为空字符串。

declare -a arr=('aa' 'bb' 'cc' 'dd' 'ee')
unset 'arr[1]'
declare -a arr2=()
i=0
for element in "${arr[@]}"
do
    arr2[$i]=$element
    ((++i))
done
echo "${arr[@]}"
echo "1st val is ${arr[1]}, 2nd val is ${arr[2]}"
echo "${arr2[@]}"
echo "1st val is ${arr2[1]}, 2nd val is ${arr2[2]}"

输出结果为

aa cc dd ee
1st val is , 2nd val is cc
aa cc dd ee
1st val is cc, 2nd val is dd

使用 :<idx>

我们也可以使用:<idx>来删除一些元素。例如,如果我们想删除第一个元素,可以使用:1如下所示。

declare -a arr=('aa' 'bb' 'cc' 'dd' 'ee')
arr2=("${arr[@]:1}")
echo "${arr2[@]}"
echo "1st val is ${arr2[1]}, 2nd val is ${arr2[2]}"

输出结果为

bb cc dd ee
1st val is cc, 2nd val is dd

2

在上面的答案基础上,以下方法可用于从数组中删除多个元素,而不进行部分匹配:

ARRAY=(one two onetwo three four threefour "one six")
TO_REMOVE=(one four)

TEMP_ARRAY=()
for pkg in "${ARRAY[@]}"; do
    for remove in "${TO_REMOVE[@]}"; do
        KEEP=true
        if [[ ${pkg} == ${remove} ]]; then
            KEEP=false
            break
        fi
    done
    if ${KEEP}; then
        TEMP_ARRAY+=(${pkg})
    fi
done
ARRAY=("${TEMP_ARRAY[@]}")
unset TEMP_ARRAY

这将导致一个包含以下内容的数组: (两个 onetwo three threefour "one six")

0

http://wiki.bash-hackers.org/syntax/pe#substring_removal

${PARAMETER#PATTERN} # 从开头删除

${PARAMETER##PATTERN} # 从开头删除,贪婪匹配

${PARAMETER%PATTERN} # 从结尾删除

${PARAMETER%%PATTERN} # 从结尾删除,贪婪匹配

为了完全删除元素,您必须使用if语句执行unset命令。如果您不关心从其他变量中删除前缀或支持数组中的空格,则可以删除引号并忘记for循环。

请参见下面的示例,了解清理数组的几种不同方法。

options=("foo" "bar" "foo" "foobar" "foo bar" "bars" "bar")

# remove bar from the start of each element
options=("${options[@]/#"bar"}")
# options=("foo" "" "foo" "foobar" "foo bar" "s" "")

# remove the complete string "foo" in a for loop
count=${#options[@]}
for ((i = 0; i < count; i++)); do
   if [ "${options[i]}" = "foo" ] ; then
      unset 'options[i]'
   fi
done
# options=(  ""   "foobar" "foo bar" "s" "")

# remove empty options
# note the count variable can't be recalculated easily on a sparse array
for ((i = 0; i < count; i++)); do
   # echo "Element $i: '${options[i]}'"
   if [ -z "${options[i]}" ] ; then
      unset 'options[i]'
   fi
done
# options=("foobar" "foo bar" "s")

# list them with select
echo "Choose an option:"
PS3='Option? '
select i in "${options[@]}" Quit
 do
    case $i in 
       Quit) break ;;
       *) echo "You selected \"$i\"" ;;
    esac
 done

输出

Choose an option:
1) foobar
2) foo bar
3) s
4) Quit
Option? 

希望这有所帮助。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接