Rimuove un elemento da un array Bash


116

Devo rimuovere un elemento da un array in bash shell. In genere farei semplicemente:

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

Sfortunatamente l'elemento che voglio rimuovere è una variabile quindi non posso usare il comando precedente. Di seguito un esempio:

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

Qualche idea?


Quale conchiglia? Il tuo esempio sembra zsh.
chepner

array=( ${array[@]/$delete} )funziona come previsto in Bash. Ti sei semplicemente perso il =?
Ken Sharp

1
@Ken, non è proprio quello che si vuole: rimuoverà tutte le corrispondenze da ogni stringa e lascerà stringhe vuote nell'array in cui corrisponde all'intera stringa.
Toby Speight,

Risposte:


165

Il seguente funziona come vorresti in bashe zsh:

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

Se è necessario eliminare più di un elemento:

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

Avvertimento

Questa tecnica rimuove effettivamente i prefissi corrispondenti $deletedagli elementi, non necessariamente gli interi elementi.

Aggiornare

Per rimuovere davvero un elemento esatto, è necessario scorrere l'array, confrontando l'obiettivo con ciascun elemento e utilizzando unsetper eliminare una corrispondenza esatta.

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

Nota che se lo fai e uno o più elementi vengono rimossi, gli indici non saranno più una sequenza continua di numeri interi.

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

Il semplice fatto è che gli array non sono stati progettati per essere utilizzati come strutture dati mutabili. Sono utilizzati principalmente per memorizzare elenchi di elementi in una singola variabile senza dover sprecare un carattere come delimitatore (ad esempio, per memorizzare un elenco di stringhe che possono contenere spazi bianchi).

Se gli spazi sono un problema, è necessario ricostruire l'array per riempire gli spazi vuoti:

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

43
sappi solo che: $ array=(sun sunflower) $ delete=(sun) $ echo ${array[@]/$delete}risultati inflower
bernstein

12
Nota che questo in realtà sta facendo una sostituzione, quindi se l'array è qualcosa del genere, (pluto1 pluto2 pippo)ti ritroverai con (1 2 pippo).
haridsv

5
Fai solo attenzione a usarlo in un ciclo for perché ti ritroverai con un elemento vuoto dove si trovava l'elemento eliminato. Per ragioni di sanità mentale potresti fare qualcosa del tipofor element in "${array[@]}" do if [[ $element ]]; then echo ${element} fi done
Joel B

2
Quindi come eliminare solo gli elementi corrispondenti?
UmaN

4
Nota: questo potrebbe impostare il rispettivo valore su nulla, ma l'elemento sarà ancora nell'array.
phil294

29

È possibile creare un nuovo array senza l'elemento indesiderato, quindi assegnarlo nuovamente al vecchio array. Funziona in bash:

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

Questo produce:

echo "${array[@]}"
pippo

14

Questo è il modo più diretto per annullare l'impostazione di un valore se conosci la sua posizione.

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

3
Prova echo ${array[1]}, otterrai una stringa nulla. E per ottenerlo threedevi fare echo ${array[2]}. Quindi unsetnon è il meccanismo giusto per rimuovere un elemento nell'array bash.
rashok

@rashok, no, ${array[1]+x}è una stringa nulla, quindi array[1]non è impostato. unsetnon modifica gli indici degli elementi rimanenti. Non è necessario citare l'argomento per unset. Il modo per distruggere un elemento dell'array è descritto nel manuale di Bash .
jarno

@rashok non vedo perché no. Non puoi presumere che ${array[1]}esista solo perché la dimensione è 2. Se vuoi gli indici, controlla ${!array[@]}.
Daniel C. Sobral,

4

Ecco una soluzione su una riga con mapfile:

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

Esempio:

$ 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

Questo metodo consente una grande flessibilità modificando / scambiando il comando grep e non lascia stringhe vuote nell'array.


1
Si prega di utilizzare printf '%s\n' "${array[@]}"al posto di quel brutto IFS/ echocosa.
gniourf_gniourf

Notare che ciò non riesce con i campi che contengono newline.
gniourf_gniourf

@Socowi Hai sbagliato, almeno su bash 4.4.19. -d $'\0'funziona perfettamente bene mentre solo -dsenza l'argomento non lo fa.
Niklas Holm

Ah sì, l'ho confuso. Scusate. Quello che volevo dire era: -d $'\0'è lo stesso -d $'\0 something'o solo -d ''.
Socowi

Non fa male da usare $'\0'per la chiarezza però
Niklas Holm

4

Questa risposta è specifica per l'eliminazione di più valori da array di grandi dimensioni, in cui le prestazioni sono importanti.

