Verifica se un glob ha delle corrispondenze in bash


223

Se voglio verificare l'esistenza di un singolo file, posso provarlo usando test -e filenameo [ -e filename ].

Supponiamo di avere un glob e voglio sapere se esistono file i cui nomi corrispondono al glob. Il glob può abbinare 0 file (nel qual caso non devo fare nulla) oppure può corrispondere a 1 o più file (nel qual caso devo fare qualcosa). Come posso verificare se un glob ha delle corrispondenze? (Non mi importa quante partite ci siano, e sarebbe meglio se potessi farlo con una iffrase e senza loop (semplicemente perché trovo che sia più leggibile).

( test -e glob*non riesce se il glob corrisponde a più di un file.)


3
Sospetto che la mia risposta di seguito sia "chiaramente corretta" in un modo che tutti gli altri tipi di hack-around. È una shell costruita su una riga che esiste da sempre e sembra essere "lo strumento previsto per questo particolare lavoro". Sono preoccupato che gli utenti facciano riferimento erroneamente alla risposta accettata qui. Qualcuno per favore sentiti libero di correggermi e ritirerò il mio commento qui, sono più che felice di sbagliarmi e imparare da esso. Se la differenza non fosse così drastica, non solleverei questo problema.
Brian Chrisman,

1
Le mie soluzioni preferite a questa domanda sono il comando find che funziona in qualsiasi shell (anche shell non Bourne) ma richiede GNU find e il comando compgen che è chiaramente un Bashismo. Peccato che non posso accettare entrambe le risposte.
Ken Bloom,

Nota: questa domanda è stata modificata da quando è stata posta. Il titolo originale era "Verifica se un glob ha delle corrispondenze in bash". La shell specifica, 'bash', è stata abbandonata dalla domanda dopo aver pubblicato la mia risposta. La modifica del titolo della domanda fa apparire la mia risposta errata. Spero che qualcuno possa modificare o almeno affrontare questo cambiamento.
Brian Chrisman,

Risposte:


178

Soluzione specifica per Bash :

compgen -G "<glob-pattern>"

Sfuggire allo schema o verrà pre-espanso nelle partite.

Lo stato di uscita è:

  • 1 per nessuna corrispondenza,
  • 0 per "una o più partite"

stdoutè un elenco di file corrispondenti al glob .
Penso che questa sia l'opzione migliore in termini di concisione e minimizzazione dei potenziali effetti collaterali.

AGGIORNAMENTO : esempio di utilizzo richiesto.

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi

9
Si noti che compgenè un comando incorporato specifico per bash e non fa parte dei comandi integrati specificati dalla shell Unix standard POSIX. pubs.opengroup.org/onlinepubs/9699919799 pubs.opengroup.org/onlinepubs/9699919799/utilities/… Pertanto, evita di usarlo negli script in cui la portabilità ad altre shell è un problema.
Diomidis Spinellis,

1
Mi sembra che un effetto simile senza builtin bash sarebbe quello di usare qualsiasi altro comando che agisce su un glob e fallisce se nessun file corrisponde, come ls: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi- forse utile per il codice golf? Non riesce se esiste un file con lo stesso nome del glob, al quale il glob non avrebbe dovuto corrispondere, ma in questo caso probabilmente hai problemi più grandi.
Dewi Morgan,

4
@DewiMorgan Questo è più semplice:if ls /tmp/*Files &> /dev/null; then echo exists; fi
Clay Bridges

Per i dettagli su compgen, vedere man basho conhelp compgen
el-teedee,

2
sì, citalo o il carattere jolly del nome file sarà pre-espanso. compgen "dir / *. ext"
Brian Chrisman,

169

L'opzione shell nullglob è davvero un bashismo.

Per evitare la necessità di un noioso salvataggio e ripristino dello stato nullglob, lo imposto solo all'interno della subshell che espande il glob:

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

Per una migliore portabilità e un globbing più flessibile, usare find:

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

Le azioni esplicite -quit -quit vengono utilizzate per trovare anziché l' azione predefinita -print implicita in modo che find venga chiuso non appena trova il primo file che corrisponde ai criteri di ricerca. Laddove molti file corrispondono, questo dovrebbe essere eseguito molto più velocemente di echo glob*o ls glob*ed evita anche la possibilità di sovrastampare la riga di comando espansa (alcune shell hanno un limite di lunghezza 4K).

Se find sembra eccessivo e il numero di file che possono corrispondere è piccolo, usa stat:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi

10
findsembra essere esattamente corretto. Non ha casi angolari, poiché la shell non sta eseguendo l'espansione (e passando un glob non espanso ad altri comandi), è portatile tra le shell (anche se apparentemente non tutte le opzioni che usi sono specificate da POSIX), ed è più veloce di ls -d glob*(la precedente risposta accettata) perché si ferma quando raggiunge la prima partita.
Ken Bloom,

1
Nota che questa risposta potrebbe richiedere un shopt -u failglobdato che queste opzioni sembrano essere in conflitto in qualche modo.
Calimo,

La findsoluzione corrisponderà anche a un nome file senza caratteri glob. In questo caso, è quello che volevo. Solo qualcosa di cui essere consapevoli però.
Siamo tutti Monica il

1
Dal momento che qualcun altro ha deciso di modificare la mia risposta per farlo dire, a quanto pare.
flabdablet,

1
unix.stackexchange.com/questions/275637/… discute come sostituire l' -maxdepthopzione per una ricerca 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

2
Per evitare un possibile falso "nessuna corrispondenza" impostato nullglobinvece di verificare se un singolo risultato è uguale al modello stesso. Alcuni pattern possono corrispondere a nomi esattamente uguali al pattern stesso (ad es a*b., Ma non ad es. a?bO [a]).
Chris Johnsen,

Suppongo che ciò fallisca nell'improbabile possibilità che esista effettivamente un file chiamato glob. (es. qualcuno correva touch '*py'), ma questo mi indica un'altra buona direzione.
Ken Bloom,

Mi piace questa come la versione più generale.
Ken Bloom,

E anche il più corto. Se ti aspetti solo una partita, puoi usarla "$M"come scorciatoia per "${M[0]}". Altrimenti, hai già l'espansione glob in una variabile array, quindi sei gtg per passarlo ad altre cose come un elenco, invece di farli espandere nuovamente il glob.
Peter Cordes,

Bello. Puoi testare M più rapidamente (meno byte e senza generare un [processo) conif [[ $M ]]; then ...
Tobia,

22

mi piace

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

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

Questo è sia leggibile che efficiente (a meno che non ci sia un numero enorme di file).
Lo svantaggio principale è che è molto più sottile di quanto sembri, e a volte mi sento in dovere di aggiungere un lungo commento.
Se c'è una corrispondenza, "glob*"viene espansa dalla shell e tutte le partite vengono passate exists(), il che controlla la prima e ignora il resto.
Se non vi è alcuna corrispondenza, "glob*"viene passato exists()e non viene trovato neanche lì.

Modifica: potrebbe esserci un falso positivo, vedi commento


13
Può restituire un falso positivo se il glob è qualcosa di simile *.[cC](potrebbe non esserci co Cfile, ma un file chiamato *.[cC]) o falso negativo se il primo file espanso da quello è ad esempio un collegamento simbolico a un file inesistente o a un file in un directory a cui non si ha accesso (si desidera aggiungere a || [ -L "$1" ]).
Stephane Chazelas,

Interessante. Shellcheck riferisce che il globbing funziona solo con -e0 o 1 corrispondenze. Non funziona per più partite, perché sarebbe diventato [ -e file1 file2 ]e questo non avrebbe funzionato. Vedi anche github.com/koalaman/shellcheck/wiki/SC2144 per la logica e le soluzioni suggerite.
Thomas Praxl

10

Se hai impostato globfail puoi usare questo pazzo (cosa che non dovresti davvero)

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

o

q=( * ) && echo 0 || echo 1

2
Un uso fantastico di un noop che fallisce. Non dovrebbe mai essere usato ... ma davvero bello. :)
Brian Chrisman il

Puoi mettere lo shopt dentro le parentesi. In questo modo influenza solo il test:(shopt -s failglob; : *) 2>/dev/null && echo exists
flabdablet

8

test -e ha lo sfortunato avvertimento che ritiene che non esistano collegamenti simbolici rotti. Quindi potresti voler controllare anche quelli.

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

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

4
Ciò non risolve ancora i falsi positivi sui nomi dei file che contengono caratteri speciali glob, come Stephane Chazelas indica la risposta di Dan Bloch. (a meno che non si scimmia con nullglob).
Peter Cordes,

3
Dovresti evitare -oed -ain test/ [. Ad esempio, qui, non riesce se $1è =con la maggior parte delle implementazioni. Usa [ -e "$1" ] || [ -L "$1" ]invece.
Stephane Chazelas,

4

Semplificare in qualche modo la risposta di MYYN, in base alla sua idea:

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi

4
Chiudi, ma cosa succede se stai abbinando [a], hai un file chiamato [a], ma nessun file nominato a? Mi piace ancora nullglobper questo. Alcuni potrebbero vederlo come pedante, ma potremmo anche essere tanto corretti quanto ragionevoli.
Chris Johnsen,

@ sondra.kinsey È sbagliato; il glob [a]dovrebbe corrispondere solo a, non il nome letterale del file [a].
Tripleee,

4

Basato sulla risposta di flabdablet , per me sembra che il modo più semplice (non necessariamente più veloce) sia solo quello di trovare se stesso, lasciando l'espansione glob su shell, come:

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

O ifcome:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi

Funziona esattamente come la versione che ho già presentato usando stat; non so come trovare "sia più facile" di stat.
flabdablet,

3
Tieni presente che il reindirizzamento è un bashismo e farà tranquillamente la cosa sbagliata in altre shell.
flabdablet,

Questo sembra essere migliore della findrisposta di flabdablet perché accetta percorsi nel glob ed è più conciso (non richiede -maxdepthecc.). Sembra anche migliore della sua statrisposta perché non continua a fare il statmassimo su ogni partita glob aggiuntiva. Gradirei se qualcuno potesse contribuire ai casi angolari in cui ciò non funziona.
drwatsoncode

1
Dopo ulteriori considerazioni, aggiungerei -maxdepth 0perché consente una maggiore flessibilità nell'aggiunta di condizioni. ad esempio, suppongo di voler limitare il risultato solo ai file corrispondenti. Potrei provare find $glob -type f -quit, ma ciò sarebbe vero se il glob NON corrispondesse a un file, ma corrispondesse a una directory che conteneva un file (anche in modo ricorsivo). Al contrario find $glob -maxdepth 0 -type f -quit, restituirebbe vero solo se il glob stesso corrispondesse ad almeno un file. Nota che maxdepthnon impedisce al glob di avere un componente di directory. (FYI 2>è sufficiente. Non è necessario &>)
drwatsoncode

2
Il punto di utilizzare findin primo luogo è evitare che la shell generi e ordini un elenco potenzialmente enorme di corrispondenze glob; find -name ... -quitcorrisponderà al massimo a un nome file. Se uno script si basa sul passaggio a un elenco di corrispondenze glob generate dalla shell find, invocare findnon ottiene nient'altro che un sovraccarico di avvio del processo non necessario. Semplicemente testando direttamente l'elenco risultante per non-vuoto sarà più veloce e più chiaro.
flabdablet,

4

Ho ancora un'altra soluzione:

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

Questo funziona bene per me. Ci sono alcuni casi angolari che mi mancano?


2
Funziona tranne se il file è effettivamente chiamato 'glob *'.
Ian Kelling,

funziona per passare glob come variabile - dà l'errore "troppi argomenti" quando c'è più di una corrispondenza. "$ (echo $ GLOB)" non restituisce una singola stringa o almeno non viene interpretato come singolo singolo, quindi l'errore di troppi argomenti
DKebler,

@DKebler: dovrebbe essere interpretato come stringa singola, perché racchiuso tra virgolette doppie.
user1934428

3

In Bash, puoi glob a un array; se il glob non corrisponde, il tuo array conterrà una singola voce che non corrisponde a un file esistente:

#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

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

Nota: se hai nullglobimpostato, scriptssarà un array vuoto e dovresti testare con [ "${scripts[*]}" ]o con [ "${#scripts[*]}" != 0 ]invece. Se stai scrivendo una libreria che deve funzionare con o senza nullglob, ti consigliamo

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

Un vantaggio di questo approccio è che hai quindi l'elenco dei file con cui vuoi lavorare, piuttosto che dover ripetere l'operazione glob.


Perché, con nullglob impostato e l'array eventualmente vuoto, non è ancora possibile eseguire il test if [ -e "${scripts[0]}" ]...? Stai anche consentendo la possibilità di impostare il set di nomi delle opzioni di shell ?
johnraff,

@johnraff, sì, presumo normalmente nounsetsia attivo. Inoltre, potrebbe essere (leggermente) più economico verificare che la stringa non sia vuota che verificare la presenza di un file. Tuttavia, è improbabile, dato che abbiamo appena eseguito un glob, il che significa che il contenuto della directory dovrebbe essere aggiornato nella cache del sistema operativo.
Toby Speight,

1

Questo abominio sembra funzionare:

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

Probabilmente richiede bash, non sh.

Questo funziona perché l'opzione nullglob fa sì che il glob valuti una stringa vuota se non ci sono corrispondenze. Pertanto, qualsiasi output non vuoto dal comando echo indica che il glob corrisponde a qualcosa.


Dovresti usareif [ "`echo *py`" != "*py"]
yegle il

1
Ciò non funzionerebbe correttamente se esistesse un file chiamato *py.
Ryan C. Thompson,

Se non è presente alcun file con fine py, `echo *py`verrà valutato *py.
yegle,

1
Sì, ma lo farà anche se viene chiamato un singolo file *py, che è il risultato errato.
Ryan C. Thompson,

Correggimi se sbaglio, ma se non ci sono file corrispondenti *py, il tuo script farà eco "Glob matched"?
yegle

1

Non ho visto questa risposta, quindi ho pensato di metterlo là fuori:

set -- glob*
[ -f "$1" ] && echo "found $@"

1
set -- glob*
if [ -f "$1" ]; then
  echo "It matched"
fi

Spiegazione

Quando non c'è una corrispondenza per glob*, allora $1conterrà 'glob*'. Il test -f "$1"non sarà vero perché il glob*file non esiste.

Perché questo è meglio delle alternative

Funziona con sh e derivati: ksh e bash. Non crea alcuna sotto-shell. $(..)e i `...`comandi creano una sotto-shell; forzano un processo e quindi sono più lenti di questa soluzione.


1

In questo modo (file di test contenenti 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

È molto meglio di compgen -G: perché possiamo discriminare più casi e più precisamente.

Può funzionare con un solo carattere jolly *


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

Nota che ciò può richiedere molto tempo se ci sono molte corrispondenze o l'accesso ai file è lento.


1
Questo darà la risposta sbagliata se [a]viene usato un modello simile quando il file [a]è presente e il file aè assente. Dirà "trovato" anche se l'unico file che dovrebbe corrispondere, in arealtà non è presente.
Chris Johnsen,

Questa versione dovrebbe funzionare in un normale POSIX / bin / sh (senza bashismi), e nel caso in cui ne abbia bisogno, il glob non ha comunque parentesi e non devo preoccuparmi dei casi che sono terribilmente patologico. Ma immagino che non ci sia un buon modo per testare se qualche file corrisponde a 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

2
Questa versione ha esito negativo quando un file corrisponde esattamente, ma puoi evitare FOUND = -1 kludge usando l' nullglobopzione shell.
Ken Bloom,

@Ken: Hmm, non definirei nullglobun kludge. Confrontare un singolo risultato con il modello originale è un kludge (e incline a risultati falsi), usando nullglobnon lo è.
Chris Johnsen,

@ Chris: Penso che tu abbia letto male. Non ho chiamato nullglobun Kludge.
Ken Bloom,

1
@Ken: In effetti, ho letto male. Ti prego di accettare le mie scuse per le mie critiche non valide.
Chris Johnsen,

-1
(ls glob* &>/dev/null && echo Files found) || echo No file found

5
Restituirebbe anche falso se ci sono directory corrispondenti glob*e per esempio non hai la scrittura per elencare quelle directory.
Stephane Chazelas,

-1

ls | grep -q "glob.*"

Non è la soluzione più efficiente (se ci sono tonnellate di file nella directory potrebbe essere lenta), ma è semplice, facile da leggere e ha anche il vantaggio che le regex sono più potenti dei semplici pattern glob bash.


-2
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true

1
Per una risposta migliore prova ad aggiungere qualche spiegazione al tuo codice.
Masoud Rahimi,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.