Supprimer un élément d'un tableau Bash


116

J'ai besoin de supprimer un élément d'un tableau dans le shell bash. En général, je ferais simplement:

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

Malheureusement, l'élément que je souhaite supprimer est une variable, je ne peux donc pas utiliser la commande précédente. Ci-dessous un exemple:

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

Une idée?


Quelle coquille? Votre exemple ressemble à zsh.
chepner

array=( ${array[@]/$delete} )fonctionne comme prévu dans Bash. Avez-vous simplement manqué le =?
Ken Sharp

1
@Ken, ce n'est pas tout à fait ce que l'on veut - il supprimera toutes les correspondances de chaque chaîne et laissera des chaînes vides dans le tableau où elle correspond à la chaîne entière.
Toby Speight

Réponses:


165

Ce qui suit fonctionne comme vous le souhaitez bashet zsh:

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

Si besoin de supprimer plus d'un élément:

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

Caveat

Cette technique supprime en fait les préfixes correspondants $deletedes éléments, pas nécessairement des éléments entiers.

Mettre à jour

Pour vraiment supprimer un élément exact, vous devez parcourir le tableau, comparer la cible à chaque élément et utiliser unsetpour supprimer une correspondance exacte.

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

Notez que si vous faites cela et qu'un ou plusieurs éléments sont supprimés, les indices ne seront plus une séquence continue d'entiers.

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

Le fait est que les tableaux n'ont pas été conçus pour être utilisés en tant que structures de données mutables. Ils sont principalement utilisés pour stocker des listes d'éléments dans une seule variable sans avoir besoin de gaspiller un caractère comme délimiteur (par exemple, pour stocker une liste de chaînes pouvant contenir des espaces).

Si les lacunes sont un problème, vous devez reconstruire la matrice pour combler les lacunes:

for i in "${!array[@]}"; do
    new_array+=( "${array[i]}" )
done
array=("${new_array[@]}")
unset new_array

43
sachez que: $ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}résultatsflower
bernstein

12
Notez que cela fait en fait une substitution, donc si le tableau est quelque chose comme (pluto1 pluto2 pippo)ça, vous vous retrouverez avec (1 2 pippo).
haridsv

5
Faites juste attention à l'utiliser dans une boucle for car vous vous retrouverez avec un élément vide à la place de l'élément supprimé. Pour la raison, vous pouvez faire quelque chose commefor element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Joel B

2
Alors, comment supprimer uniquement les éléments correspondants?
UmaN

4
Remarque: cela peut définir la valeur respective sur rien, mais l'élément sera toujours dans le tableau.
phil294

29

Vous pouvez créer un nouveau tableau sans l'élément indésirable, puis le réattribuer à l'ancien tableau. Cela fonctionne dans bash:

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

Cela donne:

echo "${array[@]}"
pippo

14

C'est le moyen le plus direct d'annuler une valeur si vous connaissez sa position.

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

3
Essayez echo ${array[1]}, vous obtiendrez une chaîne nulle. Et pour y arriver, threevous devez le faire echo ${array[2]}. Ce unsetn'est donc pas le bon mécanisme pour supprimer un élément dans un tableau bash.
rashok

@rashok, non, ${array[1]+x}est une chaîne nulle, donc array[1]non définie. unsetne modifie pas les index des éléments restants. Il n'est pas nécessaire de citer l'argument pour unset. La manière de détruire un élément de tableau est décrite dans le manuel de Bash .
jarno

@rashok Je ne vois pas pourquoi. Vous ne pouvez pas supposer que cela ${array[1]}existe simplement parce que la taille est de 2. Si vous voulez les indices, vérifiez ${!array[@]}.
Daniel C. Sobral le

4

Voici une solution en une ligne avec mapfile:

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

Exemple:

$ 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

Cette méthode permet une grande flexibilité en modifiant / échangeant la commande grep et ne laisse aucune chaîne vide dans le tableau.


1
Veuillez utiliser à la printf '%s\n' "${array[@]}"place de ce moche IFS/ echochose.
gniourf_gniourf

Notez que cela échoue avec les champs qui contiennent des retours à la ligne.
gniourf_gniourf

@Socowi Vous avez tort, du moins sur bash 4.4.19. -d $'\0'fonctionne parfaitement bien alors que juste -dsans l'argument ne fonctionne pas.
Niklas Holm

Ah oui, je l'ai mélangé. Désolé. Ce que je voulais dire, c'est: -d $'\0'est identique -d $'\0 something'ou juste -d ''.
Socowi

Ne fait pas de mal à utiliser $'\0'pour plus de clarté
Niklas Holm

4

Cette réponse est spécifique au cas de la suppression de plusieurs valeurs de grands tableaux, où les performances sont importantes.

