Controlla se un array Bash contiene un valore

In Bash, qual è il modo più semplice per verificare se un array contiene un determinato valore?

Modifica : con l’aiuto delle risposte e dei commenti, dopo alcuni test, mi sono inventato questo:

function contains() { local n=$# local value=${!n} for ((i=1;i < $#;i++)) { if [ "${!i}" == "${value}" ]; then echo "y" return 0 fi } echo "n" return 1 } A=("one" "two" "three four") if [ $(contains "${A[@]}" "one") == "y" ]; then echo "contains one" fi if [ $(contains "${A[@]}" "three") == "y" ]; then echo "contains three" fi 

Non sono sicuro che sia la soluzione migliore, ma sembra funzionare.

C’è un codice di esempio che mostra come sostituire una sottostringa da un array . È ansible creare una copia dell’array e provare a rimuovere il valore target dalla copia. Se la copia e l’originale sono poi diversi, allora il valore di destinazione esiste nella stringa originale.

La soluzione semplice (ma potenzialmente più dispendiosa in termini di tempo) consiste semplicemente nell’iterare l’intero array e controllare singolarmente ciascun elemento. Questo è quello che faccio in genere perché è facile da implementare e puoi includerlo in una funzione (vedi queste informazioni sul passaggio di una matrice a una funzione ).

Di seguito è una piccola funzione per raggiungere questo. La stringa di ricerca è il primo argomento e il resto sono gli elementi dell’array:

 containsElement () { local e match="$1" shift for e; do [[ "$e" == "$match" ]] && return 0; done return 1 } 

Un test di questa funzione potrebbe essere simile a:

 $ array=("something to search for" "a string" "test2000") $ containsElement "a string" "${array[@]}" $ echo $? 0 $ containsElement "blaha" "${array[@]}" $ echo $? 1 

Questo approccio ha il vantaggio di non dover ricorrere a tutti gli elementi (almeno non esplicitamente). Ma dato che array_to_string_internal() in array.c scorre ancora su elementi di array e li concatena in una stringa, probabilmente non è più efficiente delle soluzioni di looping proposte, ma è più leggibile.

 if [[ " ${array[@]} " =~ " ${value} " ]]; then # whatever you want to do when arr contains value fi if [[ ! " ${array[@]} " =~ " ${value} " ]]; then # whatever you want to do when arr doesn't contain value fi 

Si noti che nei casi in cui il valore che si sta cercando è una delle parole in un elemento dell’array con spazi, darà dei falsi positivi. Per esempio

 array=("Jack Brown") value="Jack" 

La regex vedrà Jack come nell’array anche se non lo è. Quindi dovrai cambiare IFS e i caratteri separatori sulla tua espressione regolare se vuoi ancora usare questa soluzione, come questa

 IFS=$'\t' array=("Jack Brown\tJack Smith") unset IFS value="Jack Smith" if [[ "\t${array[@]}\t" =~ "\t${value}\t" ]]; then echo "yep, it's there" fi 
 $ myarray=(one two three) $ case "${myarray[@]}" in *"two"*) echo "found" ;; esac found 
 for i in "${array[@]}" do if [ "$i" -eq "$yourValue" ] ; then echo "Found" fi done 

Per le stringhe:

 for i in "${array[@]}" do if [ "$i" == "$yourValue" ] ; then echo "Found" fi done 

Di solito uso solo:

 inarray=$(echo ${haystack[@]} | grep -o "needle" | wc -w) 

il valore diverso da zero indica che è stata trovata una corrispondenza.

Se hai bisogno di prestazioni, non vuoi eseguire il ciclo su tutto l’array ogni volta che esegui una ricerca.

