web-dev-qa-db-fra.com

Vérifier si un tableau de Bash contient une valeur

Dans Bash, quel est le moyen le plus simple de tester si un tableau contient une certaine valeur? 

Edit : Avec l’aide des réponses et des commentaires, après quelques tests, j’ai trouvé ceci:

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

Je ne suis pas sûr que ce soit la meilleure solution, mais cela semble fonctionner.

308
Paolo Tedesco

Il existe un exemple de code qui montre comment remplacer une sous-chaîne d'un tableau . Vous pouvez faire une copie du tableau et essayer de supprimer la valeur cible de la copie. Si la copie et l'original sont alors différents, la valeur cible existe dans la chaîne d'origine.

La solution simple (mais potentiellement plus longue) consiste simplement à parcourir l'ensemble du tableau et à vérifier chaque élément individuellement. C’est ce que je fais généralement car il est facile à mettre en œuvre et vous pouvez l’envelopper dans une fonction (voir cette information sur le passage d’un tableau à une fonction ).

8
bta

Vous trouverez ci-dessous une petite fonction pour y parvenir. La chaîne de recherche est le premier argument et le reste sont les éléments du tableau:

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

Un test de cette fonction pourrait ressembler à:

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

Cette approche a l’avantage de ne pas avoir besoin de boucler sur tous les éléments (du moins pas explicitement). Mais puisque array_to_string_internal() dans array.c encore boucle sur des éléments de tableau et les concatène en une chaîne, ce n'est probablement pas plus efficace que les solutions de boucle proposées, mais c'est plus lisible.

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

Notez que dans les cas où la valeur que vous recherchez est l'un des mots d'un élément de tableau avec des espaces, cela donnera des faux positifs. Par exemple

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

La regex verra que Jack sera dans le tableau, même si ce n'est pas le cas. Donc vous devrez changer IFS et les caractères de séparation sur votre regex si vous voulez toujours utiliser cette solution, comme ceci

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
294
Keegan
$ myarray=(one two three)
$ case "${myarray[@]}" in  *"two"*) echo "found" ;; esac
found
57
ghostdog74
for i in "${array[@]}"
do
    if [ "$i" -eq "$yourValue" ] ; then
        echo "Found"
    fi
done

Pour les cordes:

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

Si vous avez besoin de performances, vous ne voulez pas parcourir tout votre tableau à chaque recherche.

Dans ce cas, vous pouvez créer un tableau associatif (table de hachage ou dictionnaire) qui représente un index de ce tableau. C'est à dire. il mappe chaque élément du tableau dans son index dans le tableau:

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
}

Ensuite, vous pouvez l'utiliser comme ceci:

myarray=('a a' 'b b' 'c c')
make_index myarray_index "${myarray[@]}"

Et testez l'appartenance comme ceci:

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

Ou aussi:

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

Notez que cette solution est la solution, même s’il existe des espaces dans la valeur testée ou dans les valeurs du tableau.

En bonus, vous obtenez également l'index de la valeur dans le tableau avec:

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

Je n'utilise généralement que:

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

une valeur non nulle indique qu'une correspondance a été trouvée.

15
Sean DiSanti

Une autre doublure sans fonction:

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

Merci @Qwerty pour le heads up concernant les espaces!

fonction correspondante:

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

exemple:

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

Voici une petite contribution:

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

Remarque: cette manière ne distingue pas le cas "deux mots" mais cela n'est pas requis dans la question.

10
hornetbzz

Solution à une ligne

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

Explication

L'instruction printf imprime chaque élément du tableau sur une ligne distincte.

L'instruction grep utilise les caractères spéciaux ^ et $ pour rechercher une ligne contenant exactement le modèle indiqué par mypattern (ni plus, ni moins).


Utilisation

Pour mettre cela dans une instruction if ... then:

if printf '%s\n' ${myarray[@]} | grep -q -P '^mypattern$'; then
    # ...
fi

J'ai ajouté un indicateur -q à l'expression grep afin d'éviter l'impression des correspondances. cela traitera simplement l’existence d’une correspondance comme "vraie".

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

Maintenant gère correctement les tableaux vides.

9
Yann

