Konwertuj glob na „znajdź”

11

Ciągle miałem ten problem: mam glob, który pasuje dokładnie do prawidłowych plików, ale powoduje Command line too long. Za każdym razem, gdy konwertowałem go na jakąś kombinację findi grepktóra działa w konkretnej sytuacji, ale która nie jest w 100% równoważna.

Na przykład:

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

Czy istnieje narzędzie do przekształcania globów w findwyrażenia, których nie jestem świadomy? Czy też istnieje opcja finddopasowania globu bez dopasowania tej samej globuli w podkatalogu (np. foo/*.jpgNiedozwolone jest dopasowanie bar/foo/*.jpg)?

Ole Tange
źródło
Rozwiń nawias klamrowy i powinieneś być w stanie używać wynikowych wyrażeń za pomocą -pathlub -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'powinien działać - oprócz tego, że będzie pasował /fooz/blah/bar/quuxA/pic1234d.jpg. Czy to będzie problem?
muru
Tak, to będzie problem. Musi być w 100% równoważny.
Ole Tange
Problem polega na tym, że nie mamy pojęcia, jaka jest dokładnie różnica. Twój wzór jest całkiem w porządku.
peterh - Przywróć Monikę
Dodałem Twój post z rozszerzeniem jako odpowiedź na pytanie. Mam nadzieję, że nie jest tak źle.
peterh - Przywróć Monikę
Nie możesz tego zrobić echo <glob> | cat, zakładając, że wiem o bash, echo jest
wbudowane

Odpowiedzi:

15

Jeśli problem polega na tym, że pojawia się błąd, że lista argumentów jest za długa, użyj pętli lub wbudowanej powłoki. Podczas gdy command glob-that-matches-too-muchmoże wystąpić błąd, for f in glob-that-matches-too-muchnie, więc możesz po prostu:

for f in foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg
do
    something "$f"
done

Pętla może być bardzo powolna, ale powinna działać.

Lub:

printf "%s\0" foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg |
  xargs -r0 something

( printfjest wbudowane w większość powłok, powyższe działa wokół ograniczenia execve()wywołania systemowego)

$ cat /usr/share/**/* > /dev/null
zsh: argument list too long: cat
$ printf "%s\n" /usr/share/**/* | wc -l
165606

Działa również z bash. Nie jestem jednak pewien, gdzie dokładnie to jest udokumentowane.


Zarówno Vima, jak glob2regpat()i Pythona fnmatch.translate()mogą konwertować globusy na wyrażenia regularne, ale oba używają również .*do *dopasowywania w poprzek /.

muru
źródło
Jeśli to prawda, to zastąpienie somethingze echopowinien to zrobić.
Ole Tange
1
@OleTange Właśnie dlatego zasugerowałem printf- będzie szybszy niż dzwonienie echotysiące razy i oferuje większą elastyczność.
muru
4
Istnieje ograniczenie liczby argumentów, które można przekazać exec, które dotyczą poleceń zewnętrznych, takich jak cat; ale ten limit nie dotyczy wbudowanych poleceń powłoki, takich jak printf.
Stephen Kitt,
1
@OleTange Linia nie jest zbyt długa, ponieważ printfjest wbudowana, a powłoki prawdopodobnie używają tej samej metody do dostarczania argumentów, której używają do wyliczania argumentów for. catnie jest wbudowany.
muru
1
Technicznie istnieją powłoki takie jak mkshgdzie printfnie jest wbudowane i powłoki takie jak ksh93gdzie catjest (lub może być) wbudowane. Zobacz także zargsw, zshaby obejść to bez konieczności uciekania się xargs.
Stéphane Chazelas,
9

find(dla predykatów -name/ -pathstandardowych) używa wzorców symboli wieloznacznych, podobnie jak globs (zauważ, że {a,b}nie jest operatorem glob; po rozwinięciu otrzymujesz dwa globusy). Główną różnicą jest obsługa ukośników (oraz plików kropkowych i katalogów, które nie są specjalnie traktowane find). *w globach nie obejmuje kilku katalogów. */*/*spowoduje wyświetlenie maksymalnie 2 poziomów katalogów. Dodanie -path './*/*/*'spowoduje dopasowanie do plików o głębokości co najmniej 3 poziomów i nie przestanie findwyświetlać zawartości dowolnego katalogu na dowolnej głębokości.

Do tego konkretnego

./foo*bar/quux[A-Z]{.bak,}/pic[0-9][0-9][0-9][0-9]?.jpg

kilka globów, łatwo to przetłumaczyć, potrzebujesz katalogów na głębokości 3, więc możesz użyć:

find . -mindepth 3 -maxdepth 3 \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

(lub -depth 3z niektórymi findimplementacjami). Lub POSIXly:

find . -path './*/*/*' -prune \
       \( -path './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' -o \
          -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg' \) \
       -exec cmd {} +

Co zagwarantowałoby, że te *i ?nie będą pasowały do /postaci.

(w findprzeciwieństwie do globów odczytywałoby zawartość katalogów innych niż foo*barte w bieżącym katalogu¹, a nie sortowało listy plików. Ale pomijając problem, to, co jest dopasowane [A-Z]lub zachowanie */ ?w odniesieniu do nieprawidłowych znaków jest nieokreślony, otrzymasz tę samą listę plików).