In questo caso, è ansible creare un array associativo (tabella hash o dizionario) che rappresenta un indice di tale matrice. Vale a dire che ogni elemento dell’array viene mappato nel suo indice nella matrice:

 make_index () { local index_name=$1 shift local -a value_array=("$@") local i # -A means associative array, -g means create a global variable: declare -g -A ${index_name} for i in "${!value_array[@]}"; do eval ${index_name}["${value_array[$i]}"]=$i done } 

Quindi puoi usarlo in questo modo:

 myarray=('aa' 'bb' 'c c') make_index myarray_index "${myarray[@]}" 

E prova l’adesione in questo modo:

 member="bb" # the "|| echo NOT FOUND" below is needed if you're using "set -e" test "${myarray_index[$member]}" && echo FOUND || echo NOT FOUND 

O anche:

 if [ "${myarray_index[$member]}" ]; then echo FOUND fi 

Si noti che questa soluzione fa la cosa giusta anche se ci sono spazi nel valore testato o nei valori dell’array.

Come bonus, ottieni anche l’indice del valore all’interno dell’array con:

 echo "< < ${myarray_index[$member]} >> is the index of $member" 

Ecco un piccolo contributo:

 array=(word "two words" words) search_string="two" match=$(echo "${array[@]:0}" | grep -o $search_string) [[ ! -z $match ]] && echo "found !" 

Nota: in questo modo non si distingue il caso “due parole”, ma questo non è richiesto nella domanda.

 containsElement () { for e in "${@:2}"; do [[ "$e" = "$1" ]] && return 0; done; return 1; } 

Ora gestisce correttamente gli array vuoti.

Un altro rivestimento senza funzione:

 (for e in "${array[@]}"; do [[ "$e" == "searched_item" ]] && exit 0; done) && echo found || not found 

Grazie a @Qwerty per l’idea degli spazi!

funzione corrispondente:

 find_in_array() { local word=$1 shift for e in "$@"; do [[ "$e" == "$word" ]] && return 0; done } 

esempio:

 some_words=( these are some words ) find_in_array word "${some_words[@]}" || echo "expected missing! since words != word" 

Se vuoi fare un test rapido e sporco per vedere se vale la pena di iterare sull’intero array per ottenere una corrispondenza precisa, Bash può trattare array come scalari. Provare una corrispondenza nello scalare, se nessuno, quindi saltare il ciclo, fa risparmiare tempo. Ovviamente puoi ottenere falsi positivi.

 array=(word "two words" words) if [[ ${array[@]} =~ words ]] then echo "Checking" for element in "${array[@]}" do if [[ $element == "words" ]] then echo "Match" fi done fi 

Questo produrrà “Checking” e “Match”. Con array=(word "two words" something) uscirà solo “Controllo”. Con array=(word "two widgets" something) non ci sarà output.

 a=(bcd) if printf '%s\0' "${a[@]}" | grep -Fqxz c then echo 'array “a” contains value “c”' fi 

Se preferisci puoi utilizzare opzioni lunghe equivalenti:

 --fixed-strings --quiet --line-regexp --null-data 

Questo funziona per me:

 # traditional system call return values-- used in an `if`, this will be true when returning 0. Very Odd. contains () { # odd syntax here for passing array parameters: http://stackoverflow.com/questions/8082947/how-to-pass-an-array-to-a-bash-function local list=$1[@] local elem=$2 # echo "list" ${!list} # echo "elem" $elem for i in "${!list}" do # echo "Checking to see if" "$i" "is the same as" "${elem}" if [ "$i" == "${elem}" ] ; then # echo "$i" "was the same as" "${elem}" return 0 fi done # echo "Could not find element" return 1 } 

Chiamata di esempio:

 arr=("abc" "xyz" "123") if contains arr "abcx"; then echo "Yes" else echo "No" fi 

dato:

 array=("something to search for" "a string" "test2000") elem="a string" 

quindi un semplice controllo di:

 if c=$'\x1E' && p="${c}${elem} ${c}" && [[ ! "${array[@]/#/${c}} ${c}" =~ $p ]]; then echo "$elem exists in array" fi 

dove

 c is element separator p is regex pattern 

(La ragione per assegnare p separatamente, piuttosto che usare l’espressione direttamente all’interno [[]] è di mantenere la compatibilità per bash 4)

Generalmente scrivo questo tipo di utilità per operare sul nome della variabile, piuttosto che sul valore della variabile, principalmente perché bash non può altrimenti passare le variabili per riferimento.

Ecco una versione che funziona con il nome dell’array:

 function array_contains # array value { [[ -n "$1" && -n "$2" ]] || { echo "usage: array_contains  " echo "Returns 0 if array contains value, 1 otherwise" return 2 } eval 'local values=("${'$1'[@]}")' local element for element in "${values[@]}"; do [[ "$element" == "$2" ]] && return 0 done return 1 } 

Con questo, l’esempio di domanda diventa:

 array_contains A "one" && echo "contains one" 

eccetera.

Utilizzando grep e printf

Formattare ciascun membro dell’array su una nuova riga, quindi grep il grep delle righe.

 if printf '%s\n' "${array[@]}" | grep -x -q "search string"; then echo true; else echo false; fi 

esempio:

 $ array=("word", "two words") $ if printf '%s\n' "${array[@]}" | grep -x -q "two words"; then echo true; else echo false; fi true 

Si noti che questo non ha problemi con delimitatori e spazi.

Semplice se non ti dispiace le espressioni regolari:

 printf '%s\n' ${myarray[@]} | grep -P '^mypattern$' 

L’istruzione printf stampa ogni elemento dell’array su una riga separata.

L’istruzione grep usa i caratteri speciali ^ e $ per trovare una linea che contenga esattamente il modello dato il mio mypattern .

Dopo aver risposto, ho letto un’altra risposta che mi è particolarmente piaciuta, ma era imperfetta e downvoted. Mi sono ispirato e qui ci sono due nuovi approcci che vedo fattibili.

 array=("word" "two words") # let's look for "two words" 

usando grep e printf :

 (printf '%s\n' "${array[@]}" | grep -x -q "two words") &&  

usando for :

 (for e in "${array[@]}"; do [[ "$e" == "two words" ]] && exit 0; done; exit 1) &&  

Per i risultati not_found aggiungi || ||

Ecco la mia opinione su questo.

Preferirei non usare un bash per il loop se posso evitarlo, perché ci vuole del tempo per correre. Se qualcosa deve fare un ciclo, lascia che sia qualcosa che è stato scritto in un linguaggio di livello inferiore rispetto a uno script di shell.

 function array_contains { # arrayname value local -A _arr=() local IFS= eval _arr=( $(eval printf '[%q]="1"\ ' "\${$1[@]}") ) return $(( 1 - 0${_arr[$2]} )) } 

Questo funziona creando un array associativo temporaneo, _arr , i cui indici derivano dai valori dell’array di input. (Si noti che gli array associativi sono disponibili in bash 4 e versioni successive, quindi questa funzione non funzionerà nelle versioni precedenti di bash.) $IFS per evitare la divisione delle parole su spazi vuoti.

La funzione non contiene cicli espliciti, sebbene passi internamente bash attraverso l’array di input per popolare printf . Il formato printf utilizza %q per garantire che i dati di input siano sfuggiti in modo tale che possano essere tranquillamente utilizzati come chiavi di array.

 $ a=("one two" three four) $ array_contains a three && echo BOOYA BOOYA $ array_contains a two && echo FAIL $ 

Nota che tutto ciò che questa funzione utilizza è un built-in per bash, quindi non ci sono pipe esterne che ti trascinano giù, anche nell’espansione dei comandi.

E se non ti piace usare eval … beh, sei libero di usare un altro approccio. 🙂

Combinando alcune delle idee presentate qui puoi creare un’elegante statica senza loop che fa corrispondere esattamente le parole .

 $find="myword" $array=(value1 value2 myword) if [[ ! -z $(printf '%s\n' "${array[@]}" | grep -w $find) ]]; then echo "Array contains myword"; fi 

Questo non si innesca su word o val , solo parole intere corrispondono. Si romperà se ogni valore dell’array contiene più parole.

Prendendo in prestito la risposta di Dennis Williamson , la seguente soluzione combina matrici, citazioni prive di shell ed espressioni regolari per evitare la necessità di: iterare su loop; usando tubi o altri sotto processi; o usando utilità non bash.

 declare -a array=('hello, stack' one 'two words' words last) printf -v array_str -- ',,%q' "${array[@]}" if [[ "${array_str},," =~ ,,words,, ]] then echo 'Matches' else echo "Doesn't match" fi 

Il codice sopra funziona usando le espressioni regolari di Bash per confrontarsi con una versione con stringhe dei contenuti dell’array. Esistono sei passaggi importanti per garantire che la corrispondenza dell’espressione regolare non possa essere ingannata da combinazioni intelligenti di valori all’interno dell’array:

  1. Costruisci la stringa di confronto usando la query di printf incorporata di Bash, %q . Il cinguettamento delle shell garantisce che i caratteri speciali diventino “sicuri per la shell” essendo scappati con la barra rovesciata \ .
  2. Scegli un carattere speciale da utilizzare come delimitatore di valori. Il delimitatore deve essere uno dei caratteri speciali che verranno sfuggiti quando si utilizza %q ; questo è l’unico modo per garantire che i valori all’interno dell’array non possano essere costruiti in modi intelligenti per ingannare la corrispondenza dell’espressione regolare. Scelgo la virgola , perché quel personaggio è il più sicuro quando viene valutato o utilizzato in modo inaspettato.
  3. Combina tutti gli elementi dell’array in una singola stringa, utilizzando due istanze del carattere speciale da utilizzare come delimitatore. Usando virgola come esempio, ho usato ,,%q come argomento per printf . Questo è importante perché due istanze del carattere speciale possono apparire solo una accanto all’altra quando appaiono come delimitatori; tutte le altre istanze del carattere speciale verranno sfuggite.
  4. Aggiungi due istanze finali del delimitatore alla stringa, per consentire le corrispondenze con l’ultimo elemento dell’array. Pertanto, invece di confrontare con ${array_str} , confrontare con ${array_str},, .
  5. Se la stringa di destinazione che stai cercando è fornita da una variabile utente, devi sfuggire a tutte le istanze del carattere speciale con una barra rovesciata. Altrimenti, la corrispondenza delle espressioni regolari diventa vulnerabile a essere ingannati da elementi di matrice abilmente predisposti.
  6. Esegui una corrispondenza di espressioni regolari Bash con la stringa.

Ecco la mia opinione su questo problema. Ecco la versione breve:

 function arrayContains() { local haystack=${!1} local needle="$2" printf "%s\n" ${haystack[@]} | grep -q "^$needle$" } 

E la versione lunga, che penso sia molto più facile per gli occhi.

 # With added utility function. function arrayToLines() { local array=${!1} printf "%s\n" ${array[@]} } function arrayContains() { local haystack=${!1} local needle="$2" arrayToLines haystack[@] | grep -q "^$needle$" } 

Esempi:

 test_arr=("hello" "world") arrayContains test_arr[@] hello; # True arrayContains test_arr[@] world; # True arrayContains test_arr[@] "hello world"; # False arrayContains test_arr[@] "hell"; # False arrayContains test_arr[@] ""; # False 

Avevo il caso che dovessi controllare se un ID fosse contenuto in un elenco di ID generati da un altro script / comando. Per me ha funzionato il seguente:

 # the ID I was looking for ID=1 # somehow generated list of IDs LIST=$(  ) # list is curiously concatenated with a single space character LIST=" $LIST " # grep for exact match, boundaries are marked as space # would therefore not reliably work for values containing a space # return the count with "-c" ISIN=$(echo $LIST | grep -F " $ID " -c) # do your check (eg 0 for nothing found, everything greater than 0 means found) if [ ISIN -eq 0 ]; then echo "not found" fi # etc. 

Potresti anche abbreviare / compattare in questo modo:

 if [ $(echo " $(  

Nel mio caso, stavo eseguendo jq per filtrare alcuni JSON per un elenco di ID e ho dovuto verificare in seguito se il mio ID era in questa lista e questo ha funzionato al meglio per me. Non funzionerà con gli array creati manualmente del tipo LIST=("1" "2" "4") ma con output di script separato a capo nuovo.


PS .: non ho potuto commentare una risposta perché sono relativamente nuova ...

Il seguente codice controlla se un determinato valore è nell’array e restituisce il suo offset basato su zero:

 A=("one" "two" "three four") VALUE="two" if [[ "$(declare -p A)" =~ '['([0-9]+)']="'$VALUE'"' ]];then echo "Found $VALUE at offset ${BASH_REMATCH[1]}" else echo "Couldn't find $VALUE" fi 

La corrispondenza viene eseguita sui valori completi, pertanto l’impostazione di VALUE = “tre” non corrisponde.

Potrebbe valere la pena di indagare se non si desidera eseguire un’iterazione:

 #!/bin/bash myarray=("one" "two" "three"); wanted="two" if `echo ${myarray[@]/"$wanted"/"WAS_FOUND"} | grep -q "WAS_FOUND" ` ; then echo "Value was found" fi exit 

Snippet adattato da: http://www.thegeekstuff.com/2010/06/bash-array-tutorial/ Penso che sia abbastanza intelligente.

EDIT: Probabilmente potresti semplicemente fare:

 if `echo ${myarray[@]} | grep -q "$wanted"` ; then echo "Value was found" fi 

Ma quest’ultimo funziona solo se l’array contiene valori univoci. Cercare 1 in “143” darà falsi positivi, mi pare.

Sebbene ci fossero molte risposte grandiose e utili qui, non ho trovato quella che sembrava essere la giusta combinazione di performante, multipiattaforma e robusta; quindi volevo condividere la soluzione che ho scritto per il mio codice:

 #!/bin/bash # array_contains "$needle" "${haystack[@]}" # # Returns 0 if an item ($1) is contained in an array ($@). # # Developer note: # The use of a delimiter here leaves something to be desired. The ideal # method seems to be to use `grep` with --line-regexp and --null-data, but # Mac/BSD grep doesn't support --line-regexp. function array_contains() { # Extract and remove the needle from $@. local needle="$1" shift # Separates strings in the array for matching. Must be extremely-unlikely # to appear in the input array or the needle. local delimiter='#!-\8/-!#' # Create a string with containing every (delimited) element in the array, # and search it for the needle with grep in fixed-string mode. if printf "${delimiter}%s${delimiter}" "$@" | \ grep --fixed-strings --quiet "${delimiter}${needle}${delimiter}"; then return 0 fi return 1 } 

La mia versione della tecnica delle espressioni regolari che è stata già suggerita:

 values=(foo bar) requestedValue=bar requestedValue=${requestedValue##[[:space:]]} requestedValue=${requestedValue%%[[:space:]]} [[ "${values[@]/#/X-}" =~ "X-${requestedValue}" ]] || echo "Unsupported value" 

Quello che sta succedendo qui è che stai espandendo l’intera serie di valori supportati in parole e prependi una stringa specifica, “X-” in questo caso, a ciascuno di essi, e facendo lo stesso con il valore richiesto. Se questo è effettivamente contenuto nell’array, la stringa risultante combacerà al massimo uno dei token risultanti, o non lo sarà affatto. Nell’ultimo caso il || l’operatore si innesca e sai che hai a che fare con un valore non supportato. Prima di tutto, il valore richiesto viene rimosso da tutti gli spazi bianchi iniziali e finali tramite la manipolazione della stringa di shell standard.

È pulito ed elegante, credo, anche se non sono troppo sicuro di quanto possa essere performante se l’array di valori supportati è particolarmente ampio.

Espandendo la risposta di cui sopra da Sean DiSanti, penso che la seguente sia una soluzione semplice ed elegante che evita di dover ricorrere all’array e non dare falsi positivi a causa di corrispondenze parziali

 function is_in_array { local ELEMENT="${1}" local DELIM="," printf "${DELIM}%s${DELIM}" "${@:2}" | grep -q "${DELIM}${ELEMENT}${DELIM}" } 

Che può essere chiamato così:

 $ haystack=("needle1" "needle2" "aneedle" "spaced needle") $ is_in_array "needle" "${haystack[@]}" $ echo $? 1 $ is_in_array "needle1" "${haystack[@]}" $ echo $? 0 

Una combinazione di risposte di Beorn Harris e loentar offre un altro interessante test di un solo elemento:

 delim=$'\x1F' # define a control code to be used as more or less reliable delimiter if [[ "${delim}${array[@]}${delim}" =~ "${delim}a string to test${delim}" ]]; then echo "contains 'a string to test'" fi 

Questo non usa funzioni extra, non sostituisce i test e aggiunge una protezione extra contro le false corrispondenze occasionali usando un codice di controllo come delimitatore.

Un po ‘tardi, ma potresti usare questo:

 #!/bin/bash # isPicture.sh FILE=$1 FNAME=$(basename "$FILE") # Filename, without directory EXT="${FNAME##*.}" # Extension FORMATS=(jpeg JPEG jpg JPG png PNG gif GIF svg SVG tiff TIFF) NOEXT=( ${FORMATS[@]/$EXT} ) # Formats without the extension of the input file # If it is a valid extension, then it should be removed from ${NOEXT}, #+making the lengths inequal. if ! [ ${#NOEXT[@]} != ${#FORMATS[@]} ]; then echo "The extension '"$EXT"' is not a valid image extension." exit fi