Testez si un glob a des correspondances dans bash

223

Si je veux vérifier l'existence d'un seul fichier, je peux le tester en utilisant test -e filenameou [ -e filename ].

Supposons que j'ai un glob et que je veuille savoir s'il existe des fichiers dont les noms correspondent au glob. Le glob peut correspondre à 0 fichier (auquel cas je n'ai rien à faire), ou il peut correspondre à 1 ou plusieurs fichiers (auquel cas je dois faire quelque chose). Comment puis-je tester si un glob a des correspondances? (Je me fiche du nombre de correspondances, et il serait préférable que je puisse le faire avec une seule ifinstruction et sans boucles (simplement parce que je trouve cela plus lisible).

( test -e glob*échoue si le glob correspond à plusieurs fichiers.)

Ken Bloom
la source
3
Je soupçonne que ma réponse ci-dessous est «clairement correcte» d'une manière que tous les autres types de piratage. C'est un shell intégré à une ligne qui existe depuis toujours et semble être `` l'outil prévu pour ce travail particulier ''. Je crains que les utilisateurs ne référencent par erreur la réponse acceptée ici. N'importe qui, n'hésitez pas à me corriger et je retirerai mon commentaire ici, je suis plus qu'heureux de me tromper et d'en tirer des leçons. Si la différence ne semblait pas si radicale, je ne soulèverais pas ce problème.
Brian Chrisman
1
Mes solutions préférées à cette question sont la commande find qui fonctionne dans n'importe quel shell (même les shells non Bourne) mais nécessite GNU find, et la commande compgen qui est clairement un bashisme. Dommage que je ne puisse pas accepter les deux réponses.
Ken Bloom
Remarque: Cette question a été modifiée depuis qu'elle a été posée. Le titre original était "Tester si un glob a des correspondances dans bash". Le shell spécifique, «bash», a été supprimé de la question après avoir publié ma réponse. La modification du titre de la question donne l'impression que ma réponse est erronée. J'espère que quelqu'un pourra modifier ou au moins aborder ce changement.
Brian Chrisman

Réponses:

178

Solution spécifique à Bash :

compgen -G "<glob-pattern>"

Échappez au motif ou il sera pré-développé en matchs.

Le statut de sortie est:

  • 1 pour match nul,
  • 0 pour «un ou plusieurs matchs»

stdoutest une liste de fichiers correspondant au glob .
Je pense que c'est la meilleure option en termes de concision et de minimisation des effets secondaires potentiels.

MISE À JOUR : Exemple d'utilisation demandée.

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi
Brian Chrisman
la source
9
Notez qu'il compgens'agit d'une commande intégrée spécifique à bash et qui ne fait pas partie des commandes intégrées spécifiées par le shell Unix POSIX standard. pubs.opengroup.org/onlinepubs/9699919799 pubs.opengroup.org/onlinepubs/9699919799/utilities/… Par conséquent, évitez de l'utiliser dans des scripts où la portabilité vers d'autres shells est une préoccupation.
Diomidis Spinellis
1
Il me semble qu'un effet similaire sans les commandes bash serait d'utiliser toute autre commande qui agit sur un glob et échoue si aucun fichier ne correspond, comme ls: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi- peut-être utile pour le golf de code? Échoue s'il existe un fichier nommé le même que le glob, auquel le glob n'aurait pas dû correspondre, mais si c'est le cas, vous avez probablement de plus gros problèmes.
Dewi Morgan
4
@DewiMorgan C'est plus simple:if ls /tmp/*Files &> /dev/null; then echo exists; fi
Clay Bridges
Pour plus de détails sur compgen, voir man bashou avechelp compgen
el-teedee
2
oui, citez-le ou le caractère générique du nom de fichier sera pré-développé. compgen "dir / *. ext"
Brian Chrisman
169

L'option shell nullglob est en effet un bashisme.

Pour éviter la nécessité d'une sauvegarde et d'une restauration fastidieuses de l'état nullglob, je ne le définirais qu'à l'intérieur du sous-shell qui étend le glob:

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

Pour une meilleure portabilité et une globalisation plus flexible, utilisez find:

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

Les actions explicites -print -quit sont utilisées pour rechercher au lieu de l'implicite par défaut action -print sorte que find se ferme dès qu'il trouve le premier fichier correspondant aux critères de recherche. Lorsque de nombreux fichiers correspondent, cela devrait fonctionner beaucoup plus rapidement que echo glob*ou ls glob*et cela évite également la possibilité de surcharger la ligne de commande étendue (certains shells ont une limite de longueur 4K).

Si find semble exagéré et que le nombre de fichiers susceptibles de correspondre est petit, utilisez stat:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi
flabdablet
la source
10
findsemble être exactement correct. Il n'a pas de cas d'angle, puisque le shell ne fait pas d'expansion (et ne passe pas un glob non développé à une autre commande), il est portable entre les shells (bien qu'apparemment toutes les options que vous n'utilisez pas soient spécifiées par POSIX), et c'est plus rapide que ls -d glob*(la réponse acceptée précédente) car elle s'arrête lorsqu'elle atteint le premier match.
Ken Bloom
1
Notez que cette réponse peut nécessiter un shopt -u failglobcar ces options semblent en conflit d'une manière ou d'une autre.
Calimo
La findsolution correspondra également à un nom de fichier sans caractères globaux. Dans ce cas, c'est ce que je voulais. Juste quelque chose à savoir cependant.
Nous sommes tous Monica
1
Depuis que quelqu'un d'autre a décidé de modifier ma réponse pour le faire dire, apparemment.
flabdablet
1
unix.stackexchange.com/questions/275637/… explique comment remplacer l' -maxdepthoption pour une recherche POSIX.
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
la source
2
Pour éviter une éventuelle fausse «non correspondance» définie nullglobau lieu de vérifier si un seul résultat est égal au modèle lui-même. Certains modèles peuvent correspondre à des noms qui sont exactement égaux au modèle lui-même (par exemple a*b, mais pas par exemple a?bou [a]).
Chris Johnsen
Je suppose que cela échoue sur la chance très improbable qu'il y ait en fait un fichier nommé comme le glob. (par exemple, quelqu'un a couru touch '*py'), mais cela m'indique une autre bonne direction.
Ken Bloom
J'aime celui-ci comme la version la plus générale.
Ken Bloom
Et aussi le plus court. Si vous n'attendez qu'une seule correspondance, vous pouvez l'utiliser "$M"comme raccourci pour "${M[0]}". Sinon, vous avez déjà l'expansion glob dans une variable de tableau, vous êtes donc gtg pour la passer à d'autres choses sous forme de liste, au lieu de les faire redévelopper le glob.
Peter Cordes
Agréable. Vous pouvez tester M plus rapidement (moins d'octets et sans générer de [processus) avecif [[ $M ]]; then ...
Tobia
22

J'aime

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

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

C'est à la fois lisible et efficace (sauf s'il y a un grand nombre de fichiers).
Le principal inconvénient est qu'il est beaucoup plus subtil qu'il n'y paraît, et je me sens parfois obligé d'ajouter un long commentaire.
S'il y a une correspondance, "glob*"est développé par le shell et toutes les correspondances sont passées à exists(), qui vérifie la première et ignore le reste.
S'il n'y a pas de correspondance, "glob*"est passé àexists() et trouvé comme n'existant pas non plus.

