Konvertiere glob in `find`

11

Ich hatte immer wieder dieses Problem: Ich habe einen Glob, der genau den richtigen Dateien entspricht, aber Ursachen hat Command line too long. Jedes Mal , wenn ich es auf eine Kombination von umgestellt habe findund grepdass die Arbeiten für die besondere Situation, die aber nicht 100% entspricht.

Beispielsweise:

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

Gibt es ein Tool zum Konvertieren von Globs in findAusdrücke, die mir nicht bekannt sind? Oder gibt es eine Option findzum Abgleichen des Globs, ohne dass derselbe Glob in einem Unterverzeichnis übereinstimmt (z. B. foo/*.jpgdarf er nicht übereinstimmen bar/foo/*.jpg)?

Ole Tange
quelle
Erweitern Sie die Klammer, und Sie sollten in der Lage sein, die resultierenden Ausdrücke mit -pathoder zu verwenden -ipath. find . -path './foo*bar/quux[A-Z]/pic[0-9][0-9][0-9][0-9]?.jpg'sollte funktionieren - außer dass es passt /fooz/blah/bar/quuxA/pic1234d.jpg. Wird das ein Problem sein?
Muru
Ja, das wird ein Problem sein. Es muss 100% äquivalent sein.
Ole Tange
Das Problem ist, dass wir keine Ahnung haben, was genau der Unterschied ist. Dein Muster ist ziemlich in Ordnung.
Peterh - Wiedereinsetzung Monica
Ich habe Ihren Erweiterungsbeitrag als Antwort auf die Frage hinzugefügt. Ich hoffe es ist nicht so schlimm.
Peterh
Kannst du nicht tun echo <glob> | cat, vorausgesetzt mein Wissen über Bash, Echo ist
eingebaut

Antworten:

15

Wenn das Problem darin besteht, dass Sie einen Fehler erhalten, bei dem die Argumentliste zu lang ist, verwenden Sie eine Schleife oder eine integrierte Shell. Während command glob-that-matches-too-muchFehler auftreten können, for f in glob-that-matches-too-muchnicht, so können Sie einfach tun:

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

Die Schleife mag unerträglich langsam sein, aber sie sollte funktionieren.

Oder:

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

( printfDa dies in den meisten Shells integriert ist, umgeht das oben Gesagte die Einschränkung des execve()Systemaufrufs.)

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

Funktioniert auch mit Bash. Ich bin mir jedoch nicht sicher, wo genau dies dokumentiert ist.


Sowohl Vims glob2regpat()als auch Pythons fnmatch.translate()können Globs in reguläre Ausdrücke konvertieren, aber beide verwenden sie auch .*für *Übereinstimmungen zwischen /.

muru
quelle
Wenn das wahr ist, dann ersetzt somethingmit echosollte es tun.
Ole Tange
1
@OleTange Deshalb habe ich vorgeschlagen printf- es ist schneller als echotausende Male anzurufen und bietet mehr Flexibilität.
Muru
4
Es gibt eine Grenze für die Argumente, die durchlaufen werden können exec, was für externe Befehle gilt, wie z cat. Diese Begrenzung gilt jedoch nicht für Shell-integrierte Befehle wie printf.
Stephen Kitt
1
@OleTange Die Zeile ist nicht zu lang, da sie integriert printfist, und die Shells verwenden vermutlich dieselbe Methode zum Bereitstellen von Argumenten, die sie zum Auflisten von Argumenten verwenden for. catist kein eingebauter.
Muru
1
Technisch gesehen gibt es Muscheln wie mkshwo printfnicht eingebaut ist und Muscheln wie ksh93wo gebaut catist (oder sein kann). Siehe auch zargsin zsh, um es zu umgehen, ohne darauf zurückgreifen zu müssen xargs.
Stéphane Chazelas
9

find(für die Prädikate -name/ -pathstandard) verwendet Platzhaltermuster wie Globs (beachten Sie, dass dies {a,b}kein Glob-Operator ist; nach der Erweiterung erhalten Sie zwei Globs). Der Hauptunterschied ist die Behandlung von Schrägstrichen (und Punktdateien und Verzeichnissen, die nicht speziell behandelt werden find). *in Globs werden nicht mehrere Verzeichnisse überspannt. */*/*Dadurch werden bis zu 2 Verzeichnisebenen aufgelistet. Das Hinzufügen von a entspricht -path './*/*/*'allen Dateien, die mindestens 3 Ebenen tief sind, und hört nicht auf, findden Inhalt eines Verzeichnisses in irgendeiner Tiefe aufzulisten.

Für diesen besonderen

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

Ein paar Globs, es ist einfach zu übersetzen. Sie möchten Verzeichnisse in Tiefe 3, damit Sie Folgendes verwenden können:

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 {} +

(oder -depth 3mit einigen findImplementierungen). Oder 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 {} +

Welches würde garantieren, dass diese *und ?nicht /Zeichen übereinstimmen konnten .

(Im findGegensatz zu Globs würde der Inhalt anderer Verzeichnisse als foo*barder im aktuellen Verzeichnis¹ gelesen und die Liste der Dateien nicht sortiert. Wenn wir jedoch das Problem außer Acht lassen, dass das, was übereinstimmt, [A-Z]oder das Verhalten von */ ?in Bezug auf ungültige Zeichen ist nicht spezifiziert, würden Sie die gleiche Liste von Dateien erhalten).