Le soluzioni più votate sono (1) la sostituzione di pattern su un array o (2) l'iterazione sugli elementi dell'array. Il primo è veloce, ma può trattare solo elementi che hanno prefissi distinti, il secondo ha O (n * k), n = dimensione dell'array, k = elementi da rimuovere. Gli array associativi sono una nuova funzionalità relativa e potrebbero non essere comuni quando la domanda è stata originariamente pubblicata.

Per il caso di corrispondenza esatta, con n e k grandi, è possibile migliorare le prestazioni da O (n k) a O (n + k log (k)). In pratica, O (n) assumendo k molto inferiore a n. La maggior parte della velocità si basa sull'utilizzo di array associativi per identificare gli elementi da rimuovere.

Prestazioni (dimensione n-array, valori k da eliminare). Le prestazioni misurano i secondi del tempo dell'utente

   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%

Come previsto, la currentsoluzione è lineare per N * K e la fastsoluzione è praticamente lineare per K, con una costante molto più bassa. La fastsoluzione è leggermente più lenta rispetto alla currentsoluzione quando k = 1, a causa della configurazione aggiuntiva.

La soluzione "veloce": array = elenco di input, delete = elenco di valori da rimuovere.

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

Benchmark rispetto alla currentsoluzione, dalla risposta più votata.

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

3

Ecco una piccola funzione (probabilmente molto specifica per bash) che coinvolge l'indirizzamento di variabili bash e unset; è una soluzione generale che non implica la sostituzione del testo o l'eliminazione di elementi vuoti e non ha problemi con citazioni / spazi bianchi ecc.

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

