从Bash数组中删除元素


116

我需要从bash shell中的数组中删除一个元素。通常,我只是做:

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

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

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

任何想法?


哪个壳?您的示例如下所示zsh
13年

array=( ${array[@]/$delete} )在Bash中按预期工作。您只是错过了=吗?
肯·夏普

1
@Ken,这不是真正想要的-它会删除每个字符串中的所有匹配项,并在与整个字符串匹配的数组中保留空字符串。
Toby Speight,

Answers:


165

您可以在bash和中进行以下操作zsh

$ 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

43
才知道:$ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}结果flower
伯恩斯坦2014年

12
请注意,这实际上是在进行替换,因此,如果数组是类似的东西,(pluto1 pluto2 pippo)则最终会得到(1 2 pippo)
haridsv 2014年

5
请注意在for循环中使用此方法,因为您最终将得到一个空元素,其中已删除元素所在的位置。为了保持理智,您可以执行以下操作for element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Joel B

2
那么如何只删除匹配的元素呢?
UmaN '16

4
注意:这可能会将各自的值设置为空,但是该元素仍将位于数组中。
phil294 '16

29

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

14

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

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

3
试试看echo ${array[1]},您将获得空字符串。为了得到three你需要做的echo ${array[2]}。因此unset,不是删除bash数组中的元素的正确机制。
rashok

@rashok,否,${array[1]+x}为空字符串,因此array[1]未设置。unset不会更改其余元素的索引。不需要将参数引用为unset。在Bash手册中描述了销毁数组元素的方法。
jarno

@rashok我不明白为什么不。您不能${array[1]}仅仅因为大小为2 而假定存在。如果需要索引,请选中${!array[@]}
Daniel C. Sobral

4

这是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没有参数就没有。
尼古拉斯·霍尔姆

是的,我把它混了。抱歉。我的意思是:-d $'\0'-d $'\0 something'或相同-d ''
Socowi

$'\0'不过,为了清晰起见,使用它也不会有伤害
Niklas Holm

4

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

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

对于n和k较大的精确匹配情况,有可能将性能从O(n k)提高到O(n + k log(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呈线性关系,常数要低得多。fastcurrentk = 1时的解决方案相比,该解决方案的速度稍慢一些,这是由于进行了其他设置。

“快速”解决方案:数组=输入列表,删除=要删除的值列表。

        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[@]}")

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没有任何$标志一样使用它。切换== $wordfor == $word*for前缀匹配;使用${elmt,,} == ${word,,}为不区分大小写匹配; 等等,无论bash [[支持什么。

它通过确定输入数组的索引并向后迭代来进行工作(因此删除元素不会破坏迭代顺序)。要获取索引,您需要按名称访问输入数组,这可以通过bash变量indirection来完成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,这可以cat绕过输入行顺序。

没有变量间接寻址的函数解决方案可能必须涉及eval,在这种情况下使用它可能安全,也可能不安全(我无法告诉)。


这几乎可以很好地工作,但是它没有重新声明传递给函数的初始数组,因此尽管该初始数组缺少其值,但其索引也被弄乱了。这意味着您在同一数组上对delete_ary_elmt进行的下一次调用将不起作用(或将删除错误的内容)。例如,粘贴之后,尝试运行delete_ary_elmt "d" array,然后重新打印阵列。您将看到错误的元素被删除。删除最后一个元素也将永远无法进行。
斯科特,

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

这将导致包含以下内容的数组:(两个一个两个三个三个三个四个“一个六个”)



1

仅部分答案

删除数组中的第一项

unset 'array[0]'

删除数组中的最后一项

unset 'array[-1]'

@gniourf_gniourf不需要为参数使用引号unset
jarno

2
@jarno:必须使用这些引号:如果您array0在当前目录中有一个文件名,那么由于array[0]是glob,它将首先被展开为array0unset命令。
gniourf_gniourf

@gniourf_gniourf你是正确的。这应该在Bash参考手册中得到纠正,该手册目前说“未设置名称[下标]破坏了索引下标处的数组元素”。
jarno

1

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

0

POSIX Shell脚本没有数组。

因此,很可能您使用的是特定的方言,例如bashkorn shell或zsh

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

也许这对您有用:

unset array[$delete]

2
嗨,我正在使用bash shell atm。“ $ delete”不是元素的位置,而是字符串本身。所以我认为“ unset”不会起作用
Alex

0

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

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


0

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

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

$ {PARAMETER ## PATTERN}#从开头删除,贪婪的比赛

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

$ {PARAMETER %% PATTERN}#从最后删除,贪婪的比赛

为了执行完整的remove元素,您必须使用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? 

希望有帮助。


0

在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中不起作用
Falk

它工作正常,我刚刚对其进行了测试。事情不适合您吗?请尽可能详细地解释哪些行不通。您正在使用哪个ZSH版本?
trevorj

0

还有这种语法,例如,如果要删除第二个元素:

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

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


-1

我要做的是:

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

BAM,该项目被删除。


1
这打破了array=('first item' 'second item')
本杰明W.

-1

这是一个快捷的解决方案,在简单的情况下可以使用,但是如果(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`)

-1

怎么样:

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

-1

为避免与数组索引冲突,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/ ]


-2
#/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

1
您可以添加一些评论或描述来告诉我们您的答案吗?
迈克尔(Michael)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.