从 Bash 数组中删除元素

我需要在 bash shell 中从数组中删除一个元素。 一般来说,我会这么做:

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

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

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

知道吗?

216889 次浏览

POSIX shell 脚本没有数组。

因此,最有可能的情况是,您正在使用一种特定的方言,如 bash、 korn shell 或 zsh

因此,你的问题现在还无法回答。

也许这对你有用:

unset array[$delete]

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

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

要扩展上述答案,可以使用以下内容从数组中删除多个元素,而不需要进行部分匹配:

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

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

在 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

我要做的是:

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

砰,那个东西被移除了。

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

这是一个快速而简单的解决方案,在简单的情况下可以工作,但是如果(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]-> pippo

fmt有点晦涩: fmt -1在第一列进行了包装(将每个项目放在自己的行中)。这就是空间中物品的问题所在。)fmt -999999将其打开回一行,放回项目之间的空格。还有其他方法可以做到这一点,例如 xargs

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

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

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

从开始删除

从头开始,贪婪的匹配

${ 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?

希望能帮上忙。

实际上,我只是注意到 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+=()语法构造数组的吗?

实际上可以用它添加多个项,即一次添加整个其他数组的内容。

这里有一个(可能是非常特定于 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 样式的 for 循环。我也可以通过 ${!arycopy[@]}访问索引,然后用 tac反转索引,tac是一个 cat,它可以转换输入行的顺序。

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

比如说:

array=(one two three)
array_t=" ${array[@]} "
delete=one
array=(${array_t// $delete / })
unset array_t
#/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

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

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

使用 unset

要删除特定索引处的元素,可以使用 unset,然后复制到另一个数组。只有 unset在这种情况下是不需要的。因为 unset不删除元素,它只是将 null 字符串设置为数组中的特定索引。

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

只是部分回答

删除数组中的第一项

unset 'array[0]'

删除数组中的最后一项

unset 'array[-1]'

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

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

实际上是两个制表符的连接。第一个从索引0到索引1(独占) ,第二个从索引2到结束。

为了避免与使用 unset的数组索引发生冲突-有关更多信息,请参见 https://stackoverflow.com/a/49626928/3223785https://stackoverflow.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/]

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

投票最多的解决方案是(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[@]}")