Edit: il peut y avoir un faux positif, voir commentaire

Dan Bloch
la source
13
Il peut retourner un faux positif si le glob est quelque chose comme *.[cC](il peut y avoir pas cou Cfichier, mais un fichier appelé *.[cC]) ou faux négatif si le premier fichier élargi de qui est par exemple un lien symbolique vers un fichier unexistent ou à un fichier dans un répertoire auquel vous n'avez pas accès (vous voulez en ajouter un || [ -L "$1" ]).
Stephane Chazelas
Intéressant. Shellcheck signale que la globalisation ne fonctionne qu'avec -e, lorsqu'il y a 0 ou 1 correspondance. Cela ne fonctionne pas pour plusieurs correspondances, car cela deviendrait [ -e file1 file2 ]et cela échouerait. Voir également github.com/koalaman/shellcheck/wiki/SC2144 pour la justification et les solutions suggérées.
Thomas Praxl
10

Si vous avez défini globfail, vous pouvez utiliser ce fou (ce que vous ne devriez vraiment pas)

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

ou

q=( * ) && echo 0 || echo 1
James
la source
2
Une utilisation fantastique d'un noop qui échoue. Ne devrait jamais être utilisé ... mais vraiment beau. :)
Brian Chrisman
Vous pouvez mettre le shopt à l'intérieur des parens. De cette façon, cela n'affecte que le test:(shopt -s failglob; : *) 2>/dev/null && echo exists
flabdablet
8

test -e a la fâcheuse mise en garde qu'il considère que les liens symboliques rompus n'existent pas. Donc, vous voudrez peut-être aussi les vérifier.

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

if globexists glob*; then
    echo found
else
    echo not found
