Remova um elemento de uma matriz Bash

116

Preciso remover um elemento de uma matriz no shell bash. Geralmente, eu simplesmente faria:

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

Infelizmente, o elemento que desejo remover é uma variável, então não posso usar o comando anterior. Abaixo, um exemplo:

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

Qualquer ideia?

Alex
fonte
Qual concha? Seu exemplo se parece zsh.
chepner
array=( ${array[@]/$delete} )funciona como esperado no Bash. Você simplesmente perdeu o =?
Ken Sharp
1
@Ken, isso não é exatamente o que se deseja - removerá quaisquer correspondências de cada string e deixará strings vazias no array onde corresponde a toda a string.
Toby Speight

Respostas:

165

O seguinte funciona como você gostaria em bashe zsh:

$ array=(pluto pippo)
$ delete=pluto
$ echo ${array[@]/$delete}
pippo
$ array=( "${array[@]/$delete}" ) #Quotes when working with strings

Se for necessário excluir mais de um elemento:

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

Embargo

Essa técnica, na verdade, remove prefixos correspondentes $deletedos elementos, não necessariamente elementos inteiros.

Atualizar

Para realmente remover um item exato, você precisa percorrer a matriz, comparando o destino com cada elemento e usando unsetpara excluir uma correspondência exata.

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

Observe que se você fizer isso e um ou mais elementos forem removidos, os índices não serão mais uma sequência contínua de inteiros.

$ declare -p array
declare -a array=([0]="pluto" [2]="bob")

O simples fato é que os arrays não foram projetados para serem usados ​​como estruturas de dados mutáveis. Eles são usados ​​principalmente para armazenar listas de itens em uma única variável sem a necessidade de desperdiçar um caractere como delimitador (por exemplo, para armazenar uma lista de strings que pode conter espaços em branco).

Se as lacunas forem um problema, você precisará reconstruir a matriz para preencher as lacunas:

for i in "${!array[@]}"; do
    new_array+=( "${array[i]}" )
done
array=("${new_array[@]}")
unset new_array
chepner
fonte
43
só saiba que: $ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}resulta emflower
bernstein
12
Observe que isso está na verdade fazendo uma substituição, então se o array for algo assim (pluto1 pluto2 pippo), você terminará com (1 2 pippo).
haridsv
5
Apenas tome cuidado ao usar isso em um loop for porque você acabará com um elemento vazio onde estava o elemento excluído. Por sanidade, você poderia fazer algo comofor element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Joel B,
2
Então, como excluir apenas os elementos correspondentes?
UmaN de
4
Observação: isso pode definir o respectivo valor para nada, mas o elemento ainda estará na matriz.
phil294
29

Você poderia construir um novo array sem o elemento indesejado e, em seguida, atribuí-lo de volta ao array antigo. Isso funciona em bash:

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

Isso produz:

echo "${array[@]}"
pippo
Steve Kehlet
fonte
14

Esta é a maneira mais direta de remover a definição de um valor se você souber sua posição.