Les solutions les plus votées sont (1) la substitution de motif sur un tableau, ou (2) l'itération sur les éléments du tableau. Le premier est rapide, mais ne peut traiter que des éléments qui ont un préfixe distinct, le second a O (n * k), n = taille du tableau, k = éléments à supprimer. Les tableaux associatifs sont une nouvelle fonctionnalité relative et peuvent ne pas avoir été courants lorsque la question a été initialement publiée.

Pour le cas de correspondance exacte, avec n et k grands, il est possible d'améliorer les performances de O (n k) à O (n + k log (k)). En pratique, O (n) en supposant que k bien inférieur à n. La plupart de l'accélération repose sur l'utilisation d'un tableau associatif pour identifier les éléments à supprimer.

Performances (taille n-array, valeurs k à supprimer). Mesure des performances en secondes de temps utilisateur

   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%

Comme prévu, la currentsolution est linéaire à N * K, et la fastsolution est pratiquement linéaire à K, avec une constante beaucoup plus faible. La fastsolution est légèrement plus lente que la currentsolution lorsque k = 1, en raison d'une configuration supplémentaire.

La solution 'Rapide': array = liste des entrées, delete = liste des valeurs à supprimer.

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

Comparé à la currentsolution, d'après la réponse la plus votée.

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

3

Voici une petite fonction (probablement très spécifique à bash) impliquant l'indirection de variable bash et unset; c'est une solution générale qui n'implique pas de substitution de texte ou de suppression d'éléments vides et qui n'a aucun problème avec les guillemets / les espaces, 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

