从 Bash 数组中删除一个元素

Posted

技术标签:

【中文标题】从 Bash 数组中删除一个元素【英文标题】:Remove an element from a Bash array 【发布时间】:2013-05-27 11:41:21 【问题描述】:

我需要从 bash shell 中的数组中删除一个元素。 通常我会这样做:

array=("$(@)array:#<element to remove>")

不幸的是,我要删除的元素是一个变量,所以我不能使用前面的命令。 下面是一个例子:

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

有什么想法吗?

【问题讨论】:

哪个外壳?你的例子看起来像zsh array=( $array[@]/$delete ) 在 Bash 中按预期工作。您是否只是错过了= @Ken,这不是我们想要的 - 它会从每个字符串中删除任何匹配项,并在匹配整个字符串的数组中留下空字符串。 【参考方案1】:

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

【讨论】:

只知道:$ array=(sun sunflower) $ delete=(sun) $ echo $array[@]/$delete 导致flower 请注意,这实际上是在进行替换,因此如果数组类似于(pluto1 pluto2 pippo),那么您最终将得到(1 2 pippo) 在 for 循环中使用它时要小心,因为您最终会在删除元素所在的位置得到一个空元素。为了理智,你可以做类似for element in "$array[@]" do if [[ $element ]]; then echo $element fi done 注意:这可能会将相应的值设置为空,但该元素仍将在数组中。 为了重新创建数组,因为间隙必须消失,以下就足够了:arr=("$arr[@]")【参考方案2】:

您可以构建一个没有不需要的元素的新数组,然后将其分配回旧数组。这适用于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

【讨论】:

【参考方案3】:

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

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

【讨论】:

试试echo $array[1],你会得到空字符串。要获得three,您需要执行echo $array[2]。所以unset 不是删除 bash 数组中元素的正确机制。 @rashok,不,$array[1]+x 是空字符串,所以 array[1] 未设置。 unset 不会更改剩余元素的索引。不需要引用 unset 的参数。 Bash manual 中描述了销毁数组元素的方法。 @rashok 我不明白为什么不这样做。您不能仅仅因为大小为 2 而假设 $array[1] 存在。如果您想要索引,请检查 $!array[@] 您可以通过以下方式更新/刷新索引:array=($array[*])【参考方案4】:

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

投票最多的解决方案是 (1) 数组上的模式替换,或 (2) 迭代数组元素。第一个速度很快,但只能处理具有不同前缀的元素,第二个有 O(n*k),n=数组大小,k=要删除的元素。关联数组是相对较新的功能,在最初发布问题时可能并不常见。

对于完全匹配的情况,在 n 和 k 较大的情况下,可以将性能从 O(nk) 提高到 O(n+klog(k))。在实践中,O(n) 假设 k 远低于 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 解决方案稍慢。

“快速”解决方案: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[@]")

【讨论】:

【参考方案5】:

这是一个使用 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 命令具有很大的灵活性,并且不会在数组中留下任何空字符串。

【讨论】:

请使用printf '%s\n' "$array[@]",而不是那个丑陋的IFS/echo 请注意,包含换行符的字段会失败。 @Socowi 你是不正确的,至少在 bash 4.4.19 上。 -d $'\0' 工作得很好,而没有参数的 -d 则不行。 啊,是的,我把它弄混了。对不起。我的意思是:-d $'\0'-d $'\0 something' 相同或只是-d '' 虽然为了清楚起见,使用$'\0' 并没有什么坏处【参考方案6】:

只回答部分问题

删除数组中的第一项

unset 'array[0]'

删除数组中的最后一项

unset 'array[-1]'

【讨论】:

@gniourf_gniourf unset 的参数不需要使用引号。 @jarno:必须使用这些引号:如果您在当前目录中有一个名为array0 的文件,那么由于array[0] 是glob,它将首先扩展为array0,然后取消设置命令。 @gniourf_gniourf 你是对的。这应该在Bash Reference Manual 中更正,目前说“未设置名称[下标] 会破坏索引下标处的数组元素”。【参考方案7】:

这是一个(可能是 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-word 引用和空格,就像echo "$a[@]" 一样。但这仅适用于打印数组的元素,不适用于打印其长度或索引(aryref="!a[@]"aryref="#a[@]""$!!aryref""$#!aryref",它们都失败了)。

所以我通过 bash 间接复制原始数组的名称并从副本中获取索引。为了反向迭代索引,我使用了 C 风格的 for 循环。我也可以通过$!arycopy[@] 访问索引并使用tac 反转它们,这是一个cat,它可以改变输入行顺序。

没有变量间接的函数解决方案可能必须涉及eval,在那种情况下使用它可能安全也可能不安全(我不知道)。

【讨论】:

这几乎可以很好地工作,但是它不会重新声明传递给函数的初始数组,因此虽然该初始数组的值丢失了,但它的索引也混乱了。这意味着您在同一数组上对 delete_ary_elmt 进行的下一次调用将不起作用(或将删除错误的内容)。例如,粘贴后,尝试运行delete_ary_elmt "d" array,然后重新打印数组。您将看到错误的元素被删除。删除最后一个元素也将永远不会起作用。【参考方案8】:

为了扩展上述答案,可以使用以下方法从数组中删除多个元素,而无需部分匹配:

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

这将产生一个包含以下内容的数组: (二一二三三四“一六”)

【讨论】:

【参考方案9】:

如果有人发现自己处于需要记住 set -e 或 set -x 值并能够恢复它们的位置,请查看使用第一个数组删除解决方案来管理其自己的堆栈的这个要点:

https://gist.github.com/kigster/94799325e39d2a227ef89676eed44cc6

【讨论】:

【参考方案10】:

使用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

使用:&lt;idx&gt;

我们也可以使用:&lt;idx&gt; 删除一些元素集。例如,如果我们想删除第一个元素,我们可以使用: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

【讨论】:

【参考方案11】:

POSIX shell 脚本没有数组。

所以很可能您使用的是特定方言,例如 bash、korn shells 或 zsh

因此,您的问题目前无法回答。

也许这对你有用:

unset array[$delete]

【讨论】:

嗨,我正在使用 bash shell atm。而“$delete”不是元素的位置,而是字符串本身。所以我不认为“未设置”会起作用【参考方案12】:

实际上,我只是注意到,shell 语法在某种程度上具有内置行为,当问题中提出的应删除项目时,该行为允许轻松重建数组。

# let's set up an array of items to consume:
x=()
for (( i=0; i<10; i++ )); do
    x+=("$i")
done

# here, we consume that array:
while (( $#x[@] )); do
    i=$(( $RANDOM % $#x[@] ))
    echo "$x[i] / $x[@]"
    x=("$x[@]:0:i" "$x[@]:i+1")
done

注意我们是如何使用 bash 的 x+=() 语法构造数组的?

您实际上可以同时添加多个项目,即整个其他数组的内容。

【讨论】:

【参考方案13】:

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? 

希望对您有所帮助。

【讨论】:

【参考方案14】:

在 ZSH 中,这非常容易(请注意,为了便于理解,这使用了比必要的更多的 bash 兼容语法):

# I always include an edge case to make sure each element
# is not being word split.
start=(one two three 'four 4' five)
work=($(@)start)

idx=2
val=$work[idx]

# How to remove a single element easily.
# Also works for associative arrays (at least in zsh)
work[$idx]=()

echo "Array size went down by one: "
[[ $#work -eq $(($#start - 1)) ]] && echo "OK"

echo "Array item "$val" is now gone: "
[[ -z $work[(r)$val] ]] && echo OK

echo "Array contents are as expected: "
wanted=("$start[@]:0:1" "$start[@]:2")
[[ "$(j.:.)wanted[@]" == "$(j.:.)work[@]" ]] && echo "OK"

echo "-- array contents: start --"
print -l -r -- "-- $#start elements" $(@)start
echo "-- array contents: work --"
print -l -r -- "-- $#work elements" "$work[@]"

结果:

Array size went down by one:
OK
Array item two is now gone:
OK
Array contents are as expected:
OK
-- array contents: start --
-- 5 elements
one
two
three
four 4
five
-- array contents: work --
-- 4 elements
one
three
four 4
five

【讨论】:

抱歉,刚试过。它在 zsh 中不适用于关联数组 它工作得很好,我刚刚测试了它(再次)。事情不适合你?请尽可能详细地解释什么不起作用。你用的是什么 ZSH 版本?【参考方案15】:

也有这种语法,例如如果你想删除第二个元素:

array=("$array[@]:0:1" "$array[@]:2")

这实际上是 2 个选项卡的串联。第一个从索引 0 到索引 1(不包括),第二个从索引 2 到末尾。

【讨论】:

【参考方案16】:

我做的是:

array="$(echo $array | tr ' ' '\n' | sed "/itemtodelete/d")"

BAM,该项目已删除。

【讨论】:

array=('first item' 'second item') 中断。【参考方案17】:

这是一个快速而简单的解决方案,可以在简单的情况下工作,但如果 (a) $delete 中有正则表达式特殊字符,或者 (b) 任何项目中都存在任何空格,则会中断。开始于:

array+=(pluto)
array+=(pippo)
delete=(pluto)

删除与$delete完全匹配的所有条目:

array=(`echo $array | fmt -1 | grep -v "^$delete$" | fmt -999999`)

导致 echo $array -> pippo,并确保它是一个数组: echo $array[1] -> 皮波

fmt 有点晦涩:fmt -1 在第一列换行(将每个项目放在自己的行上。这就是空格中的项目出现问题的地方。)fmt -999999 将其解开回一行,放回项目之间的空间。还有其他方法可以做到这一点,例如xargs

附录:如果您只想删除第一个匹配项,请使用 sed,如 here 所述:

array=(`echo $array | fmt -1 | sed "0,/^$delete$///d;" | fmt -999999`)

【讨论】:

【参考方案18】:

怎么样:

array=(one two three)
array_t=" $array[@] "
delete=one
array=($array_t// $delete / )
unset array_t

【讨论】:

【参考方案19】:

为避免使用 unset 与数组索引发生冲突 - 请参阅 https://***.com/a/49626928/3223785 和 https://***.com/a/47798640/3223785 了解更多信息 - 将数组重新分配给自身:ARRAY_VAR=($ARRAY_VAR[@])

#!/bin/bash

ARRAY_VAR=(0 1 2 3 4 5 6 7 8 9)
unset ARRAY_VAR[5]
unset ARRAY_VAR[4]
ARRAY_VAR=($ARRAY_VAR[@])
echo $ARRAY_VAR[@]
A_LENGTH=$#ARRAY_VAR[*]
for (( i=0; i<=$(( $A_LENGTH -1 )); i++ )) ; do
    echo ""
    echo "INDEX - $i"
    echo "VALUE - $ARRAY_VAR[$i]"
done

exit 0

[参考:https://tecadmin.net/working-with-array-bash-script/]

【讨论】:

【参考方案20】:
#/bin/bash

echo "# define array with six elements"
arr=(zero one two three 'four 4' five)

echo "# unset by index: 0"
unset -v 'arr[0]'
for i in $!arr[*]; do echo "arr[$i]=$arr[$i]"; done

arr_delete_by_content()  # value to delete
        for i in $!arr[*]; do
                [ "$arr[$i]" = "$1" ] && unset -v 'arr[$i]'
        done
        

echo "# unset in global variable where value: three"
arr_delete_by_content three
for i in $!arr[*]; do echo "arr[$i]=$arr[$i]"; done

echo "# rearrange indices"
arr=( "$arr[@]" )
for i in $!arr[*]; do echo "arr[$i]=$arr[$i]"; done

delete_value()  # value arrayelements..., returns array decl.
        local e val=$1; new=(); shift
        for e in "$@"; do [ "$val" != "$e" ] && new+=("$e"); done
        declare -p new|sed 's,^[^=]*=,,'
        

echo "# new array without value: two"
declare -a arr="$(delete_value two "$arr[@]")"
for i in $!arr[*]; do echo "arr[$i]=$arr[$i]"; done

delete_values()  # arraydecl values..., returns array decl. (keeps indices)
        declare -a arr="$1"; local i v; shift
        for v in "$@"; do 
                for i in $!arr[*]; do
                        [ "$v" = "$arr[$i]" ] && unset -v 'arr[$i]'
                done
        done
        declare -p arr|sed 's,^[^=]*=,,'
        
echo "# new array without values: one five (keep indices)"
declare -a arr="$(delete_values "$(declare -p arr|sed 's,^[^=]*=,,')" one five)"
for i in $!arr[*]; do echo "arr[$i]=$arr[$i]"; done

# new array without multiple values and rearranged indices is left to the reader

【讨论】:

您能否添加一些 cmets 或描述来告诉我们您的答案?

以上是关于从 Bash 数组中删除一个元素的主要内容,如果未能解决你的问题,请参考以下文章

如何删除bash中数组的最后一个元素?

如何删除bash数组中的确切元素?

在JAVA中如何从数组中删除一个元素

Bash:awk从数组项中删除括号

为啥bash只将第一个元素附加到数组

C语言如何从数组中删除一个指定元素?