$ array=(one two three)
$ echo ${#array[@]}
3
$ unset 'array[1]'
$ echo ${array[@]}
one three
$ echo ${#array[@]}
2
Signull
fonte
3
Tente echo ${array[1]}, você obterá string nula. E para obter threevocê precisa fazer echo ${array[2]}. Portanto, unsetnão é o mecanismo certo para remover um elemento do array bash.
Rashok
@rashok, não, ${array[1]+x}é uma string nula, então array[1]não está definida. unsetnão altera os índices dos elementos restantes. Citar o argumento para unset não é necessário. A maneira de destruir um elemento da matriz é descrita no manual do Bash .
jarno de
@rashok Não vejo porque não. Você não pode presumir que ${array[1]}existe apenas porque o tamanho é 2. Se você quiser os índices, verifique ${!array[@]}.
Daniel C. Sobral
4

Aqui está uma solução de uma linha com mapfile:

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

Exemplo:

$ 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

Este método permite grande flexibilidade modificando / trocando o comando grep e não deixa nenhuma string vazia no array.

Niklas Holm
fonte
1
Por favor, use em printf '%s\n' "${array[@]}"vez daquela coisa IFS/ feia echo.
gniourf_gniourf
Observe que isso falha com campos que contêm novas linhas.
gniourf_gniourf de
@Socowi Você está incorreto, pelo menos no bash 4.4.19. -d $'\0'funciona perfeitamente bem, enquanto apenas -dsem o argumento não.
Niklas Holm
Sim, eu confundi tudo. Desculpe. O que eu quis dizer foi: -d $'\0'é igual -d $'\0 something'ou justo -d ''.
Socowi
Não atrapalha usar $'\0'para maior clareza
Niklas Holm
4

Essa resposta é específica para o caso de exclusão de vários valores de grandes arrays, onde o desempenho é importante.

As soluções mais votadas são (1) substituição de padrão em uma matriz ou (2) iteração sobre os elementos da matriz. O primeiro é rápido, mas só pode lidar com elementos que têm prefixo distinto, o segundo tem O (n * k), n = tamanho do array, k = elementos para remover. Matrizes associativas são recursos relativamente novos e podem não ter sido comuns quando a pergunta foi postada originalmente.

Para o caso de correspondência exata, com n e k grandes, é possível melhorar o desempenho de O (n k) para O (n + k log (k)). Na prática, O (n) assumindo k muito menor que n. A maior parte da aceleração é baseada no uso de matriz associativa para identificar os itens a serem removidos.

Desempenho (tamanho da matriz n, valores k a serem excluídos). Mede o desempenho em segundos de tempo do usuário

   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%

Como esperado, a currentsolução é linear para N * K, e a fastsolução é praticamente linear para K, com constante muito menor. A fastsolução é ligeiramente mais lenta em relação aocurrent solução quando k = 1, devido à configuração adicional.

A solução 'Rápida': array = lista de entrada, delete = lista de valores a serem removidos.

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

Comparado com a currentsolução, a partir da resposta mais votada.

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

Aqui está uma pequena função (provavelmente muito específica do bash) envolvendo indireção da variável bash e unset; é uma solução geral que não envolve substituição de texto ou descartar elementos vazios e não tem problemas com citações / espaços em branco etc.

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

Use-o delete_ary_elmt ELEMENT ARRAYNAMEsem qualquer $sigilo. Mude o == $wordfor == $word*para correspondências de prefixo; use ${elmt,,} == ${word,,}para correspondências que não diferenciam maiúsculas de minúsculas; etc., qualquer que seja o [[suporte do bash .

Ele funciona determinando os índices da matriz de entrada e iterando-os de trás para frente (portanto, a exclusão de elementos não atrapalha a ordem da iteração). Para obter os índices, você precisa acessar a matriz de entrada por nome, o que pode ser feito por via indireta da variável bash x=1; varname=x; echo ${!varname} # prints "1".

Você não pode acessar matrizes por nome aryname=a; echo "${$aryname[@]}, como , isso dá um erro. Você não pode fazer aryname=a; echo "${!aryname[@]}", isso dá a você os índices da variável aryname(embora não seja uma matriz). O que FUNCIONA é aryref="a[@]"; echo "${!aryref}", o que imprimirá os elementos do array a, preservando aspas de palavra de shell e espaços em branco exatamente como echo "${a[@]}". Mas isso só funciona para imprimir os elementos de uma matriz, não para imprimir seu comprimento ou índices ( aryref="!a[@]"ou aryref="#a[@]"ou "${!!aryref}"ou"${#!aryref}" , todos falham).

Então, copio o array original por seu nome via indireta bash e obtenho os índices da cópia. Para iterar os índices ao contrário, uso um loop for estilo C. Eu também poderia fazer isso acessando os índices via ${!arycopy[@]}e revertendo-os com tac, que é um catque gira em torno da ordem da linha de entrada.

Uma solução de função sem indireção variável provavelmente teria que envolver eval, o que pode ou não ser seguro para usar nessa situação (não sei dizer).

SVP
fonte
Isso quase funciona bem, mas não redeclama o array inicial passado para a função, portanto, embora esse array inicial tenha seus valores ausentes, ele também tem seus índices bagunçados. O que isso significa é que a próxima chamada que você fizer para delete_ary_elmt no mesmo array não funcionará (ou removerá as coisas erradas). Por exemplo, após o que você colou, tente executar delete_ary_elmt "d" arraye imprimir novamente o array. Você verá que o elemento errado é removido. Remover o último elemento também nunca funcionará.
Scott
2

Para expandir as respostas acima, o seguinte pode ser usado para remover vários elementos de uma matriz, sem correspondência parcial:

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

Isso resultará em uma matriz contendo: (dois um dois três três quatro "um seis")

Dylan
fonte
2

Se alguém se encontrar em uma posição em que precise se lembrar dos valores set -e ou set -x e ser capaz de restaurá-los, verifique esta essência que usa a primeira solução de exclusão de array para gerenciar sua própria pilha:

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

Konstantin Gredeskoul
fonte
1

Resposta parcial apenas

Para deletar o primeiro item da matriz

unset 'array[0]'

Para deletar o último item da matriz

unset 'array[-1]'
consideRatio
fonte
@gniourf_gniourf não há necessidade de usar aspas para o argumento de unset.
jarno
2
@jarno: estas aspas DEVEM ser usadas: se você tiver um arquivo nomeado array0no diretório atual, como array[0]é glob, ele será primeiro expandido para array0antes do comando unset.
gniourf_gniourf
@gniourf_gniourf você está correto. Isso deve ser corrigido no Manual de Referência do Bash que atualmente diz "nome não definido [subscrito] destrói o elemento do array no índice subscrito".
jarno de
1

Usando unset

Para remover um elemento em um índice particular, podemos usar unsete então copiar para outro array. Apenas unsetnão é necessário neste caso. Como unsetnão remove o elemento, ele apenas define a string nula para o índice particular na matriz.

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]}"

A saída é

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

Usando :<idx>

Podemos remover alguns conjuntos de elementos usando :<idx>também. Por exemplo, se quisermos remover o primeiro elemento, podemos usar :1conforme mencionado abaixo.

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

A saída é

bb cc dd ee
1st val is cc, 2nd val is dd
Rashok
fonte
0

O script de shell POSIX não possui matrizes.

Portanto, provavelmente você está usando um dialeto específico, como bashkorn shells ou zsh.

Portanto, sua pergunta a partir de agora não pode ser respondida.

Talvez isso funcione para você:

unset array[$delete]
Tem QUIT - Anony-Mousse
fonte
2
Olá, estou usando o bash shell atm. E "$ delete" não é a posição do elemento, mas a própria string. Então, eu não acho que "não definido" vai funcionar
Alex
0

Na verdade, acabei de notar que a sintaxe do shell tem um comportamento embutido que permite a fácil reconstrução do array quando, como colocado na pergunta, um item deve ser removido.

# 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

Observe como construímos o array usando a x+=()sintaxe do bash ?

Você poderia realmente adicionar mais de um item com isso, o conteúdo de todo um outro array de uma vez.

mar77i
fonte
0

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

$ {PARAMETER # PATTERN} # remover do início

$ {PARAMETER ## PATTERN} # remove do início, correspondência gananciosa

$ {PARAMETER% PATTERN} # remover do final

$ {PARAMETER %% PATTERN} # remove do final, correspondência gananciosa

Para fazer um elemento de remoção completo, você deve executar um comando unset com uma instrução if. Se você não se preocupa em remover prefixos de outras variáveis ​​ou em suportar espaços em branco no array, você pode simplesmente descartar as aspas e esquecer os loops for.

Veja o exemplo abaixo para algumas maneiras diferentes de limpar um array.

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

Resultado

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

Espero que ajude.

Phyatt
fonte
0

No ZSH isso é muito fácil (observe que ele usa uma sintaxe mais compatível com o bash do que o necessário, sempre que possível, para facilitar o entendimento):

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

Resultados:

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
Trevorj
fonte
Desculpe, apenas tentei. Não funcionou em zsh para uma matriz associativa
Falk
Funciona muito bem, eu apenas testei (de novo). As coisas não estão funcionando para você? Explique o que não funcionou exatamente com o máximo de detalhes possível. Qual versão do ZSH você está usando?
trevorj
0

Também existe esta sintaxe, por exemplo, se você deseja excluir o segundo elemento:

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

que é na verdade a concatenação de 2 guias. A primeira do índice 0 ao índice 1 (exclusivo) e a segunda do índice 2 ao final.

OphyTe
fonte
-1

O que eu faço é:

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

BAM, esse item foi removido.

Garfield
fonte
1
Isso quebra para array=('first item' 'second item').
Benjamin W.
-1

Esta é uma solução rápida e suja que funcionará em casos simples, mas será interrompida se (a) houver caracteres especiais regex $deleteou (b) houver qualquer espaço em qualquer item. Começando com:

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

Exclua todas as entradas que correspondam exatamente a $delete:

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

resultando em echo $array-> pippo e certificando-se de que é um array: echo $array[1]-> pippo

fmté um pouco obscuro: fmt -1quebra na primeira coluna (para colocar cada item em sua própria linha. É aí que surge o problema com itens em espaços.) fmt -999999quebra de volta em uma linha, recolocando os espaços entre os itens. Existem outras maneiras de fazer isso, como xargs.

Adendo: se você deseja excluir apenas a primeira correspondência, use sed, conforme descrito aqui :

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

Que tal algo como:

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

Para evitar conflitos com índice de matriz usando unset- ver https://stackoverflow.com/a/49626928/3223785 e https://stackoverflow.com/a/47798640/3223785 para mais informações - transferir a matriz para si: 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

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

Eduardo lucio
fonte
-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
Gombok Arthur
fonte
1
Você pode adicionar alguns comentários ou uma descrição para nos contar sobre sua resposta?
Michael,