Utilisez-le comme delete_ary_elmt ELEMENT ARRAYNAMEsans aucun $sceau. Commutez le == $wordfor == $word*pour les correspondances de préfixe; utiliser ${elmt,,} == ${word,,}pour les correspondances insensibles à la casse; etc., tout ce que bash [[prend en charge.

Cela fonctionne en déterminant les indices du tableau d'entrée et en les itérant à l'envers (la suppression d'éléments ne gâche donc pas l'ordre d'itération). Pour obtenir les indices, vous devez accéder au tableau d'entrée par nom, ce qui peut être fait via l'indirection de variable bash x=1; varname=x; echo ${!varname} # prints "1".

Vous ne pouvez pas accéder aux tableaux par nom comme aryname=a; echo "${$aryname[@]}, cela vous donne une erreur. Vous ne pouvez pas faire aryname=a; echo "${!aryname[@]}", cela vous donne les indices de la variable aryname(bien que ce ne soit pas un tableau). Ce qui fonctionne aryref="a[@]"; echo "${!aryref}", c'est imprimer les éléments du tableau a, en préservant exactement les guillemets et les espaces blancs echo "${a[@]}". Mais cela ne fonctionne que pour imprimer les éléments d'un tableau, pas pour imprimer sa longueur ou ses indices ( aryref="!a[@]"ou aryref="#a[@]"ou "${!!aryref}"ou "${#!aryref}", ils échouent tous).

Je copie donc le tableau d'origine par son nom via l'indirection bash et récupère les indices de la copie. Pour parcourir les indices en sens inverse, j'utilise une boucle for de style C. Je pourrais aussi le faire en accédant aux index via ${!arycopy[@]}et en les inversant avec tac, ce catqui tourne autour de l'ordre des lignes d'entrée.

Une solution de fonction sans indirection variable devrait probablement impliquer eval, ce qui peut ou non être sûr à utiliser dans cette situation (je ne peux pas le dire).


Cela fonctionne presque bien, mais il ne redéclarera pas le tableau initial passé dans la fonction, donc bien que ce tableau initial ait ses valeurs manquantes, ses index sont également corrompus. Cela signifie que le prochain appel que vous faites à delete_ary_elmt sur le même tableau ne fonctionnera pas (ou supprimera les mauvaises choses). Par exemple, après ce que vous avez collé, essayez d'exécuter delete_ary_elmt "d" arraypuis de réimprimer la matrice. Vous verrez que le mauvais élément est supprimé. La suppression du dernier élément ne fonctionnera donc jamais non plus.
Scott

2

Pour développer les réponses ci-dessus, les éléments suivants peuvent être utilisés pour supprimer plusieurs éléments d'un tableau, sans correspondance partielle:

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

Cela se traduira par un tableau contenant: (deux un deux trois trois quatre "un six")



1

Réponse partielle uniquement

Pour supprimer le premier élément du tableau

unset 'array[0]'

Pour supprimer le dernier élément du tableau

unset 'array[-1]'

@gniourf_gniourf il n'est pas nécessaire d'utiliser des guillemets pour l'argument de unset.
jarno

2
@jarno: ces guillemets DOIVENT être utilisés: si vous avez un fichier nommé array0dans le répertoire courant, alors comme array[0]est glob, il sera d'abord développé array0avant la commande unset.
gniourf_gniourf

@gniourf_gniourf vous avez raison. Cela devrait être corrigé dans le manuel de référence de Bash qui dit actuellement "un nom non défini [indice] détruit l'élément du tableau à l'indice d'index".
jarno

1

En utilisant unset

Pour supprimer un élément à un index particulier, nous pouvons utiliser unsetpuis copier dans un autre tableau. Seulement juste unsetn'est pas nécessaire dans ce cas. Comme unsetne supprime pas l'élément, il définit simplement une chaîne nulle sur l'index particulier du tableau.

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 sortie est

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

En utilisant :<idx>

Nous pouvons supprimer un ensemble d'éléments en utilisant :<idx>également. Par exemple, si nous voulons supprimer le 1er élément, nous pouvons utiliser :1comme mentionné ci-dessous.

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 sortie est

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

0

Le script shell POSIX n'a ​​pas de tableaux.

Donc, très probablement, vous utilisez un dialecte spécifique tel que bash, korn shells ou zsh.

Par conséquent, vous ne pouvez pas répondre à votre question pour le moment.

Peut-être que cela fonctionne pour vous:

unset array[$delete]

2
Salut, j'utilise bash shell atm. Et "$ delete" n'est pas la position de l'élément mais la chaîne elle-même. Donc je ne pense pas que "unset" fonctionnera
Alex

0

En fait, je viens de remarquer que la syntaxe du shell a quelque peu un comportement intégré qui permet une reconstruction facile du tableau lorsque, comme posé dans la question, un élément doit être supprimé.

# 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

Remarquez comment nous avons construit le tableau en utilisant la x+=()syntaxe de bash ?

Vous pouvez en fait ajouter plus d'un élément avec cela, le contenu d'un tout autre tableau à la fois.


0

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

$ {PARAMETER # PATTERN} # supprimer du début

$ {PARAMETER ## PATTERN} # supprimer du début, correspondance gourmande

$ {PARAMETER% PATTERN} # supprimer de la fin

$ {PARAMETER %% PATTERN} # supprimer de la fin, correspondance gourmande

Afin de faire un élément de suppression complet, vous devez exécuter une commande unset avec une instruction if. Si vous ne vous souciez pas de supprimer les préfixes d'autres variables ou de prendre en charge les espaces dans le tableau, vous pouvez simplement laisser tomber les guillemets et oublier les boucles for.

Consultez l'exemple ci-dessous pour découvrir différentes façons de nettoyer une baie.

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

Production

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

J'espère que cela pourra aider.


0

Dans ZSH, c'est extrêmement simple (notez que cela utilise plus de syntaxe compatible bash que nécessaire lorsque cela est possible pour faciliter la compréhension):

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

Résultats:

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

Désolé, je viens d'essayer. Cela n'a pas fonctionné dans zsh pour un tableau assoziatif
Falk

Cela fonctionne très bien, je viens de le tester (à nouveau). Les choses ne fonctionnent pas pour vous? Veuillez expliquer ce qui n'a pas fonctionné avec autant de détails que possible. Quelle version de ZSH utilisez-vous?
trevorj

0

Il existe également cette syntaxe, par exemple si vous souhaitez supprimer le 2ème élément:

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

qui est en fait la concaténation de 2 onglets. Le premier de l'index 0 à l'index 1 (exclusif) et le second de l'index 2 à la fin.


-1

Ce que je fais c'est:

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

BAM, cet élément est supprimé.


1
Cela casse pour array=('first item' 'second item').
Benjamin W.

-1

C'est une solution rapide et sale qui fonctionnera dans des cas simples mais qui se cassera si (a) il y a des caractères spéciaux regex dans $delete, ou (b) il y a des espaces dans n'importe quel élément. Commençant par:

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

Supprimez toutes les entrées correspondant exactement $delete:

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

résultant en echo $array-> pippo, et en s'assurant que c'est un tableau: echo $array[1]-> pippo

fmtest un peu obscur: fmt -1s'enroule à la première colonne (pour mettre chaque élément sur sa propre ligne. C'est là que le problème se pose avec les éléments dans les espaces.) le fmt -999999déroule en une ligne, en remettant les espaces entre les éléments. Il existe d'autres moyens de le faire, par exemple xargs.

Addendum: Si vous souhaitez supprimer uniquement la première correspondance, utilisez sed, comme décrit ici :

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

-1

Que diriez-vous de quelque chose comme:

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

-1

Pour éviter les conflits avec l' index de tableau à l' aide unset- voir https://stackoverflow.com/a/49626928/3223785 et https://stackoverflow.com/a/47798640/3223785 pour plus d' informations - réattribuer le tableau à lui - même: 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

[Réf.: 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
Pouvez-vous ajouter quelques commentaires ou une description pour nous parler de votre réponse?
Michael
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.