Eliminar un elemento de una matriz Bash

116

Necesito eliminar un elemento de una matriz en bash shell. Generalmente, simplemente haría:

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

Desafortunadamente, el elemento que quiero eliminar es una variable, por lo que no puedo usar el comando anterior. Aquí abajo un ejemplo:

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

¿Alguna idea?

Alex
fuente
¿Qué caparazón? Su ejemplo parece zsh.
chepner
array=( ${array[@]/$delete} )funciona como se esperaba en Bash. ¿Simplemente te has perdido el =?
Ken Sharp
1
@Ken, eso no es exactamente lo que se desea: eliminará las coincidencias de cada cadena y dejará cadenas vacías en la matriz donde coincide con toda la cadena.
Toby Speight

Respuestas:

165

Lo siguiente funciona como le gustaría en bashy zsh:

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

Si necesita eliminar más de un elemento:

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

Consideración

Esta técnica en realidad elimina los prefijos que coinciden $deletecon los elementos, no necesariamente elementos completos.

Actualizar

Para eliminar realmente un elemento exacto, debe recorrer la matriz, comparar el objetivo con cada elemento y usar unsetpara eliminar una coincidencia exacta.

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

Tenga en cuenta que si hace esto y se eliminan uno o más elementos, los índices ya no serán una secuencia continua de números enteros.

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

El simple hecho es que los arreglos no fueron diseñados para usarse como estructuras de datos mutables. Se utilizan principalmente para almacenar listas de elementos en una sola variable sin necesidad de desperdiciar un carácter como delimitador (por ejemplo, para almacenar una lista de cadenas que pueden contener espacios en blanco).

Si los huecos son un problema, entonces necesita reconstruir la matriz para llenar los huecos:

for i in "${!array[@]}"; do
    new_array+=( "${array[i]}" )
done
array=("${new_array[@]}")
unset new_array
chepner
fuente
43
solo sé eso: $ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}resultados enflower
bernstein
12
Tenga en cuenta que esto realmente está haciendo una sustitución, por lo que si la matriz es algo así (pluto1 pluto2 pippo), terminará con (1 2 pippo).
haridsv
5
Solo tenga cuidado al usar esto en un bucle for porque terminará con un elemento vacío donde estaba el elemento eliminado. Para la cordura, podría hacer algo comofor element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Joel B
2
Entonces, ¿cómo eliminar solo los elementos coincidentes?
UmaN
4
Nota: esto puede establecer el valor respectivo en nada, pero el elemento seguirá estando en la matriz.
phil294
29

Puede crear una nueva matriz sin el elemento no deseado y luego volver a asignarla a la matriz anterior. Esto funciona en bash:

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

Esto produce:

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

Esta es la forma más directa de desarmar un valor si conoce su posición.