Si vous souhaitez effectuer un test rapide et compliqué pour voir s'il est utile de parcourir l'ensemble du tableau pour obtenir une correspondance précise, Bash peut traiter les tableaux comme des scalaires. Recherchez une correspondance dans le scalaire. Si aucune option n'est définie, ignorer la boucle vous fera gagner du temps. De toute évidence, vous pouvez obtenir des faux positifs.

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

Cela produira "Vérification" et "Correspondance". Avec array=(Word "two words" something), il ne produira que "Vérification". Avec array=(Word "two widgets" something), il n'y aura pas de sortie.

5
Dennis Williamson
a=(b c d)

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

Si vous préférez, vous pouvez utiliser des options longues équivalentes:

--fixed-strings --quiet --line-regexp --null-data
5
Steven Penny

Cela fonctionne pour moi:

# 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
}

Exemple d'appel:

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

donné :

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

puis un simple contrôle de:

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

c is element separator
p is regex pattern

(La raison pour affecter p séparément, plutôt que d'utiliser l'expression directement dans [[]] est de maintenir la compatibilité pour bash 4)

3
Beorn Harris

Utiliser grep et printf

Formatez chaque membre du tableau sur une nouvelle ligne, puis grep les lignes. 

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

Notez que cela n'a pas de problèmes avec les délimètres et les espaces.

2
Qwerty

Empruntant à Dennis Williamson / s answer , la solution suivante associe des tableaux, des guillemets Shell et des expressions régulières afin d’éviter la nécessité de: effectuer des itérations sur des boucles; utiliser des tuyaux ou d'autres sous-processus; ou en utilisant des utilitaires 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

Le code ci-dessus fonctionne en utilisant des expressions régulières Bash pour correspondre à une version du contenu du tableau sous forme de chaîne. Il y a six étapes importantes pour s'assurer que la correspondance d'expression régulière ne peut pas être trompée par des combinaisons intelligentes de valeurs dans le tableau:

  1. Construisez la chaîne de comparaison en utilisant la variable printf intégrée à Bash, %q. Shell-quoting garantira que les caractères spéciaux deviennent "Shell-safe" en étant échappés avec la barre oblique inversée \.
  2. Choisissez un caractère spécial pour servir de délimiteur de valeur. Le délimiteur DOIT être l'un des caractères spéciaux qui seront échappés lors de l'utilisation de %q; C'est la seule façon de garantir que les valeurs du tableau ne peuvent pas être construites de manière intelligente pour tromper la correspondance de l'expression régulière. J'ai choisi la virgule , car ce caractère est le plus sûr lorsqu'il est évalué ou utilisé de manière inattendue.
  3. Combinez tous les éléments du tableau en une seule chaîne, en utilisant deux occurrences du caractère spécial comme délimiteur. En utilisant la virgule comme exemple, j’ai utilisé ,,%q comme argument de printf. Cela est important car deux occurrences du caractère spécial ne peuvent apparaître l'une à côté de l'autre que lorsqu'elles apparaissent en tant que délimiteur; toutes les autres occurrences du caractère spécial seront échappées.
  4. Ajoutez deux occurrences de fin du délimiteur à la chaîne, pour permettre des correspondances avec le dernier élément du tableau. Ainsi, au lieu de comparer avec ${array_str}, comparez avec ${array_str},,.
  5. Si la chaîne cible que vous recherchez est fournie par une variable utilisateur, vous devez échapper à toutes les occurrences du caractère spécial avec une barre oblique inverse. Sinon, la correspondance de l'expression régulière devient vulnérable au risque d'être trompée par des éléments de tableau intelligemment conçus.
  6. Effectuez une correspondance d'expression régulière Bash avec la chaîne.
2
Dejay Clayton

J'écris généralement ces types d'utilitaires pour agir sur le nom de la variable, plutôt que sur la valeur de la variable, principalement parce que bash ne peut pas autrement passer de variables par référence.

Voici une version qui fonctionne avec le nom du tableau:

function array_contains # array value
{
    [[ -n "$1" && -n "$2" ]] || {
        echo "usage: array_contains <array> <value>"
        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
}

Avec cela, l'exemple de question devient:

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

etc.

2
Barry Kelly

Après avoir répondu, j’ai lu une autre réponse qui me plaisait particulièrement, mais elle était imparfaite et votée à la baisse. Je me suis inspiré et voici deux nouvelles approches que je vois viables.

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

en utilisant grep et printf:

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

en utilisant for:

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

Pour les résultats not_found, ajoutez || <run_your_if_notfound_command_here>

1
Qwerty

Voici mon point de vue sur ceci.

Je préférerais ne pas utiliser un bash pour la boucle si je peux l'éviter, car cela prend du temps à courir. Si quelque chose doit être bouclé, laissez-le être écrit dans un langage de niveau inférieur à un script Shell.

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

Cela fonctionne en créant un tableau associatif temporaire, _arr, dont les index sont dérivés des valeurs du tableau en entrée. (Notez que les tableaux associatifs sont disponibles à partir de bash 4 et supérieur, de sorte que cette fonction ne fonctionnera pas dans les versions antérieures de bash.) Nous avons défini $IFS pour éviter le fractionnement de Word sur des espaces.

La fonction ne contient pas de boucles explicites, même si en interne bash traverse le tableau en entrée afin de peupler printf. Le format printf utilise %q pour garantir que les données d'entrée sont protégées, de sorte qu'elles puissent être utilisées en toute sécurité comme clés de matrice.

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

Notez que tout ce que cette fonction utilise est intégré à bash, il n'y a donc pas de tuyaux externes qui vous traînent vers le bas, même dans le développement des commandes.

Et si vous n'aimez pas utiliser eval ... eh bien, vous êtes libre d'utiliser une autre approche. :-)

1
ghoti

En combinant quelques-unes des idées présentées ici, vous pouvez créer une déclaration élégante si elle ne contient pas de boucles et que mot exact correspond.

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

Cela ne déclenchera pas sur Word ou val, seules les correspondances de mots entières. Cela cassera si chaque valeur de tableau contient plusieurs mots.

1
Ecker00

J'avais le cas où je devais vérifier si un identifiant était contenu dans une liste d'identifiants générés par un autre script/commande . Pour moi, fonctionnait comme suit:

# the ID I was looking for
ID=1

# somehow generated list of IDs
LIST=$( <some script that generates lines with IDs> )
# 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 (e. g. 0 for nothing found, everything greater than 0 means found)
if [ ISIN -eq 0 ]; then
    echo "not found"
fi
# etc.

Vous pouvez aussi le raccourcir/réduire comme ceci:

if [ $(echo " $( <script call> ) " | grep -F " $ID " -c) -eq 0 ]; then
    echo "not found"
fi

Dans mon cas, jq exécutait jq pour filtrer une liste d’ID avec certains JSON et je devais vérifier plus tard si mon ID figurait dans cette liste et cela fonctionnait le mieux pour moi . Cela ne fonctionnerait pas pour les tableaux créés manuellement tapez LIST=("1" "2" "4") mais pour une sortie de script séparée par une nouvelle ligne.


PS: je ne peux pas commenter une réponse parce que je suis relativement nouveau ...

0
E. Körner

Un petit ajout à la réponse de @ ghostdog74 sur l'utilisation de la logique case pour vérifier que le tableau contient une valeur particulière:

myarray=(one two three)
Word=two
case "${myarray[@]}" in  ("$Word "*|*" $Word "*|*" $Word") echo "found" ;; esac

Ou avec l'option extglob activée, vous pouvez le faire comme ceci:

myarray=(one two three)
Word=two
shopt -s extglob
case "${myarray[@]}" in ?(*" ")"$Word"?(" "*)) echo "found" ;; esac

Nous pouvons aussi le faire avec la déclaration if:

myarray=(one two three)
Word=two
if [[ $(printf "_[%s]_" "${myarray[@]}") =~ .*_\[$Word\]_.* ]]; then echo "found"; fi
0
Aleksandr Podkutin

Voici mon point de vue sur ce problème. Voici la version courte:

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

Et la version longue, qui me semble beaucoup plus facile à regarder.

# 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$"
}

Exemples:

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
0
robert