Verifique se uma matriz Bash contém um valor

No Bash, qual é a maneira mais simples de testar se um array contém um certo valor?

Edit : Com a ajuda das respostas e dos comentários, depois de alguns testes, descobri o seguinte:

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 

Não tenho certeza se é a melhor solução, mas parece funcionar.

Existe um código de exemplo que mostra como replace uma substring de uma matriz . Você pode fazer uma cópia da matriz e tentar remover o valor de destino da cópia. Se a cópia e o original forem diferentes, o valor de destino existirá na string original.

A solução simples (mas potencialmente mais demorada) é simplesmente percorrer todo o array e verificar cada item individualmente. Isso é o que eu normalmente faço porque é fácil de implementar e você pode envolvê-lo em uma function (veja esta informação sobre passar um array para uma function ).

Abaixo está uma pequena function para alcançar isso. A cadeia de pesquisa é o primeiro argumento e o restante são os elementos da matriz:

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

Um teste dessa function poderia se parecer com:

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

Essa abordagem tem a vantagem de não precisar fazer o loop de todos os elementos (pelo menos não explicitamente). Mas como array_to_string_internal() em array.c ainda faz um loop sobre elementos de array e os concatena em uma string, provavelmente não é mais eficiente do que as soluções de looping propostas, mas é mais legível.

 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 

Observe que, nos casos em que o valor que você está procurando é uma das palavras em um elemento de matriz com espaços, ele fornecerá falsos positivos. Por exemplo

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

O regex vai ver Jack como estando na matriz, embora não seja. Então você terá que mudar o IFS e os caracteres separadores em seu regex se você ainda quiser usar esta solução, como esta

 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 

Para strings:

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

Eu normalmente uso apenas:

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

O valor diferente de zero indica que uma correspondência foi encontrada.

Se você precisa de desempenho, não quer passar o tempo todo pela sua matriz toda vez que pesquisa.

Nesse caso, você pode criar uma matriz associativa (tabela de hash ou dictionary) que representa um índice dessa matriz. Ou seja, mapeia cada elemento da matriz em seu índice na matriz:

 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 } 

Então você pode usá-lo assim:

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

E teste a associação assim:

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

Ou também:

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

Observe que essa solução faz a coisa certa, mesmo que haja espaços no valor testado ou nos valores da matriz.

Como bônus, você também obtém o índice do valor dentro da matriz com:

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

Aqui está uma pequena contribuição:

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

Nota: desta forma não distingue o caso “duas palavras”, mas isso não é necessário na questão.

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

Agora lida com matrizes vazias corretamente.

Outro forro sem function:

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

Obrigado @Qwerty pelo heads up em relação aos espaços!

function correspondente:

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

exemplo:

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

Se você quiser fazer um teste rápido e sujo para ver se vale a pena iterar em toda a matriz para obter uma correspondência precisa, Bash pode tratar matrizes como escalares. Teste para uma correspondência no escalar, se nada, então pulando o loop economiza tempo. Obviamente, você pode obter falsos positivos.

 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 

Isso produzirá “Checking” e “Match”. Com array=(word "two words" something) só irá mostrar “Checking”. Com array=(word "two widgets" something) não haverá saída.

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

Se preferir, você pode usar opções longas equivalentes:

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

Isso está funcionando para mim:

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

Exemplo de chamada:

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

dado:

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

então uma simples verificação de:

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

Onde

 c is element separator p is regex pattern 

(O motivo para atribuir p separadamente, em vez de usar a expressão diretamente dentro de [[]], é manter a compatibilidade para o bash 4)

Eu geralmente escrevo esses tipos de utilitários para operar no nome da variável, em vez do valor da variável, principalmente porque o bash não pode passar variables ​​por referência.

Aqui está uma versão que funciona com o nome da matriz:

 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 } 

Com isso, o exemplo da pergunta se torna:

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

etc.

Usando grep e printf

Formate cada membro da matriz em uma nova linha e, em seguida, grep as linhas.

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

exemplo:

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

Observe que isso não tem problemas com delimitadores e espaços.

Simples se você não se importa com expressões regulares:

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

A instrução printf imprime cada elemento da matriz em uma linha separada.

A instrução grep usa os caracteres especiais ^ e $ para encontrar uma linha que contenha exatamente o padrão dado ao meu mypattern .

Depois de ter respondido, li outra resposta que gostei particularmente, mas foi falha e rejeitada. Eu me inspirei e aqui estão duas novas abordagens que vejo viáveis.

 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) &&  

Para resultados not_found, adicione || ||

Aqui está a minha opinião sobre isso.

Eu prefiro não usar um bash for loop se eu puder evitá-lo, pois isso leva tempo para ser executado. Se algo tiver que fazer um loop, seja algo escrito em uma linguagem de nível inferior a um script de shell.

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

Isso funciona criando uma matriz associativa temporária, _arr , cujos índices são derivados dos valores da matriz de input. (Observe que as matrizes associativas estão disponíveis no bash 4 e acima, portanto, essa function não funcionará em versões anteriores do bash.) Definimos o $IFS para evitar a divisão de palavras no espaço em branco.

A function não contém loops explícitos, apesar de passar internamente por uma matriz de input para preencher printf . O formato printf usa %q para garantir que os dados de input sejam escapados para que possam ser usados ​​com segurança como chaves de matriz.

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