Ale w każdym razie, jak pokazał @muru , nie trzeba uciekać się, findjeśli chodzi tylko o podzielenie listy plików na kilka przebiegów, aby obejść limit execve()wywołania systemowego. Niektóre powłoki, takie jak zsh(z zargs) lub ksh93(z command -x) mają nawet wbudowaną obsługę tego.

Z zsh(którego globusy mają również odpowiedniki -type fi większość innych findpredykatów), na przykład:

autoload zargs # if not already in ~/.zshrc
zargs ./foo*bar/quux[A-Z](|.bak)/pic[0-9][0-9][0-9][0-9]?.jpg(.) -- cmd

( (|.bak)Jest to sprzeczne operator glob do {,.bak}The (.)glob kwalifikator jest odpowiednikiem find„s -type f, dodać oNtam pominąć sortowania jak z find, Daby to kropka pliki (nie stosuje się do tego glob))


¹ Aby findprzeszukiwać drzewo katalogów tak jak globs, potrzebujesz czegoś takiego:

find . ! -name . \( \
  \( -path './*/*' -o -name 'foo*bar' -o -prune \) \
  -path './*/*/*' -prune -name 'pic[0-9][0-9][0-9][0-9]?.jpg' -exec cmd {} + -o \
  \( ! -path './*/*' -o -name 'quux[A-Z]' -o -name 'quux[A-Z].bak' -o -prune \) \)

To jest przycinanie wszystkich katalogów na poziomie 1, z wyjątkiem foo*bartych, i wszystkie na poziomie 2, z wyjątkiem quux[A-Z]lub quux[A-Z].bak, a następnie wybierz pic...te na poziomie 3 (i przycinaj wszystkie katalogi na tym poziomie).

Stéphane Chazelas
źródło
3

Możesz napisać wyrażenie regularne dla znalezienia pasującego do twoich wymagań:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
źródło
Czy istnieje narzędzie, które dokonuje tej konwersji, aby uniknąć błędów ludzkich?
Ole Tange
Nie, ale tylko zmienia zrobiłem było uciec ., dodać opcjonalny mecz dla .baki zmiany *do [^/]*nie pasuje do ścieżki jak / foo / foo / bar itd.
sebasth
Ale nawet twoje nawrócenie jest błędne. ? nie zmienia się na [^ /]. To jest właśnie rodzaj błędu ludzkiego, którego chcę uniknąć.
Ole Tange
1
Myślę, że z egrep, można skrócić [0-9][0-9][0-9][0-9]?do[0-9]{3,4}
wjandrea
0

Uogólniając uwagę na moją drugą odpowiedź , jako bardziej bezpośrednią odpowiedź na twoje pytanie, możesz użyć tego shskryptu POSIX do konwersji globu na findwyrażenie:

#! /bin/sh -
glob=${1#./}
shift
n=$#
p='./*'

while true; do
  case $glob in
    (*/*)
      set -- "$@" \( ! -path "$p" -o -path "$p/*" -o -name "${glob%%/*}" -o -prune \)
      glob=${glob#*/} p=$p/*;;
    (*)
      set -- "$@" -path "$p" -prune -name "$glob"
      while [ "$n" -gt 0 ]; do
        set -- "$@" "$1"
        shift
        n=$((n - 1))
      done
      break;;
  esac
done
find . "$@"

Do użycia z jednym standardowym shglobem (więc nie z dwoma globami twojego przykładu, który używa interpretacji nawiasów ):

glob2find './foo*bar/quux[A-Z].bak/pic[0-9][0-9][0-9][0-9]?.jpg' \
  -type f -exec cmd {} +

(co nie ignoruje plików-kropek ani katalogów-kropek oprócz .i ..nie sortuje listy plików).

Ten działa tylko z globs względem bieżącego katalogu, bez .lub ze ..składnikami. Przy odrobinie wysiłku możesz rozszerzyć go na dowolną glob, więcej niż glob ... Można to również zoptymalizować, aby glob2find 'dir/*'nie wyglądało dirtak samo jak wzorzec.

Stéphane Chazelas
źródło