fi
NerdMachine
la source
4
Cela ne résout toujours pas le faux positif sur les noms de fichiers qui contiennent des caractères spéciaux glob, comme le souligne Stéphane Chazelas pour la réponse de Dan Bloch. (sauf si vous singe avec nullglob).
Peter Cordes
3
Vous devez éviter -oet -adans test/ [. Par exemple, ici, il échoue si $1c'est =avec la plupart des implémentations. Utilisez [ -e "$1" ] || [ -L "$1" ]plutôt.
Stephane Chazelas
4

Pour simplifier quelque peu la réponse de MYYN, basée sur son idée:

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi
Ken Bloom
la source
4
Fermer, mais que se passe-t-il si vous faites correspondre [a]un fichier nommé [a], mais aucun fichier nommé a? J'aime toujours nullglobça. Certains pourraient considérer cela comme pédant, mais nous pourrions tout aussi bien avoir raison que cela est raisonnable.
Chris Johnsen
@ sondra.kinsey C'est faux; le glob [a]doit seulement correspondre a, pas le nom de fichier littéral [a].
tripleee
4

Sur la base de la réponse de flabdablet , pour moi, il semble que le plus simple (pas nécessairement le plus rapide) consiste à utiliser find lui-même, tout en laissant l'expansion de glob sur le shell, comme:

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

Ou ifcomme:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi
queria
la source
Cela fonctionne exactement comme la version que j'ai déjà présentée en utilisant stat; Je ne sais pas comment trouver est "plus facile" que stat.
flabdablet
3
Soyez conscient que la redirection &> est un bashisme, et fera tranquillement la mauvaise chose dans d'autres coquilles.
flabdablet
Cela semble être mieux que la findréponse de flabdablet car il accepte les chemins dans le glob et il est plus concis (ne nécessite pas -maxdepthetc). Cela semble également meilleur que sa statréponse, car il ne continue pas de faire des statefforts supplémentaires à chaque match de glob supplémentaire. J'apprécierais que quelqu'un puisse contribuer aux cas d'angle où cela ne fonctionne pas.
drwatsoncode
1
Après un examen plus approfondi, j'ajouterais -maxdepth 0car cela permet plus de flexibilité dans l'ajout de conditions. par exemple, supposons que je souhaite restreindre le résultat aux fichiers correspondants uniquement. Je pourrais essayer find $glob -type f -quit, mais cela retournerait vrai si le glob ne correspondait pas à un fichier, mais correspondait à un répertoire qui contenait un fichier (même récursivement). En revanche find $glob -maxdepth 0 -type f -quit, ne retournerait vrai que si le glob lui-même correspondait à au moins un fichier. Notez que cela maxdepthn'empêche pas le glob d'avoir un composant d'annuaire. (FYI 2>est suffisant. Pas besoin de &>)
drwatsoncode
2
Le but de l'utilisation finden premier lieu est d'éviter que le shell génère et trie une liste potentiellement énorme de correspondances globales; find -name ... -quitcorrespondra à au plus un nom de fichier. Si un script repose sur la transmission d'une liste de correspondances glob générées par le shell à find, l'invocation findn'entraîne rien d'autre qu'une surcharge de démarrage de processus inutile. Le simple fait de tester directement la liste résultante pour vérifier qu'elle n'est pas vide sera plus rapide et plus clair.
flabdablet
4

J'ai encore une autre solution:

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

Cela fonctionne bien pour moi. Y a-t-il des cas d'angle qui me manquent?

SaschaZorn
la source
2
Fonctionne sauf si le fichier est en fait nommé 'glob *'.
Ian Kelling
fonctionne pour passer glob comme variable - donne une erreur "trop ​​d'arguments" lorsqu'il y a plus d'une correspondance. "$ (echo $ GLOB)" ne renvoie pas une seule chaîne ou du moins il n'est pas interprété comme un seul, d'où l'erreur de trop d'arguments
DKebler
@DKebler: il doit être interprété comme une chaîne unique, car il est entouré de guillemets doubles.
user1934428
3

Dans Bash, vous pouvez glob à un tableau; si le glob ne correspond pas, votre tableau contiendra une seule entrée qui ne correspond pas à un fichier existant:

#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

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

Remarque: si vous avez nullglobdéfini, scriptssera un tableau vide, et vous devriez tester avec [ "${scripts[*]}" ]ou avec à la [ "${#scripts[*]}" != 0 ]place. Si vous écrivez une bibliothèque qui doit fonctionner avec ou sans nullglob, vous voudrez

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

Un avantage de cette approche est que vous disposez alors de la liste des fichiers avec lesquels vous souhaitez travailler, plutôt que de devoir répéter l'opération glob.

Toby Speight
la source
Pourquoi, avec nullglob défini et le tableau éventuellement vide, ne pouvez-vous toujours pas tester avec if [ -e "${scripts[0]}" ]...? Autorisez -vous également la possibilité de définir un ensemble de noms d' options de shell ?
johnraff
@johnraff, oui, je suppose normalement qu'il nounsetest actif. De plus, il peut être (légèrement) moins cher de tester que la chaîne n'est pas vide que de vérifier la présence d'un fichier. Peu probable cependant, étant donné que nous venons d'effectuer un glob, ce qui signifie que le contenu du répertoire devrait être frais dans le cache du système d'exploitation.
Toby Speight
1

Cette abomination semble fonctionner:

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

Cela nécessite probablement bash, pas sh.

Cela fonctionne car l'option nullglob oblige le glob à évaluer une chaîne vide s'il n'y a pas de correspondance. Ainsi, toute sortie non vide de la commande echo indique que le glob correspond à quelque chose.

Ryan C. Thompson
la source
Vous devez utiliserif [ "`echo *py`" != "*py"]
yegle
1
Cela ne fonctionnerait pas correctement s'il y avait un fichier appelé *py.
Ryan C. Thompson
S'il n'y a pas de fin de fichier py, `echo *py`sera évalué *py.
2014
1
Oui, mais il le fera également si un seul fichier est appelé *py, ce qui est un mauvais résultat.
Ryan C. Thompson,
Corrigez-moi si je me trompe, mais s'il n'y a pas de fichier qui correspond *py, votre script fera écho à "Glob matched"?
yegle
1

Je n'ai pas vu cette réponse, alors j'ai pensé l'avoir mise là:

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

Explication

Lorsqu'il n'y a pas de correspondance pour glob*, alors $1contiendra 'glob*'. Le test -f "$1"ne sera pas vrai car leglob* fichier n'existe pas.

Pourquoi c'est mieux que des alternatives

Cela fonctionne avec sh et ses dérivés: ksh et bash. Il ne crée aucun sous-shell. $(..)et les `...`commandes créent un sous-shell; ils bifurquent un processus, et sont donc plus lents que cette solution.

joseyluis
la source
1

Comme ça dans (fichiers de test contenant 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

C'est bien mieux que compgen -G: parce que nous pouvons discriminer plus de cas et plus précisément.

Peut fonctionner avec un seul caractère générique *

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

Notez que cela peut prendre beaucoup de temps s'il y a beaucoup de correspondances ou si l'accès aux fichiers est lent.

Florian Diesch
la source
1
Cela donnera la mauvaise réponse si un modèle similaire [a]est utilisé lorsque le fichier [a]est présent et que le fichier aest absent. Il dira «trouvé» même si le seul fichier auquel il doit correspondre a, n'est pas réellement présent.
Chris Johnsen
Cette version devrait fonctionner dans un POSIX / bin / sh ordinaire (sans bashismes), et dans le cas où j'en ai besoin, le glob n'a pas de parenthèses de toute façon, et je n'ai pas besoin de me soucier des cas qui sont terriblement pathologique. Mais je suppose qu'il n'y a pas de bonne façon de tester si des fichiers correspondent à un glob.
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
la source
2
Cette version échoue lorsque précisément un fichier correspond, mais vous pouvez éviter le kludge FOUND = -1 en utilisant l' nullgloboption shell.
Ken Bloom
@Ken: Hmm, je n'appellerais pas nullglobun kludge. La comparaison d'un seul résultat avec le modèle d'origine est un kludge (et sujette à de faux résultats), ce nullglobn'est pas le cas.
Chris Johnsen
@Chris: Je pense que vous avez mal lu. Je n'ai pas appelé nullglobun kludge.
Ken Bloom
1
@Ken: En effet, j'ai mal lu. Veuillez accepter mes excuses pour mes critiques invalides.
Chris Johnsen
-1
(ls glob* &>/dev/null && echo Files found) || echo No file found
Damodharan R
la source
5
Renvoie également false si des répertoires correspondent glob*et, par exemple, vous n'avez pas l'écriture pour répertorier ces répertoires.
Stephane Chazelas
-1

ls | grep -q "glob.*"

Ce n'est pas la solution la plus efficace (s'il y a une tonne de fichiers dans le répertoire, cela peut être lent), mais c'est simple, facile à lire et a également l'avantage que les expressions régulières sont plus puissantes que les modèles de glob bash ordinaires.

jesjimher
la source
-2
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true
Otocan
la source
1
Pour une meilleure réponse, essayez d'ajouter des explications à votre code.
Masoud Rahimi