Note que tudo o que esta function usa é um built-in para bash, então não há tubos externos arrastando você para baixo, mesmo na expansão de comando.

E se você não gosta de usar eval … bem, você está livre para usar outra abordagem. 🙂

Combinando algumas das idéias apresentadas aqui você pode fazer uma declaração elegante se não houver loops que correspondam exatamente às palavras .

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

Isso não será acionado em word ou val , apenas correspondências de palavras inteiras. Ele será quebrado se cada valor da matriz contiver várias palavras.

Emprestando da resposta de Dennis Williamson , a seguinte solução combina arrays, citações seguras e expressões regulares para evitar a necessidade de: iterar sobre loops; usando tubos ou outros subprocesss; ou usando utilitários não 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 

O código acima funciona usando expressões regulares Bash para combinar com uma versão string do conteúdo da matriz. Há seis etapas importantes para garantir que a correspondência de expressão regular não seja enganada por combinações inteligentes de valores dentro da matriz:

  1. Construa a cadeia de comparação usando aspas de printf embutidas do Bash, %q . As citações em shell garantirão que os caracteres especiais se tornem “seguros para shell” ao serem escapados com barra invertida \ .
  2. Escolha um caractere especial para servir como um delimitador de valor. O delimitador TEM que ser um dos caracteres especiais que serão escapados ao usar %q ; essa é a única maneira de garantir que os valores dentro da matriz não possam ser construídos de maneiras inteligentes para enganar a correspondência de expressão regular. Eu escolhi a vírgula , porque esse personagem é o mais seguro quando avaliado ou mal utilizado de uma maneira inesperada.
  3. Combine todos os elementos do array em uma única string, usando duas instâncias do caractere especial para servir como delimitador. Usando vírgula como exemplo, usei ,,%q como argumento para printf . Isso é importante porque duas instâncias do caractere especial só podem aparecer ao lado uma da outra quando aparecem como o delimitador; todas as outras instâncias do caractere especial serão ignoradas.
  4. Anexe duas instâncias finais do delimitador à string, para permitir correspondências contra o último elemento da matriz. Assim, em vez de comparar com ${array_str} , compare com ${array_str},, .
  5. Se a string de destino que você está procurando for fornecida por uma variável de usuário, você deve escaping todas as instâncias do caractere especial com uma barra invertida. Caso contrário, a correspondência de expressão regular fica vulnerável a ser enganada por elementos de matriz habilmente criados.
  6. Execute uma correspondência de expressão regular Bash na sequência.

Aqui está a minha opinião sobre este problema. Aqui está a versão curta:

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

E a versão longa, que acho muito mais fácil para os olhos.

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

Exemplos:

 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 

Eu tive o caso que eu tinha que verificar se um ID estava contido em uma lista de IDs gerados por outro script / comando. Para mim trabalhou o seguinte:

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

Você também pode encurtar / compactar assim:

 if [ $(echo " $(  

No meu caso, eu estava executando jq para filtrar alguns JSON para uma lista de IDs e tive que verificar mais tarde se meu ID estava nessa lista e isso funcionou melhor para mim. Ele não funcionará para arrays criados manualmente do tipo LIST=("1" "2" "4") mas com saída de script separada por nova linha.


PS .: não foi possível comentar uma resposta porque sou relativamente novo ...

O código a seguir verifica se um determinado valor está na matriz e retorna seu deslocamento baseado em 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 

A correspondência é feita nos valores completos, portanto, definir VALUE = “three” não corresponderia.

Isso pode valer a pena investigar se você não deseja iterar:

 #!/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 adaptado de: http://www.thegeekstuff.com/2010/06/bash-array-tutorial/ Eu acho que é muito inteligente.

EDIT: você provavelmente poderia apenas fazer:

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

Mas o último só funciona se o array contiver valores exclusivos. Procurando por 1 em “143” vai dar falsos positivos, methinks.

Embora houvesse várias respostas excelentes e úteis aqui, não encontrei uma que parecesse ser a combinação certa de performance, multiplataforma e robusta; então eu queria compartilhar a solução que escrevi para o meu código:

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

Minha versão da técnica de expressões regulares que já foi sugerida:

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

O que está acontecendo aqui é que você está expandindo toda a matriz de valores suportados em palavras e prefixando uma string específica, “X-” neste caso, para cada um deles, e fazendo o mesmo com o valor solicitado. Se este estiver de fato contido no array, então a string resultante corresponderá no máximo a um dos tokens resultantes, ou nenhum ao contrário. Neste último caso, o || O operador triggers e você sabe que está lidando com um valor não suportado. Antes de tudo isso, o valor solicitado é despojado de todos os espaços em branco à esquerda e à direita por meio da manipulação de string de shell padrão.

É claro e elegante, creio eu, embora não tenha muita certeza de como ele pode ser bom se sua matriz de valores suportados for particularmente grande.

Expandindo a resposta acima de Sean DiSanti, acho que o seguinte é uma solução simples e elegante que evita ter que fazer um loop sobre a matriz e não dará falsos positivos devido a correspondências parciais

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

Que pode ser chamado assim:

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

Uma combinação de respostas de Beorn Harris e loentar dá mais um interessante teste de uma linha:

 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 

Este não usa funções extras, não faz substituições para testes e adiciona proteção extra contra correspondências falsas ocasionais usando um código de controle como um delimitador.

Um pouco atrasado, mas você poderia usar isso:

 #!/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