Usalo come delete_ary_elmt ELEMENT ARRAYNAMEsenza $sigillo. Cambia il == $wordfor == $word*per le corrispondenze del prefisso; utilizzare ${elmt,,} == ${word,,}per corrispondenze senza distinzione tra maiuscole e minuscole; ecc., qualunque cosa [[supporti bash .

Funziona determinando gli indici dell'array di input e iterando su di essi all'indietro (quindi l'eliminazione degli elementi non rovina l'ordine di iterazione). Per ottenere gli indici è necessario accedere all'array di input in base al nome, operazione che può essere eseguita tramite l'indirizzamento della variabile bashx=1; varname=x; echo ${!varname} # prints "1" .

Non puoi accedere agli array per nome aryname=a; echo "${$aryname[@]}, questo ti dà un errore. Non puoi farlo aryname=a; echo "${!aryname[@]}", questo ti dà gli indici della variabile aryname(sebbene non sia un array). Ciò che funziona è aryref="a[@]"; echo "${!aryref}"che stamperà gli elementi dell'array a, preservando le virgolette delle parole della shell e gli spazi bianchi esattamente come echo "${a[@]}". Ma questo funziona solo per stampare gli elementi di un array, non per stamparne la lunghezza o gli indici ( aryref="!a[@]"o aryref="#a[@]"o "${!!aryref}"o"${#!aryref}" , falliscono tutti).

Quindi copio l'array originale con il suo nome tramite riferimento indiretto bash e ottengo gli indici dalla copia. Per scorrere gli indici al contrario, utilizzo un ciclo for in stile C. Potrei anche farlo accedendo agli indici tramite ${!arycopy[@]}e invertendoli con tac, che è un catche gira intorno all'ordine della riga di input.

Una soluzione di funzione senza l'indirizzamento indiretto delle variabili dovrebbe probabilmente coinvolgere eval, il che può o non può essere sicuro da usare in quella situazione (non posso dirlo).


Funziona quasi bene, tuttavia non dichiara nuovamente l'array iniziale passato alla funzione, quindi mentre l'array iniziale ha i suoi valori mancanti, ha anche i suoi indici incasinati. Ciò significa che la prossima chiamata che fai a delete_ary_elmt sullo stesso array non funzionerà (o rimuoverà le cose sbagliate). Ad esempio, dopo ciò che hai incollato, prova a eseguire delete_ary_elmt "d" arraye quindi a ristampare l'array. Vedrai che l'elemento sbagliato viene rimosso. Anche la rimozione dell'ultimo elemento non funzionerà mai.
Scott

2

Per espandere le risposte precedenti, è possibile utilizzare quanto segue per rimuovere più elementi da un array, senza corrispondenza parziale:

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

Ciò risulterà in un array contenente: (due uno due tre tre quattro "uno sei")



1

Solo risposta parziale

Per eliminare il primo elemento nella matrice

unset 'array[0]'

Per eliminare l'ultimo elemento nella matrice

unset 'array[-1]'

@gniourf_gniourf non è necessario utilizzare virgolette per l'argomento di unset.
jarno

2
@jarno: queste virgolette DEVONO essere usate: se hai un file denominato array0nella directory corrente, allora poiché array[0]è glob, verrà prima espanso array0prima del comando unset.
gniourf_gniourf

@gniourf_gniourf hai ragione. Questo dovrebbe essere corretto nel Bash Reference Manual che attualmente dice "unset name [subscript] distrugge l'elemento array in index subscript".
jarno

1

utilizzando unset

Per rimuovere un elemento in un determinato indice, possiamo usare unsete quindi copiare in un altro array. Solo appena unsetnon è richiesto in questo caso. Poiché unsetnon rimuove l'elemento, imposta semplicemente una stringa nulla sull'indice particolare nell'array.

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

L'output è

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

utilizzando :<idx>

Possiamo rimuovere alcuni set di elementi usando :<idx>anche. Ad esempio, se vogliamo rimuovere il primo elemento possiamo usare :1come indicato di seguito.

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

L'output è

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

0

Lo script della shell POSIX non ha array.

Quindi molto probabilmente stai usando un dialetto specifico come bash, korn shell ozsh .

Pertanto, la tua domanda al momento non può essere risolta.

Forse questo funziona per te:

unset array[$delete]

2
Ciao, sto usando bash shell atm. E "$ delete" non è la posizione dell'elemento ma la stringa stessa. Quindi non penso che "unset" funzionerà
Alex

0

In realtà, ho appena notato che la sintassi della shell ha in qualche modo un comportamento integrato che consente una facile ricostruzione dell'array quando, come posto nella domanda, un elemento dovrebbe essere rimosso.

# 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

Notare come abbiamo costruito l'array usando bash's x+=() sintassi ?

Potresti effettivamente aggiungere più di un elemento con quello, il contenuto di un intero altro array contemporaneamente.


0

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

$ {PARAMETER # PATTERN} # rimuove dall'inizio

$ {PARAMETER ## PATTERN} # rimuove dall'inizio, partita golosa

$ {PARAMETER% PATTERN} # rimuove dalla fine

$ {PARAMETER %% PATTERN} # rimuovi dalla fine, golosa corrispondenza

Per eseguire una rimozione completa dell'elemento, devi eseguire un comando unset con un'istruzione if. Se non ti interessa rimuovere i prefissi da altre variabili o supportare gli spazi bianchi nell'array, puoi semplicemente eliminare le virgolette e dimenticare i cicli for.

Vedere l'esempio di seguito per alcuni modi diversi per pulire un 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

Produzione

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

Spero che aiuti.


0

In ZSH questo è semplicissimo (nota che utilizza una sintassi compatibile con bash più del necessario, ove possibile per facilità di comprensione):

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

risultati:

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

Scusa, ho appena provato. Non ha funzionato in zsh per un array assoziativo
Falk

Funziona perfettamente, l'ho appena provato (di nuovo). Le cose non funzionano per te? Per favore, spiega cosa non ha funzionato esattamente nel modo più dettagliato possibile. Quale versione ZSH stai usando?
trevorj

0

C'è anche questa sintassi, ad esempio se vuoi eliminare il 2 ° elemento:

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

che è in effetti la concatenazione di 2 schede. Il primo dall'indice 0 all'indice 1 (esclusivo) e il secondo dall'indice 2 alla fine.


-1

Quello che faccio è:

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

BAM, quell'elemento viene rimosso.


1
Questo rompe per array=('first item' 'second item').
Benjamin W.

-1

Questa è una soluzione rapida e sporca che funzionerà in casi semplici ma si interromperà se (a) ci sono caratteri speciali regex in $delete, o (b) ci sono spazi in tutti gli elementi. Iniziare con:

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

Elimina tutte le voci che corrispondono esattamente $delete:

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

risultante in echo $array-> pippo, e assicurandoti che sia un array: echo $array[1]-> pippo

fmtè un po 'oscuro: fmt -1avvolge la prima colonna (per mettere ogni elemento sulla propria riga. Ecco dove sorge il problema con gli elementi negli spazi.) lo fmt -999999scarta di nuovo su una riga, rimettendo gli spazi tra gli elementi. Ci sono altri modi per farlo, come xargs.

Addendum: se vuoi eliminare solo la prima corrispondenza, usa sed, come descritto qui :

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

-1

Che ne dici di qualcosa come:

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

-1

Per evitare conflitti con indice di matrice utilizzando unset- vedi https://stackoverflow.com/a/49626928/3223785 e https://stackoverflow.com/a/47798640/3223785 per ulteriori informazioni - riassegnare l'array a se stesso: 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

[Rif .: 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
Puoi aggiungere qualche commento o una descrizione per dirci la tua risposta?
Michael
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.