Wie @muru gezeigt hat , muss auf keinen Fall darauf zurückgegriffen werden, findwenn nur die Liste der Dateien in mehrere Läufe aufgeteilt werden soll, um das Limit des execve()Systemaufrufs zu umgehen. Einige Shells wie zsh(mit zargs) oder ksh93(mit command -x) haben sogar eine eingebaute Unterstützung dafür.

Mit zsh(deren Globs auch das Äquivalent -type fund die meisten anderen findPrädikate haben), zum Beispiel:

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)Ist ein glob Betreiber entgegen {,.bak}, das (.)glob Qualifier die äquivalent ist find‚s -type f, fügen Sie oNdort mit der Sortierung wie zu überspringen find, Dschließen Punkt-Dateien (gilt nicht für diesen glob))


¹ Um findden Verzeichnisbaum wie Globs zu crawlen, benötigen Sie Folgendes:

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 \) \)

Das heißt beschneiden alle Verzeichnisse auf Stufe 1 bis auf die foo*bareine, und alle auf Stufe 2 , mit Ausnahme der quux[A-Z]oder quux[A-Z].bakEinsen, und wählen Sie die pic...diejenigen auf der Ebene 3 (und beschneiden alle Verzeichnisse auf dieser Ebene).

Stéphane Chazelas
quelle
3

Sie können eine Regex schreiben, um eine Suche zu finden, die Ihren Anforderungen entspricht:

find . -regextype egrep -regex './foo[^/]*bar/quux[A-Z](\.bak)?/pic[0-9][0-9][0-9][0-9][^/]?\.jpg'
sebasth
quelle
Gibt es ein Tool, das diese Konvertierung durchführt, um menschliche Fehler zu vermeiden?
Ole Tange
Nein, aber die einzigen Änderungen, die ich vorgenommen habe, waren das Entkommen ., das Hinzufügen der optionalen Übereinstimmung für .bakund die Änderung *von [^/]*, um Pfaden wie / foo / foo / bar usw. nicht zu entsprechen.
18.
Aber auch Ihre Konvertierung ist falsch. ? wird nicht in [^ /] geändert. Dies ist genau die Art von menschlichem Fehler, die ich vermeiden möchte.
Ole Tange
1
Ich denke mit egrep können Sie [0-9][0-9][0-9][0-9]?auf[0-9]{3,4}
wjandrea
1
@OleTange Siehe Regex aus Glob-Ausdruck erstellen
wjandrea
0

Wenn Sie den Hinweis zu meiner anderen Antwort als direktere Antwort auf Ihre Frage verallgemeinern , können Sie dieses POSIX- shSkript verwenden, um den Glob in einen findAusdruck zu konvertieren :

#! /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 . "$@"

Um mit verwendet werden , ein Standard - shglob (also nicht die beiden Klackse Ihres Beispiel , das verwendet Klammer Erweiterung ):

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

(das ignoriert keine Punktdateien oder Punktverzeichnisse außer .und ..sortiert die Liste der Dateien nicht).

Dieser funktioniert nur mit Globs relativ zum aktuellen Verzeichnis, ohne .oder mit ..Komponenten. Mit etwas Aufwand können Sie es auf jeden Globus erweitern, mehr als auf einen Globus ... Das könnte auch so optimiert werden, dass glob2find 'dir/*'es nicht so aussieht dirwie bei einem Muster.

Stéphane Chazelas
quelle