Teste se um glob possui correspondências no bash

223

Se eu quiser verificar a existência de um único arquivo, posso testá-lo usando test -e filenameou [ -e filename ].

Supondo que eu tenha um globo e quero saber se existem arquivos cujos nomes correspondam ao globo. O glob pode corresponder a 0 arquivo (nesse caso, não preciso fazer nada) ou pode corresponder a 1 ou mais arquivos (nesse caso, preciso fazer algo). Como posso testar se uma glob tem alguma correspondência? (Eu não ligo para quantas correspondências existem, e seria melhor se eu pudesse fazer isso com uma ifdeclaração e sem loops (simplesmente porque eu acho isso mais legível).

( test -e glob*falha se o glob corresponder a mais de um arquivo.)

Ken Bloom
fonte
3
Suspeito que minha resposta abaixo seja "claramente correta" de uma maneira que todos os outros meio que hackearam. É um shell de uma linha que existe desde sempre e parece ser 'a ferramenta pretendida para esse trabalho em particular'. Estou preocupado que os usuários façam referência errônea à resposta aceita aqui. Qualquer pessoa, por favor, sinta-se à vontade para me corrigir e eu retirarei meu comentário aqui. Estou mais do que feliz por estar errado e aprender com ele. Se a diferença não parecesse tão drástica, eu não levantaria essa questão.
22915 Chris Brianman
1
Minhas soluções favoritas para essa pergunta são o comando find, que funciona em qualquer shell (mesmo que não seja o Bourne shells), mas requer o GNU find, e o comando compgen, que é claramente um Bashism. Pena que não posso aceitar as duas respostas.
Ken Bloom
Nota: Esta pergunta foi editada desde que foi solicitada. O título original era "Teste se um glob possui correspondências no bash". O shell específico, 'bash', foi retirado da pergunta depois que publiquei minha resposta. A edição do título da pergunta faz com que minha resposta esteja errada. Espero que alguém possa alterar ou pelo menos resolver essa alteração.
Brian Chrisman

Respostas:

178

Solução específica do Bash :

compgen -G "<glob-pattern>"

Escape do padrão ou ele será pré-expandido nas partidas.

O status de saída é:

  • 1 para não correspondência,
  • 0 para 'uma ou mais correspondências'

stdouté uma lista de arquivos que correspondem ao glob .
Eu acho que essa é a melhor opção em termos de concisão e minimização de possíveis efeitos colaterais.

UPDATE : Exemplo de uso solicitado.

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi
Brian Chrisman
fonte
9
Observe que compgené um comando interno específico do bash e não faz parte dos comandos internos especificados pelo shell Unix padrão do POSIX. pubs.opengroup.org/onlinepubs/9699919799 pubs.opengroup.org/onlinepubs/9699919799/utilities/… Portanto, evite usá-lo em scripts em que a portabilidade para outros shells é uma preocupação.
Diomidis Spinellis
1
Parece-me que um efeito semelhante sem o bash builtins seria usar qualquer outro comando que atue em um globo e falhe se nenhum arquivo corresponder, como ls: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi- talvez útil para o golfe de código? Falha se houver um arquivo com o mesmo nome da glob, que a glob não deveria ter correspondido, mas se for esse o caso, você provavelmente terá problemas maiores.
Dewi Morgan
4
@DewiMorgan Isso é mais simples:if ls /tmp/*Files &> /dev/null; then echo exists; fi
Clay Bridges
Para obter detalhes sobre compgen, consulte man bashou comhelp compgen
el-teedee
2
sim, cite-o ou o curinga do nome do arquivo será pré-expandido. compgen "dir / *. ext"
Brian Chrisman
169

A opção de shell nullglob é de fato um basismo.

Para evitar a necessidade de um tedioso salvamento e restauração do estado nullglob, eu o configuraria apenas dentro do subshell que expande a glob:

if test -n "$(shopt -s nullglob; echo glob*)"
then
    echo found
else
    echo not found
fi

Para melhor portabilidade e globbing mais flexível, use find:

if test -n "$(find . -maxdepth 1 -name 'glob*' -print -quit)"
then
    echo found
else
    echo not found
fi

As ações explícitas -print -quit são usadas para localização, em vez da ação implícita -impressão padrão , para que a localização saia assim que encontrar o primeiro arquivo que corresponda aos critérios de pesquisa. Onde muitos arquivos coincidem, isso deve ser executado muito mais rápido que echo glob*ouls glob* e também evita a possibilidade de estofar demais a linha de comando expandida (algumas shells têm um limite de comprimento de 4K).

Se encontrar parecer um exagero e o número de arquivos que provavelmente corresponderem for pequeno, use stat:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi
flabdablet
fonte
10
findparece estar exatamente correto. Ele não possui caixas de canto, já que o shell não está fazendo expansão (e passando um globo não expandido para outro comando), é portátil entre shells (embora aparentemente nem todas as opções que você usa sejam especificadas pelo POSIX), e é mais rápido do que ls -d glob*(a resposta anterior aceita), pois para quando chega à primeira correspondência.
Ken Bloom
1
Observe que essa resposta pode exigir um, shopt -u failglobpois essas opções parecem conflitar de alguma forma.
Calimo 30/07
A findsolução corresponderá a um nome de arquivo sem caracteres glob também. Nesse caso, era o que eu queria. Apenas algo para estar ciente de que.
Estamos todos Monica
1
Desde que alguém decidiu editar minha resposta para fazê-lo dizer isso, aparentemente.
flabdablet
1
unix.stackexchange.com/questions/275637/… discute como substituir a -maxdepthopção para uma localização POSIX.
21718 Ken Bloom
25
#!/usr/bin/env bash

# If it is set, then an unmatched glob is swept away entirely -- 
# replaced with a set of zero words -- 
# instead of remaining in place as a single word.
shopt -s nullglob

M=(*px)

if [ "${#M[*]}" -ge 1 ]; then
    echo "${#M[*]} matches."
else
    echo "No such files."
fi
miku
fonte
2
Para evitar um possível conjunto falso "sem correspondências" em nullglobvez de verificar se um único resultado é igual ao padrão em si. Alguns padrões podem corresponder a nomes exatamente iguais ao próprio padrão (por exemplo a*b, mas não por exemplo, a?bou [a]).
Chris Johnsen
Suponho que isso falhe com a chance altamente improvável de que haja realmente um arquivo chamado como glob. (por exemplo, alguém correu touch '*py'), mas isso me leva a outra boa direção.
Ken Bloom
Eu gosto desta como a versão mais geral.
Ken Bloom
E também mais curto. Se você está esperando apenas uma partida, pode usar "$M"como abreviação de "${M[0]}". Caso contrário, você já tem a expansão glob em uma variável de matriz, então você é gtg por passá-la para outras coisas como uma lista, em vez de fazê-las reexpansir a glob.
Peter Cordes
Agradável. Você pode testar o M mais rapidamente (menos bytes e sem gerar um [processo) comif [[ $M ]]; then ...
Tobia
22

Eu gosto

exists() {
    [ -e "$1" ]
}

if exists glob*; then
    echo found
else
    echo not found
fi

Isso é legível e eficiente (a menos que haja um grande número de arquivos).
A principal desvantagem é que é muito mais sutil do que parece, e às vezes me sinto compelido a adicionar um longo comentário.
Se houver uma correspondência, ela "glob*"é expandida pelo shell e todas as correspondências são passadas exists(), o que verifica a primeira e ignora o restante.
Se não houver correspondência, "glob*"é passado para exists()e também não existe.

Editar: pode haver um falso positivo, veja o comentário

Dan Bloch
fonte
13
Ele pode retornar um falso positivo se o glob for algo parecido *.[cC](pode haver cou não um Carquivo, mas um arquivo chamado *.[cC]) ou falso negativo se o primeiro arquivo expandido for, por exemplo, um link simbólico para um arquivo inexistente ou para um arquivo em um arquivo. diretório que você não tem acesso (você deseja adicionar a || [ -L "$1" ]).
Stephane Chazelas
Interessante. O Shellcheck relata que o globbing funciona apenas -equando há 0 ou 1 correspondências. Não funciona para várias correspondências, porque isso se tornaria [ -e file1 file2 ]e isso falharia. Consulte também github.com/koalaman/shellcheck/wiki/SC2144 para obter as razões e as soluções sugeridas.
Thomas Praxl
10

Se você tiver um conjunto de globfail, você pode usar esse louco (o que você realmente não deveria)

shopt -s failglob # exit if * does not match 
( : * ) && echo 0 || echo 1

ou

q=( * ) && echo 0 || echo 1
James
fonte
2
Um uso fantástico de um noop falhando. Nunca deve ser usado ... mas realmente bonito. :)
Brian Chrisman
Você pode colocar a loja dentro dos parênteses. Dessa forma, isso afeta apenas o teste:(shopt -s failglob; : *) 2>/dev/null && echo exists
flabdablet
8

test -e tem a advertência infeliz que considera que links simbólicos quebrados não existem. Então, você também pode procurar por eles.

function globexists {
  test -e "$1" -o -L "$1"
}

if globexists glob*; then
    echo found
else
    echo not found
fi
NerdMachine
fonte
4
Isso ainda não corrige o falso positivo em nomes de arquivos que possuem caracteres especiais globos, como Stephane Chazelas aponta para a resposta de Dan Bloch. (a menos que você faça macaco com nullglob).
Peter Cordes
3
Você deve evitar -oe -aem test/ [. Por exemplo, aqui, ele falha se $1estiver =com a maioria das implementações. Use em [ -e "$1" ] || [ -L "$1" ]vez disso.
Stephane Chazelas
4

Para simplificar um pouco a resposta do MYYN, com base em sua ideia:

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi
Ken Bloom
fonte
4
Feche, mas e se você estiver correspondendo [a], tenha um arquivo chamado [a], mas nenhum arquivo chamado a? Eu ainda gosto nullglobdisso. Alguns podem ver isso como pedante, mas também podemos estar tão corretos quanto possível.
Chris Johnsen
@ sondra.kinsey Isso está errado; o glob [a]deve corresponder apenas a, não o nome do arquivo literal [a].
tripleee
4

Com base na resposta do flabdablet , para mim parece que o mais fácil (não necessariamente o mais rápido) é apenas usar o encontrar- se, deixando a expansão glob no shell, como:

find /some/{p,long-p}ath/with/*globs* -quit &> /dev/null && echo "MATCH"

Ou em if:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi
queria
fonte
Isso funciona exatamente como a versão que eu já apresentei usando stat; não sei como encontrar é "mais fácil" do que stat.
Flabdablet
3
Esteja ciente de que o redirecionamento & é um bashismo e, silenciosamente, fará a coisa errada em outras conchas.
Flabdablet
Isso parece ser melhor do que a findresposta do flabdablet porque ele aceita caminhos na glob e é mais conciso (não requer -maxdepthetc). Também parece melhor do que a statresposta dele, porque não continua fazendo o extra statem cada partida glob adicional. Eu apreciaria se alguém pudesse contribuir com casos de canto onde isso não funciona.
Drwatsoncode # 24/16
1
Após uma análise mais aprofundada, acrescentaria -maxdepth 0porque permite mais flexibilidade na adição de condições. por exemplo, suponha que eu queira restringir o resultado apenas aos arquivos correspondentes. Eu poderia tentar find $glob -type f -quit, mas isso retornaria true se o glob NÃO correspondesse a um arquivo, mas correspondesse a um diretório que continha um arquivo (mesmo recursivamente). Por outro lado find $glob -maxdepth 0 -type f -quit, só retornaria true se o glob em si correspondesse a pelo menos um arquivo. Observe que maxdepthisso não impede que a glob tenha um componente de diretório. (FYI 2>é suficiente sem a necessidade de. &>)
drwatsoncode
2
O ponto de usar findem primeiro lugar é evitar que o shell gere e classifique uma lista potencialmente enorme de correspondências glob; find -name ... -quitcorresponderá no máximo a um nome de arquivo. Se um script confiar em passar uma lista gerada por shell de correspondências glob find, a invocação findalcançará nada além de sobrecarga desnecessária na inicialização do processo. Simplesmente testar a lista resultante diretamente para verificar se não há vazio será mais rápido e claro.
Flabdablet
4

Eu tenho outra solução:

if [ "$(echo glob*)" != 'glob*' ]

Isso funciona muito bem para mim. Há alguns casos de canto que sinto falta?

SaschaZorn
fonte
2
Funciona, exceto se o arquivo for realmente chamado de 'glob *'.
Ian Kelling
funciona para passar glob como variável - gera um erro "muitos argumentos" quando há mais de uma correspondência. "$ (echo $ GLOB)" não está retornando uma única sequência ou, pelo menos, não é interpretada como única, portanto, há muitos argumentos de erro
DKebler
@ DKebler: deve ser interpretado como string única, porque está entre aspas duplas.
User1934428 de
3

No Bash, você pode enviar para uma matriz; se o glob não corresponder, sua matriz conterá uma única entrada que não corresponde a um arquivo existente:

#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

if [ -e "${scripts[0]}" ]
then stat "${scripts[@]}"
fi

Nota: se você nullglobconfigurou, scriptshaverá uma matriz vazia e você deve testar com [ "${scripts[*]}" ]ou com [ "${#scripts[*]}" != 0 ]. Se você estiver escrevendo uma biblioteca que deve funcionar com ou sem nullglob, você desejará

if [ "${scripts[*]}" ] && [ -e "${scripts[0]}" ]

Uma vantagem dessa abordagem é que você tem a lista de arquivos com os quais deseja trabalhar, em vez de precisar repetir a operação glob.

Toby Speight
fonte
Por que, com nullglob set e a matriz possivelmente vazia, você ainda não pode testar if [ -e "${scripts[0]}" ]...? Você também está permitindo a possibilidade do conjunto de substantivos da opção shell ?
21818 Johnraff
@ johnraff, sim, eu normalmente suponho que nounsetesteja ativo. Além disso, pode ser (um pouco) mais barato testar se a string não está vazia do que verificar a presença de um arquivo. É improvável, porém, dado que acabamos de executar um glob, o que significa que o conteúdo do diretório deve estar atualizado no cache do sistema operacional.
Toby Speight
1

Essa abominação parece funcionar:

#!/usr/bin/env bash
shopt -s nullglob
if [ "`echo *py`" != "" ]; then
    echo "Glob matched"
else
    echo "Glob did not match"
fi

Provavelmente requer bash, não sh.

Isso funciona porque a opção nullglob faz com que o globo seja avaliado como uma sequência vazia se não houver correspondências. Portanto, qualquer saída não vazia do comando echo indica que a glob correspondeu a algo.

Ryan C. Thompson
fonte
Você deve usarif [ "`echo *py`" != "*py"]
yegle 22/02
1
Isso não funcionaria corretamente se houvesse um arquivo chamado *py.
Ryan C. Thompson
Se não houver um final de arquivo py, `echo *py`será avaliado como *py.
yegle 22/02
1
Sim, mas também o fará se houver um único arquivo chamado *py, que é o resultado errado.
Ryan C. Thompson
Corrija-me se eu estiver errado, mas se não houver nenhum arquivo que corresponda *py, seu script ecoará "Glob correspondido"?
yegle
1

Não vi essa resposta, então pensei em publicá-la:

set -- glob*
[ -f "$1" ] && echo "found $@"
Brad Howes
fonte
1
set -- glob*
if [ -f "$1" ]; then
  echo "It matched"
fi

Explicação

Quando não houver uma correspondência para glob*, $1ela conterá 'glob*'. O teste -f "$1"não será verdadeiro porque o glob*arquivo não existe.

Por que isso é melhor do que alternativas

Isso funciona com sh e derivados: ksh e bash. Não cria nenhum sub-shell. $(..)e `...`comandos criam um sub-shell; eles bifurcam um processo e, portanto, são mais lentos que esta solução.

joseyluis
fonte
1

Assim em (arquivos de teste contendo pattern):

shopt -s nullglob
compgen -W *pattern* &>/dev/null
case $? in
    0) echo "only one file match" ;;
    1) echo "more than one file match" ;;
    2) echo "no file match" ;;
esac

É muito melhor do que compgen -G: porque podemos discriminar mais casos e com mais precisão.

Pode trabalhar com apenas um curinga *

Gilles Quenot
fonte
0
if ls -d $glob > /dev/null 2>&1; then
  echo Found.
else
  echo Not found.
fi

Observe que isso pode levar muito tempo se houver muitas correspondências ou o acesso ao arquivo for lento.

Florian Diesch
fonte
1
Isso dará a resposta errada se um padrão semelhante [a]for usado quando o arquivo [a]estiver presente e o arquivo aestiver ausente. Ele dirá "encontrado", mesmo que o único arquivo que ele deva corresponder a,, não esteja realmente presente.
Chris Johnsen
Esta versão deve funcionar em um POSIX / bin / sh comum (sem bashisms) e, no caso de eu precisar, o glob não tem colchetes de qualquer maneira e não preciso me preocupar com casos que são terrivelmente patológico. Mas acho que não existe uma boa maneira de testar se algum arquivo corresponde a um globo.
Ken Bloom
0
#!/bin/bash
set nullglob
touch /tmp/foo1 /tmp/foo2 /tmp/foo3
FOUND=0
for FILE in /tmp/foo*
do
    FOUND=$((${FOUND} + 1))
done
if [ ${FOUND} -gt 0 ]; then
    echo "I found ${FOUND} matches"
else
    echo "No matches found"
fi
Peter Lyons
fonte
2
Esta versão falha quando exatamente um arquivo corresponde, mas você pode evitar o erro FOUND = -1 usando a nullglobopção shell.
Ken Bloom
@ Ken: Hmm, eu não chamaria nullglobum kludge. Comparar um resultado único com o padrão original é uma ilusão (e propenso a resultados falsos), o uso de nullglobnão é.
Chris Johnsen
@ Chris: Eu acho que você leu errado. Eu não chamei nullglobum kludge.
Ken Bloom
1
@ Ken: Na verdade, eu interpretei mal. Aceite minhas desculpas por minhas críticas inválidas.
Chris Johnsen
-1
(ls glob* &>/dev/null && echo Files found) || echo No file found
Damodharan R
fonte
5
Também retornaria false se houver diretórios correspondentes glob*e, por exemplo, você não tiver a gravação para listar esses diretórios.
Stephane Chazelas
-1

ls | grep -q "glob.*"

Não é a solução mais eficiente (se houver uma tonelada de arquivos no diretório, pode ser lento), mas é simples, fácil de ler e também tem a vantagem de que as expressões regulares são mais poderosas do que os padrões simples do bash glob.

jesjimher
fonte
-2
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true
otocan
fonte
1
Para uma resposta melhor, tente adicionar alguma explicação ao seu código.
Masoud Rahimi