$ array=(one two three)
$ echo ${#array[@]}
3
$ unset 'array[1]'
$ echo ${array[@]}
one three
$ echo ${#array[@]}
2
signull
fuente
3
Prueba echo ${array[1]}, obtendrás una cadena nula. Y para conseguirlo threetienes que hacerlo echo ${array[2]}. Entonces, unsetno es el mecanismo correcto para eliminar un elemento en la matriz bash.
rashok
@rashok, no, ${array[1]+x}es una cadena nula, por lo que no array[1]está configurado. unsetno cambia los índices de los elementos restantes. No es necesario citar el argumento de unset. La forma de destruir un elemento de matriz se describe en el manual de Bash .
jarno
@rashok No veo por qué no. No puede asumir que ${array[1]}existe solo porque el tamaño es 2. Si desea los índices, marque ${!array[@]}.
Daniel C. Sobral
4

Aquí hay una solución de una línea con mapfile:

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

Ejemplo:

$ 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 una gran flexibilidad al modificar / intercambiar el comando grep y no deja cadenas vacías en la matriz.

Niklas Holm
fuente
1
Por favor, use printf '%s\n' "${array[@]}"en lugar de esa fea IFS/ echocosa.
gniourf_gniourf
Tenga en cuenta que esto falla con los campos que contienen nuevas líneas.
gniourf_gniourf
@Socowi Estás equivocado, al menos en bash 4.4.19. -d $'\0'funciona perfectamente bien mientras que -dsin el argumento no lo hace.
Niklas Holm
Ah, sí, lo confundí. Lo siento. Lo que quise decir fue: -d $'\0'es lo mismo que -d $'\0 something'o simplemente -d ''.
Socowi
Sin $'\0'embargo
Niklas Holm
4

Esta respuesta es específica para el caso de eliminar varios valores de matrices grandes, donde el rendimiento es importante.

Las soluciones más votadas son (1) sustitución de patrones en una matriz, o (2) iterar sobre los elementos de la matriz. El primero es rápido, pero solo puede tratar con elementos que tienen un prefijo distinto, el segundo tiene O (n * k), n = tamaño de matriz, k = elementos para eliminar. La matriz asociativa es una característica relativamente nueva y es posible que no haya sido común cuando se publicó originalmente la pregunta.

Para el caso de coincidencia exacta, con n y k grandes, es posible mejorar el rendimiento de O (n k) a O (n + k log (k)). En la práctica, O (n) asumiendo k mucho menor que n. La mayor parte de la aceleración se basa en el uso de una matriz asociativa para identificar los elementos que se eliminarán.

Rendimiento (tamaño de matriz n, valores k para eliminar). Segundos de medición del rendimiento del tiempo del usuario

   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 era de esperar, la currentsolución es lineal a N * K, y la fastsolución es prácticamente lineal a K, con una constante mucho más baja. La fastsolución es un poco más lenta que la currentsolución cuando k = 1, debido a una configuración adicional.

La solución 'Rápida': matriz = lista de entrada, eliminar = lista de valores para eliminar.

        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 con la currentsolución, a partir de la respuesta más votada.

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

Aquí hay una pequeña función (probablemente muy específica de bash) que involucra direccionamiento indirecto de variables bash y unset; es una solución general que no implica la sustitución de texto ni el descarte de elementos vacíos y no tiene problemas con las citas / espacios en blanco, 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

Úselo como delete_ary_elmt ELEMENT ARRAYNAMEsin ningún $sigilo. Cambie == $wordpor == $word*para coincidencias de prefijo; utilizar ${elmt,,} == ${word,,}para coincidencias que no distinguen entre mayúsculas y minúsculas; etc., lo que sea que [[soporte bash .

Funciona determinando los índices de la matriz de entrada e iterando sobre ellos hacia atrás (por lo que eliminar elementos no arruina el orden de iteración). Para obtener los índices, debe acceder a la matriz de entrada por nombre, lo que se puede hacer a través de la indirección de la variable bash x=1; varname=x; echo ${!varname} # prints "1".

No puede acceder a las matrices por nombre como aryname=a; echo "${$aryname[@]}, esto le da un error. No puede hacerlo aryname=a; echo "${!aryname[@]}", esto le da los índices de la variable aryname(aunque no es una matriz). Lo que SÍ funciona es aryref="a[@]"; echo "${!aryref}"que imprimirá los elementos de la matriz a, conservando las citas de palabras de shell y los espacios en blanco exactamente como echo "${a[@]}". Pero esto sólo funciona para la impresión de los elementos de una matriz, no para la impresión de su longitud o índices ( aryref="!a[@]"o aryref="#a[@]", o "${!!aryref}", o "${#!aryref}", todas fallan).

Así que copio la matriz original por su nombre a través de bash indirección y obtengo los índices de la copia. Para iterar sobre los índices a la inversa, utilizo un bucle for estilo C. También podría hacerlo accediendo a los índices a través de ${!arycopy[@]}e invirtiéndolos con tac, que es un cambio caten el orden de la línea de entrada.

Una solución de función sin indirección variable probablemente tendría que involucrar eval, lo que puede o no ser seguro de usar en esa situación (no puedo decirlo).

Vicepresidente sénior
fuente
Esto casi funciona bien, sin embargo, no vuelve a declarar la matriz inicial pasada a la función, por lo que si bien esa matriz inicial tiene sus valores perdidos, también tiene sus índices desordenados. Lo que esto significa es que la próxima llamada que haga a delete_ary_elmt en la misma matriz no funcionará (o eliminará las cosas incorrectas). Por ejemplo, después de lo que ha pegado, intente ejecutar delete_ary_elmt "d" arrayy luego volver a imprimir la matriz. Verá que se elimina el elemento incorrecto. La eliminación del último elemento tampoco funcionará.
Scott
2

Para ampliar las respuestas anteriores, se puede usar lo siguiente para eliminar varios elementos de una matriz, sin una coincidencia 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

Esto resultará en una matriz que contiene: (dos uno dos tres tres cuatro "uno seis")

Dylan
fuente
1

Solo respuesta parcial

Para eliminar el primer elemento de la matriz

unset 'array[0]'

Para eliminar el último elemento de la matriz

unset 'array[-1]'
consideRatio
fuente
@gniourf_gniourf no es necesario utilizar comillas para el argumento de unset.
jarno
2
@jarno: se DEBEN usar estas comillas: si tiene un archivo nombrado array0en el directorio actual, dado que array[0]es glob, primero se expandirá array0antes del comando unset.
gniourf_gniourf
@gniourf_gniourf tienes razón. Esto debería corregirse en el Manual de referencia de Bash que actualmente dice "el nombre no definido [subíndice] destruye el elemento de la matriz en el subíndice de índice".
jarno
1

Utilizando unset

Para eliminar un elemento en un índice particular, podemos usar unsety luego copiar a otra matriz. unsetEn este caso, solo no se requiere. Debido a unsetque no elimina el elemento, simplemente establece una cadena nula para el índice particular en la 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]}"

La salida es

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

Utilizando :<idx>

Podemos eliminar algún conjunto de elementos usando :<idx>también. Por ejemplo, si queremos eliminar el primer elemento, podemos usarlo :1como se menciona a continuación.

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

La salida es

bb cc dd ee
1st val is cc, 2nd val is dd
rashok
fuente
0

El script de shell POSIX no tiene matrices.

Así que lo más probable es que estés usando un dialecto específico, como bashkorn shells o zsh.

Por lo tanto, su pregunta a partir de ahora no puede tener respuesta.

Quizás esto funcione para ti:

unset array[$delete]
Ha QUIT - Anony-Mousse
fuente
2
Hola, estoy usando bash shell atm. Y "$ delete" no es la posición del elemento sino la cadena en sí. Así que no creo que "unset" funcione
Alex
0

En realidad, acabo de notar que la sintaxis del shell tiene un comportamiento incorporado que permite una fácil reconstrucción de la matriz cuando, como se plantea en la pregunta, se debe eliminar un elemento.

# 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

¿Observa cómo construimos la matriz usando la x+=()sintaxis de bash ?

De hecho, podría agregar más de un elemento con eso, el contenido de una matriz completamente diferente a la vez.

mar77i
fuente
0

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

$ {PARAMETER # PATTERN} # eliminar desde el principio

$ {PARAMETER ## PATTERN} # eliminar desde el principio, coincidencia codiciosa

$ {PARAMETER% PATTERN} # eliminar del final

$ {PARAMETER %% PATTERN} # eliminar del final, coincidencia codiciosa

Para hacer un elemento de eliminación completo, debe hacer un comando de desarmado con una declaración if. Si no le importa eliminar los prefijos de otras variables o admitir espacios en blanco en la matriz, puede soltar las comillas y olvidarse de los bucles for.

Vea el ejemplo a continuación para conocer algunas formas diferentes de limpiar una matriz.

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

Salida

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

Espero que ayude.

phyatt
fuente
0

En ZSH esto es muy fácil (tenga en cuenta que esto usa más sintaxis compatible con bash de la necesaria cuando sea posible para facilitar la comprensión):

# 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
fuente
Lo siento, lo intenté. No funcionó en zsh para una matriz asociada
Falk
Funciona bien, acabo de probarlo (de nuevo). ¿Las cosas no funcionan para ti? Explique lo que no funcionó exactamente con tanto detalle como pueda. ¿Qué versión de ZSH estás usando?
trevorj
0

También existe esta sintaxis, por ejemplo, si desea eliminar el segundo elemento:

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

que es de hecho la concatenación de 2 pestañas. El primero del índice 0 al índice 1 (exclusivo) y el segundo del índice 2 al final.

OphyTe
fuente
-1

Lo que hago es:

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

BAM, ese artículo se elimina.

garfield
fuente
1
Esto se rompe para array=('first item' 'second item').
Benjamin W.
-1

Esta es una solución rápida y sucia que funcionará en casos simples pero se romperá si (a) hay caracteres especiales de expresiones regulares $deleteo (b) hay espacios en todos los elementos. Empezando con:

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

Elimine todas las entradas que coincidan exactamente $delete:

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

resultando en echo $array-> pippo, y asegurándose de que sea una matriz: echo $array[1]-> pippo

fmtes un poco oscuro: se fmt -1ajusta a la primera columna (para poner cada elemento en su propia línea. Ahí es donde surge el problema con los elementos en los espacios). Lo fmt -999999desenvuelve en una línea, volviendo a colocar los espacios entre los elementos. Hay otras formas de hacerlo, como xargs.

Anexo: si desea eliminar solo la primera coincidencia, use sed, como se describe aquí :

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

¿Qué tal algo como:

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

Para evitar conflictos con el uso de índice de matriz unset- ver https://stackoverflow.com/a/49626928/3223785 y https://stackoverflow.com/a/47798640/3223785 para más información - reasignar la matriz a sí mismo: 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
fuente
-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
fuente
1
¿Puede agregar algunos comentarios o una descripción para contarnos su respuesta?
Michael