Sprawdź, czy glob ma jakieś dopasowania w bash

223

Jeśli chcę sprawdzić, czy istnieje pojedynczy plik, mogę go przetestować za pomocą test -e filenamelub [ -e filename ].

Załóżmy, że mam glob i chcę wiedzieć, czy istnieją jakieś pliki, których nazwy pasują do globu. Glob może dopasować 0 plików (w takim przypadku nie muszę nic robić) lub może dopasować 1 lub więcej plików (w takim przypadku muszę coś zrobić). Jak mogę sprawdzić, czy glob ma jakieś dopasowania? (Nie obchodzi mnie, ile jest dopasowań, i najlepiej byłoby, gdybym mógł to zrobić za pomocą jednej ifinstrukcji i bez pętli (po prostu dlatego, że uważam to za najbardziej czytelne).

( test -e glob*kończy się niepowodzeniem, jeśli glob pasuje do więcej niż jednego pliku).

Ken Bloom
źródło
3
Podejrzewam, że moja odpowiedź poniżej jest „wyraźnie poprawna” w sposób, w jaki wszyscy inni hack-wokół. Jest to jednowierszowa wbudowana powłoka, która istnieje od zawsze i wydaje się być „zamierzonym narzędziem do tego konkretnego zadania”. Obawiam się, że użytkownicy omyłkowo odnoszą się do zaakceptowanej odpowiedzi tutaj. Ktoś, proszę, popraw mnie i wycofam swój komentarz tutaj, jestem więcej niż szczęśliwy, że się mylę i uczę się z niego. Gdyby różnica nie wydawała się tak drastyczna, nie podniósłbym tego problemu.
Brian Chrisman,
1
Moje ulubione rozwiązania tego pytania to polecenie find, które działa w dowolnej powłoce (nawet w powłokach innych niż Bourne), ale wymaga GNU find, oraz polecenie compgen, które jest wyraźnie baszizmem . Szkoda, że ​​nie mogę zaakceptować obu odpowiedzi.
Ken Bloom
Uwaga: To pytanie było edytowane, odkąd zostało zadane. Pierwotny tytuł brzmiał: „Sprawdź, czy glob ma jakieś dopasowania w bash”. Konkretna powłoka „bash” została usunięta z pytania po opublikowaniu mojej odpowiedzi. Edycja tytułu pytania sprawia, że ​​moja odpowiedź wydaje się błędna. Mam nadzieję, że ktoś może zmienić lub przynajmniej rozwiązać tę zmianę.
Brian Chrisman,

Odpowiedzi:

178

Rozwiązanie specyficzne dla Bash :

compgen -G "<glob-pattern>"

Uciekaj przed schematem, bo zostanie wstępnie rozszerzony na mecze.

Status wyjścia to:

  • 1 za brak dopasowania,
  • 0 dla „jednego lub więcej dopasowań”

stdoutto lista plików pasujących do globu .
Myślę, że jest to najlepsza opcja pod względem zwięzłości i minimalizacji potencjalnych skutków ubocznych.

AKTUALIZACJA : Wymagane przykładowe użycie.

if compgen -G "/tmp/someFiles*" > /dev/null; then
    echo "Some files exist."
fi
Brian Chrisman
źródło
9
Zauważ, że compgenjest to wbudowane polecenie specyficzne dla bash i nie jest częścią wbudowanych poleceń uniksowego standardu powłoki POSIX. pubs.opengroup.org/onlinepubs/9699919799 pubs.opengroup.org/onlinepubs/9699919799/utilities/… Dlatego unikaj używania go w skryptach, w których problemem jest możliwość przenoszenia do innych powłok.
Diomidis Spinellis
1
Wydaje mi się, że podobnym efektem bez wbudowanych bashów byłoby użycie dowolnego innego polecenia, które działa na glob i kończy się niepowodzeniem, jeśli nie pasują żadne pliki, takie jak ls: if ls /tmp/*Files 2>&1 >/dev/null; then echo exists; fi- może przydatny do gry w golfa? Nie powiedzie się, jeśli istnieje plik o nazwie takiej samej jak glob, który glob nie powinien był dopasować, ale w takim przypadku prawdopodobnie masz większe problemy.
Dewi Morgan
4
@DewiMorgan To jest prostsze:if ls /tmp/*Files &> /dev/null; then echo exists; fi
Clay Bridges
Aby uzyskać szczegółowe informacje na temat compgen, patrz man bashlub zhelp compgen
el-teedee
2
tak, podaj to, bo symbol wieloznaczny nazwy pliku zostanie wstępnie rozwinięty. compgen "reż / *. ext"
Brian Chrisman
169

Opcja powłoki nullglob jest rzeczywiście bastyzmem.

Aby uniknąć konieczności żmudnego zapisywania i przywracania stanu nullglob, umieściłem go tylko w podpowłoce, która rozszerza glob:

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

Aby uzyskać lepszą przenośność i bardziej elastyczne globowanie, użyj find:

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

Jawne -print -quit są używane dla find zamiast domyślnej niejawnej akcji -print, dzięki czemu find zakończy działanie, gdy tylko znajdzie pierwszy plik spełniający kryteria wyszukiwania. Tam, gdzie pasuje wiele plików, powinno to działać znacznie szybciej niż echo glob*lub, ls glob*a także pozwala uniknąć przeciążenia rozszerzonej linii poleceń (niektóre powłoki mają limit długości 4K).

Jeśli find wydaje się przesadny, a liczba plików, które mogą się zgadzać, jest niewielka, użyj statystyki:

if stat -t glob* >/dev/null 2>&1
then
    echo found
else
    echo not found
fi
flabdablet
źródło
10
findwydaje się być dokładnie poprawne. Nie ma przypadków narożnych, ponieważ powłoka nie wykonuje ekspansji (i przekazuje nierozwiniętą glob do jakiegoś innego polecenia), jest przenośna między powłokami (choć najwyraźniej nie wszystkie używane opcje są określone przez POSIX) i jest szybsza niż ls -d glob*(poprzednio zaakceptowana odpowiedź), ponieważ zatrzymuje się, gdy osiągnie pierwszy mecz.
Ken Bloom
1
Pamiętaj, że ta odpowiedź może wymagać, shopt -u failglobponieważ te opcje wydają się w jakiś sposób kolidować.
Calimo
findRozwiązanie pozwoli dopasować nazwę pliku bez znaków glob, jak również. W tym przypadku właśnie tego chciałem. Tylko coś, o czym należy pamiętać.
We Are All Monica,
1
Ponieważ ktoś inny zdecydował się zredagować moją odpowiedź, aby tak powiedzieć.
flabdablet
1
unix.stackexchange.com/questions/275637/… omawia, jak zastąpić -maxdepthopcję znalezienia 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
źródło
2
Aby uniknąć możliwego ustawienia fałszywego „brak dopasowania” nullglobzamiast sprawdzania, czy pojedynczy wynik jest równy samemu wzorowi. Niektóre wzorce mogą pasować do nazw, które są dokładnie równe samemu wzorowi (np. a*bAle nie np. a?bLub [a]).
Chris Johnsen
Przypuszczam, że to się nie powiedzie na bardzo mało prawdopodobne, że tak naprawdę istnieje plik o nazwie glob. (np. ktoś uciekł touch '*py'), ale to wskazuje mi inny dobry kierunek.
Ken Bloom
Podoba mi się ta jako najbardziej ogólna wersja.
Ken Bloom
A także najkrótszy. Jeśli oczekujesz tylko jednego meczu, możesz użyć go "$M"jako skrótu "${M[0]}". W przeciwnym razie masz już rozszerzenie glob w zmiennej tablicowej, więc jesteś gtg za przekazanie go do innych rzeczy jako listy, zamiast zmuszania ich do ponownego rozwinięcia globu.
Peter Cordes
Miły. Możesz przetestować M szybciej (mniej bajtów i bez odradzania [procesu) za pomocąif [[ $M ]]; then ...
Tobia
22

podoba mi się

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

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

Jest to zarówno czytelne, jak i wydajne (chyba że istnieje ogromna liczba plików).
Główną wadą jest to, że jest znacznie bardziej subtelny, niż się wydaje, i czasami czuję się zmuszony do dodania długiego komentarza.
Jeśli istnieje dopasowanie, "glob*"jest ono rozszerzane przez powłokę, a wszystkie dopasowania są przekazywane do exists(), który sprawdza pierwszy i ignoruje resztę.
Jeśli nie ma dopasowania, "glob*"zostaje przekazane exists()i nie istnieje.

Edycja: może być fałszywie pozytywny, patrz komentarz

Dan Bloch
źródło
13
To może powrócić fałszywie dodatni jeśli glob jest coś *.[cC](może nie być club Cplik, ale plik o nazwie *.[cC]) lub fałszywie ujemne jeśli pierwszy plik rozbudowany od tego jest na przykład dowiązanie do pliku unexistent lub do pliku w katalog, do którego nie masz dostępu (możesz dodać a || [ -L "$1" ]).
Stephane Chazelas,
Ciekawy. Shellcheck informuje, że globowanie działa tylko -ewtedy, gdy występuje 0 lub 1 dopasowania. Nie działa w przypadku wielu dopasowań, ponieważ tak się stanie, [ -e file1 file2 ]a to się nie powiedzie. Zobacz także uzasadnienie i sugerowane rozwiązania na github.com/koalaman/shellcheck/wiki/SC2144 .
Thomas Praxl
10

Jeśli masz ustawiony globfail, możesz użyć tego szalonego (czego naprawdę nie powinieneś)

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

lub

q=( * ) && echo 0 || echo 1
James
źródło
2
Fantastyczne wykorzystanie awarii noop. Nigdy nie powinien być używany ... ale naprawdę piękny. :)
Brian Chrisman,
Możesz umieścić shopt w parens. W ten sposób wpływa tylko na test:(shopt -s failglob; : *) 2>/dev/null && echo exists
flabdablet
8

test -e ma niefortunne zastrzeżenie, które uważa, że ​​zerwane łącza symboliczne nie istnieją. Więc możesz też sprawdzić te.

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

if globexists glob*; then
    echo found
else
    echo not found
fi
NerdMachine
źródło
4
To wciąż nie naprawia fałszywie pozytywnych nazw plików zawierających globalne znaki specjalne, jak Stephane Chazelas wskazuje na odpowiedź Dana Blocha. (chyba że małpujesz z nullglob).
Peter Cordes
3
Należy unikać -oi -aw test/ [. Na przykład tutaj nie powiedzie się, jeśli $1jest =z większością implementacji. Użyj [ -e "$1" ] || [ -L "$1" ]zamiast tego.
Stephane Chazelas
4

Aby nieco uprościć odpowiedź MYYN, w oparciu o jego pomysł:

M=(*py)
if [ -e ${M[0]} ]; then
  echo Found
else
  echo Not Found
fi
Ken Bloom
źródło
4
Zamknij, ale co jeśli pasujesz [a], masz plik o nazwie [a], ale nie ma nazwy pliku a? Nadal lubię nullglobto. Niektórzy mogą postrzegać to jako pedantyczne, ale równie dobrze możemy mieć rację.
Chris Johnsen
@ sondra.kinsey To źle; glob [a]powinien tylko pasować a, a nie dosłowna nazwa pliku [a].
tripleee
4

Opierając się na odpowiedzi flabdablet , dla mnie wygląda na to, że najłatwiejszym (niekoniecznie najszybszym) jest po prostu użycie find , pozostawiając globalną ekspansję na powłoce, na przykład:

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

Lub w if:

if find $yourGlob -quit &> /dev/null; then
    echo "MATCH"
else
    echo "NOT-FOUND"
fi
queria
źródło
Działa to dokładnie tak, jak wersja, którą już przedstawiłem za pomocą statystyki; nie jestem pewien, jak znalezienie jest „łatwiejsze” niż stat.
flabdablet
3
Pamiętaj, że przekierowanie &> to bashism i po cichu zrobi coś złego w innych powłokach.
flabdablet
Wydaje się to być lepsze niż findodpowiedź flabdablet, ponieważ akceptuje ścieżki w glob i jest bardziej zwięzła (nie wymaga -maxdepthitp.). Wydaje się również lepsza niż jego statodpowiedź, ponieważ nie kontynuuje statdopełniania każdego dodatkowego dopasowania globalnego. Byłbym wdzięczny, gdyby ktoś mógł wnieść wkład w sprawy narożne, w których to nie działa.
drwatsoncode
1
Po dalszych rozważaniach dodam, -maxdepth 0ponieważ pozwala to na większą elastyczność w dodawaniu warunków. np. załóżmy, że chcę ograniczyć wynik tylko do pasujących plików. Mógłbym spróbować find $glob -type f -quit, ale zwróciłoby to wartość true, gdyby glob NIE pasował do pliku, ale pasował do katalogu zawierającego plik (nawet rekurencyjnie). W przeciwieństwie do find $glob -maxdepth 0 -type f -quittego zwróci prawdę tylko wtedy, gdy sam glob pasuje do co najmniej jednego pliku. Zauważ, że maxdepthto nie uniemożliwia globowi posiadania komponentu katalogu. (FYI 2>jest wystarczająca. Nie ma potrzeby &>)
drwatsoncode
2
Najważniejsze findjest, aby uniknąć generowania przez powłokę i sortowania potencjalnie ogromnej listy dopasowań globalnych; find -name ... -quitdopasuje co najwyżej jedną nazwę pliku. Jeśli skrypt polega na przekazaniu wygenerowanej przez powłokę listy dopasowań globalnych do find, wywoływanie powoduje findjedynie zbędne obciążenie związane z uruchomieniem procesu. Proste testowanie wynikowej listy pod kątem braku pustki będzie szybsze i bardziej przejrzyste.
flabdablet
4

Mam jeszcze inne rozwiązanie:

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

To mi ładnie działa. Czy brakuje mi niektórych narożnych skrzynek?

SaschaZorn
źródło
2
Działa, chyba że plik ma nazwę „glob *”.
Ian Kelling,
działa w przypadku przekazywania glob jako zmiennej - powoduje błąd „zbyt wielu argumentów”, gdy występuje więcej niż jedno dopasowanie. „$ (echo $ GLOB)” nie zwraca ani jednego ciągu znaków, a przynajmniej nie jest interpretowane jako pojedynczy pojedynczy, a więc błąd zbyt wielu argumentów
DKebler 1'18
@DKebler: należy interpretować jako pojedynczy ciąg, ponieważ jest zawinięty w cudzysłów.
user1934428
3

W Bash możesz globować do tablicy; jeśli glob nie pasuje, twoja tablica będzie zawierać pojedynczy wpis, który nie odpowiada istniejącemu plikowi:

#!/bin/bash

shellglob='*.sh'

scripts=($shellglob)

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

Uwaga: jeśli nullglobustawiłeś, scriptsbędzie pusta tablica i zamiast tego powinieneś przetestować za pomocą [ "${scripts[*]}" ]lub za pomocą [ "${#scripts[*]}" != 0 ]. Jeśli piszesz bibliotekę, która musi pracować z lub bez nullglob, będziesz tego potrzebować

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

Zaletą tego podejścia jest to, że masz listę plików, z którymi chcesz pracować, zamiast konieczności powtarzania operacji glob.

Toby Speight
źródło
Dlaczego, z ustawionym nullglobem i prawdopodobnie pustą tablicą, nie możesz nadal testować if [ -e "${scripts[0]}" ]...? Czy zezwalasz również na możliwość ustawienia zestawu rzeczowników w opcji powłoki ?
johnraff,
@ johnraff, tak, zwykle zakładam, że nounsetjest aktywny. Ponadto może być (nieco) tańsze przetestowanie łańcucha niepustego niż sprawdzenie obecności pliku. Mało prawdopodobne, biorąc pod uwagę, że właśnie wykonaliśmy glob, co oznacza, że ​​zawartość katalogu powinna być świeża w pamięci podręcznej systemu operacyjnego.
Toby Speight
1

Ta obrzydliwość wydaje się działać:

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

Prawdopodobnie wymaga bash, a nie sh.

Działa to, ponieważ opcja nullglob powoduje, że glob ocenia na pusty łańcuch, jeśli nie ma żadnych dopasowań. Zatem wszelkie niepuste dane wyjściowe polecenia echo wskazują, że glob coś pasuje.

Ryan C. Thompson
źródło
Powinieneś użyćif [ "`echo *py`" != "*py"]
yegle
1
To nie działałoby poprawnie, gdyby istniał plik o nazwie *py.
Ryan C. Thompson
Jeśli nie ma końca pliku py, `echo *py`zostanie oszacowane na *py.
yegle
1
Tak, ale zrobi to również, jeśli istnieje jeden plik o nazwie *py, co jest złym wynikiem.
Ryan C. Thompson
Popraw mnie, jeśli się mylę, ale jeśli nie ma pasującego pliku *py, twój skrypt wyświetli echo „Glob dopasowany”?
yegle
1

Nie widziałem tej odpowiedzi, więc pomyślałem, że ją tam opublikuję:

set -- glob*
[ -f "$1" ] && echo "found $@"
Brad Howes
źródło
1
set -- glob*
if [ -f "$1" ]; then
  echo "It matched"
fi

Wyjaśnienie

Jeśli nie ma dopasowania glob*, wówczas $1będzie zawierać 'glob*'. Test -f "$1"nie będzie prawdziwy, ponieważ glob*plik nie istnieje.

Dlaczego jest to lepsze niż alternatywy

Działa to z sh i pochodnymi: ksh i bash. Nie tworzy żadnej podpowłoki. $(..)a `...`polecenia tworzą podpowłokę; rozwidlają proces i dlatego są wolniejsze niż to rozwiązanie.

joseyluis
źródło
1

Tak to w (pliki testowe zawierające 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

Jest to o wiele lepsze niż compgen -G: ponieważ możemy dyskryminować więcej przypadków i dokładniej.

Może działać tylko z jednym znakiem wieloznacznym *

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

Pamiętaj, że może to być bardzo czasochłonne, jeśli istnieje wiele dopasowań lub dostęp do pliku jest wolny.

Florian Diesch
źródło
1
To da złą odpowiedź, jeśli wzorzec podobny [a]jest używany, gdy plik [a]jest obecny, a plik ajest nieobecny. Powie „znaleziony”, mimo że jedyny plik, który powinien pasować, anie jest tak naprawdę obecny.
Chris Johnsen
Ta wersja powinna działać w zwykłym POSIX / bin / sh (bez bashizmów), aw przypadku, gdy potrzebuję jej, glob i tak nie ma nawiasów i nie muszę się martwić o przypadki, które są strasznie patologiczne. Ale chyba nie ma jednego dobrego sposobu na sprawdzenie, czy jakieś pliki pasują do globu.
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
źródło
2
Ta wersja kończy się niepowodzeniem, gdy dokładnie jeden plik pasuje, ale można uniknąć kludge FOUND = -1 za pomocą nullglobopcji powłoki.
Ken Bloom
@Ken: Hmm, nie nazwałbym nullglobkludge. Porównywanie pojedynczego wyniku do oryginalnego wzoru jest kludge (i podatne na fałszywe wyniki), użycie nullglobnie jest.
Chris Johnsen
@Chris: Myślę, że źle odczytałeś. Nie zadzwoniłem nullglobdo kludge.
Ken Bloom
1
@Ken: Rzeczywiście źle odczytałem. Proszę przyjąć przeprosiny za moją nieważną krytykę.
Chris Johnsen
-1
(ls glob* &>/dev/null && echo Files found) || echo No file found
Damodharan R.
źródło
5
Zwraca również wartość false, jeśli istnieją pasujące katalogi glob*i na przykład nie masz zapisu, aby wyświetlić te katalogi.
Stephane Chazelas,
-1

ls | grep -q "glob.*"

Nie jest to najbardziej wydajne rozwiązanie (jeśli w katalogu jest mnóstwo plików, może być powolne), ale jest proste, łatwe do odczytania, a ponadto ma tę zaletę, że wyrażenia regularne są bardziej wydajne niż zwykłe globalne wzorce bash.

jesjimher
źródło
-2
[ `ls glob* 2>/dev/null | head -n 1` ] && echo true
otocan
źródło
1
Aby uzyskać lepszą odpowiedź, spróbuj dodać wyjaśnienie do swojego kodu.
MR.