Wie kann ich beim Pattern Matching in einer Unix / Linux-Shell inverse oder negative Platzhalter verwenden?

325

Angenommen, ich möchte den Inhalt eines Verzeichnisses mit Ausnahme von Dateien und Ordnern kopieren, deren Namen das Wort "Musik" enthalten.

cp [exclude-matches] *Music* /target_directory

Was sollte anstelle von [Ausschluss-Übereinstimmungen] geschehen, um dies zu erreichen?

Benutzer4812
quelle

Antworten:

373

In Bash Sie können es durch die Freigabe - extglobOption, so (ersetzen lsmit cpund das Zielverzeichnis hinzufügen, natürlich)

~/foobar> shopt extglob
extglob        off
~/foobar> ls
abar  afoo  bbar  bfoo
~/foobar> ls !(b*)
-bash: !: event not found
~/foobar> shopt -s extglob  # Enables extglob
~/foobar> ls !(b*)
abar  afoo
~/foobar> ls !(a*)
bbar  bfoo
~/foobar> ls !(*foo)
abar  bbar

Sie können extglob später mit deaktivieren

shopt -u extglob
Vinko Vrsalovic
quelle
14
Ich mag diese Funktion:ls /dir/*/!(base*)
Erick Robertson
6
Wie schließt man alles ein ( ) und schließt es auch aus! (B )?
Elijah Lynn
4
Wie würden Sie sagen, alles, was anfängt f, außer foo?
Noldorin
8
Warum ist dies standardmäßig deaktiviert?
weberc2
3
shopt -o -u histexpand Wenn Sie nach Dateien mit Ausrufezeichen suchen müssen - on ist standardmäßig aktiviert, extglob ist standardmäßig deaktiviert, damit histexpand nicht beeinträchtigt wird. In den Dokumenten wird erläutert, warum dies so ist. passe alles an, was mit f beginnt, außer foo: f! (oo), natürlich würde 'food' immer noch übereinstimmen (du würdest f! (oo *) brauchen, um Dinge zu stoppen, die in 'foo' beginnen oder wenn du loswerden willst von bestimmten Dingen, die mit '.foo' enden! ( .foo) oder mit dem Präfix: myprefix! ( .foo) (entspricht myprefixBLAH, aber nicht myprefixBLAH.foo)
osirisgothra
227

Mit der extglobShell-Option erhalten Sie einen leistungsstärkeren Mustervergleich in der Befehlszeile.

Sie schalten es mit ein shopt -s extglobund aus shopt -u extglob.

In Ihrem Beispiel würden Sie zunächst Folgendes tun:

$ shopt -s extglob
$ cp !(*Music*) /target_directory

Die gesamte verfügbare ext beendet glob bing Operatoren sind (Auszug aus man bash):

Wenn die Extglob-Shell-Option mithilfe des integrierten shopt aktiviert ist, werden mehrere erweiterte Mustervergleichsoperatoren erkannt. Eine Musterliste ist eine Liste von einem oder mehreren Mustern, die durch ein | getrennt sind. Zusammengesetzte Muster können unter Verwendung eines oder mehrerer der folgenden Untermuster gebildet werden:

  • (Musterliste)
    Entspricht null oder einem Vorkommen der angegebenen Muster
  • * (Musterliste)
    Entspricht null oder mehr Vorkommen der angegebenen Muster
  • + (Musterliste)
    Entspricht einem oder mehreren Vorkommen der angegebenen Muster
  • @ (Musterliste)
    Entspricht einem der angegebenen Muster
  • ! (Musterliste)
    Entspricht allem außer einem der angegebenen Muster

So zum Beispiel, wenn Sie alle Dateien im aktuellen Verzeichnis aufzulisten wollen , die nicht .coder .hDateien, würden Sie tun:

$ ls -d !(*@(.c|.h))

Natürlich funktioniert normales Shell-Globing, daher könnte das letzte Beispiel auch wie folgt geschrieben werden:

$ ls -d !(*.[ch])
tzot
quelle
1
Was ist der Grund für -d?
Big McLargeHuge
2
@Koveras für den Fall, dass eine der .coder .hDateien ein Verzeichnis ist.
Zot
@ DaveKennedy Hiermit wird alles im aktuellen Verzeichnis aufgelistet D, jedoch nicht der Inhalt von Unterverzeichnissen, die möglicherweise im Verzeichnis enthalten sind D.
spurra
23

Nicht in Bash (von dem ich weiß), aber:

cp `ls | grep -v Music` /target_directory

Ich weiß, dass dies nicht genau das ist, wonach Sie gesucht haben, aber es wird Ihr Beispiel lösen.

ejgottl
quelle
Standardmäßig werden mehrere Dateien pro Zeile abgelegt, was wahrscheinlich nicht zu den richtigen Ergebnissen führt.
Daniel Bungert
10
Nur wenn stdout ein Terminal ist. Bei Verwendung in einer Pipeline gibt ls einen Dateinamen pro Zeile aus.
Adam Rosenfield
Bei der Ausgabe an ein Terminal werden nur mehrere Dateien pro Zeile abgelegt. Probieren Sie es selbst aus - "ls | less" enthält niemals mehrere Dateien pro Zeile.
SpoonMeiser
3
Es funktioniert nicht für Dateinamen, die Leerzeichen (oder andere weiße Leerzeichen) enthalten.
Zot
7

Wenn Sie die Mem-Kosten für die Verwendung des Befehls exec vermeiden möchten, können Sie mit xargs meiner Meinung nach besser abschneiden. Ich denke, das Folgende ist eine effizientere Alternative zu

find foo -type f ! -name '*Music*' -exec cp {} bar \; # new proc for each exec



find . -maxdepth 1 -name '*Music*' -prune -o -print0 | xargs -0 -i cp {} dest/
Steve
quelle
6

In Bash shopt -s extglobist die GLOBIGNOREVariable eine Alternative zu . Es ist nicht wirklich besser, aber ich finde es einfacher, mich zu erinnern.

Ein Beispiel, das das Originalplakat wollte:

GLOBIGNORE="*techno*"; cp *Music* /only_good_music/

Wenn Sie fertig sind unset GLOBIGNORE, können Sie rm *techno*im Quellverzeichnis.

mivk
quelle
5

Sie können auch eine ziemlich einfache forSchleife verwenden:

for f in `find . -not -name "*Music*"`
do
    cp $f /target/dir
done
Mipadi
quelle
1
Dies führt einen rekursiven Fund durch, der sich anders verhält als das, was OP will.
Adam Rosenfield
1
Verwendung -maxdepth 1für nicht rekursive?
Avtomaton
Ich fand, dass dies die sauberste Lösung ist, ohne Shell-Optionen aktivieren / deaktivieren zu müssen. Die Option -maxdepth wird in diesem Beitrag empfohlen, um das vom OP benötigte Ergebnis zu erzielen. Dies hängt jedoch davon ab, was Sie erreichen möchten.
David Lapointe
Die Verwendung findin Backticks wird auf unangenehme Weise unterbrochen, wenn nicht triviale Dateinamen gefunden werden.
Tripleee
5

Meine persönliche Präferenz ist die Verwendung von grep und dem while-Befehl. Auf diese Weise können Sie leistungsstarke und dennoch lesbare Skripte schreiben, die sicherstellen, dass Sie genau das tun, was Sie möchten. Außerdem können Sie mit einem Echo-Befehl einen Probelauf durchführen, bevor Sie den eigentlichen Vorgang ausführen. Zum Beispiel:

ls | grep -v "Music" | while read filename
do
echo $filename
done

druckt die Dateien aus, die Sie am Ende kopieren werden. Wenn die Liste korrekt ist, besteht der nächste Schritt darin, den Echo-Befehl wie folgt durch den Kopierbefehl zu ersetzen:

ls | grep -v "Music" | while read filename
do
cp "$filename" /target_directory
done
Abid H. Mujtaba
quelle
1
Dies funktioniert so lange, wie Ihre Dateinamen keine Tabulatoren, Zeilenumbrüche, mehr als ein Leerzeichen in einer Reihe oder Backslashes enthalten. Während dies pathologische Fälle sind, ist es gut, sich der Möglichkeit bewusst zu sein. In können bashSie verwenden while IFS='' read -r filename, aber dann sind Zeilenumbrüche immer noch ein Problem. Im Allgemeinen ist es am besten, keine lsDateien aufzulisten. Werkzeuge wie findsind viel besser geeignet.
Thedward
Ohne zusätzliche Werkzeuge:for file in *; do case ${file} in (*Music*) ;; (*) cp "${file}" /target_directory ; echo ;; esac; done
Thedward
mywiki.wooledge.org/ParsingLs listet eine Reihe weiterer Gründe auf, warum Sie dies vermeiden sollten.
Tripleee
5

Ein Trick, den ich hier noch nicht gesehen habe und der nicht verwendet extglobwird findoder der darin grepbesteht, zwei Dateilisten als Mengen zu behandeln und sie mit folgenden Elementen zu "diff".comm

comm -23 <(ls) <(ls *Music*)

commist vorzuziehen, diffweil es keine zusätzliche Kruft hat.

Dies gibt alle Elemente von Satz 1 zurück, lsdie nicht auch in Satz 2 enthalten sind ls *Music*. Dies erfordert, dass beide Sätze sortiert sind, damit sie ordnungsgemäß funktionieren. Kein Problem für lsund Glob-Erweiterung, aber wenn Sie so etwas verwenden find, rufen Sie unbedingt auf sort.

comm -23 <(find . | sort) <(find . | grep -i '.jpg' | sort)

Potenziell nützlich.

James M. Lay
quelle
1
Einer der Vorteile des Ausschlusses besteht darin, das Verzeichnis überhaupt nicht zu durchlaufen. Diese Lösung durchläuft zwei Unterverzeichnisse - eines mit Ausschluss und eines ohne.
Mark Stosberg
Sehr guter Punkt, @MarkStosberg. Ein Nebeneffekt dieser Technik ist jedoch, dass Sie Ausschlüsse aus einer tatsächlichen Datei lesen können, z. B.comm -23 <(ls) exclude_these.list
James M. Lay
3

Eine Lösung hierfür finden Sie mit find.

$ mkdir foo bar
$ touch foo/a.txt foo/Music.txt
$ find foo -type f ! -name '*Music*' -exec cp {} bar \;
$ ls bar
a.txt

Find hat eine ganze Reihe von Optionen. Sie können ziemlich genau festlegen, was Sie ein- und ausschließen.

Bearbeiten: Adam in den Kommentaren festgestellt, dass dies rekursiv ist. Suchoptionen mindepth und maxdepth können hilfreich sein, um dies zu steuern.

Daniel Bungert
quelle
Dies führt eine rekursive Kopie durch, was ein anderes Verhalten darstellt. Außerdem wird für jede Datei ein neuer Prozess erstellt, der für eine große Anzahl von Dateien sehr ineffizient sein kann.
Adam Rosenfield
Die Kosten für das Laichen eines Prozesses betragen ungefähr null im Vergleich zu allen E / A-Vorgängen, die beim Kopieren jeder Datei generiert werden. Ich würde also sagen, dass dies für den gelegentlichen Gebrauch gut genug ist.
Dland
Einige Problemumgehungen für das Laichen des Prozesses: stackoverflow.com/questions/186099/…
Vinko Vrsalovic
Verwenden Sie "-maxdepth 1", um eine Rekursion zu vermeiden.
Ejgottl
Verwenden Sie Backticks, um das Analogon der Shell-Wildcard-Erweiterung zu erhalten: cp find -maxdepth 1 -not -name '*Music*'/ target_directory
ejgottl
2

Die folgenden Arbeiten listen alle auf *.txt Dateien im aktuellen , mit Ausnahme derjenigen, die mit einer Zahl beginnen.

Dies funktioniert in bash, dash, zshund alle anderen POSIX - kompatibelen Muscheln.

for FILE in /some/dir/*.txt; do    # for each *.txt file
    case "${FILE##*/}" in          #   if file basename...
        [0-9]*) continue ;;        #   starts with digit: skip
    esac
    ## otherwise, do stuff with $FILE here
done
  1. In Zeile 1 /some/dir/*.txtbewirkt das Muster , dass die forSchleife alle Dateien durchläuft, /some/dirderen Name mit endet .txt.

  2. In Zeile zwei wird eine case-Anweisung verwendet, um unerwünschte Dateien auszusortieren. - Der ${FILE##*/}Ausdruck entfernt alle führenden Verzeichnisnamenkomponenten vom Dateinamen (hier /some/dir/), sodass Muster nur mit dem Basisnamen der Datei übereinstimmen können. (Wenn Sie nur Dateinamen basierend auf Suffixen aussortieren, können Sie diese $FILEstattdessen auf kürzen .)

  3. In Zeile drei werden alle Dateien, die dem caseMuster [0-9]*entsprechen, übersprungen (die continueAnweisung springt zur nächsten Iteration der forSchleife). - Wenn Sie möchten, können Sie hier etwas Interessanteres tun, z. B. alle Dateien überspringen, die nicht mit einem Buchstaben (a - z) beginnen [!a-z]*, oder Sie können mehrere Muster verwenden, um verschiedene Arten von Dateinamen [0-9]*|*.bakzu überspringen, z. B. um Dateien in beiden .bakDateien zu überspringen und Dateien, die nicht mit einer Nummer beginnen.

zrajm
quelle
Doh! Es gab einen Fehler (ich habe gegen *.txtstatt nur abgestimmt *). Jetzt behoben.
Zrajm
0

Dies würde es tun, ohne genau 'Musik' auszuschließen.

cp -a ^'Music' /target

dies und das, um Dinge wie Musik auszuschließen? * oder *? Musik

cp -a ^\*?'complete' /target
cp -a ^'complete'?\* /target
gabreal
quelle
Die cpHandbuchseite unter MacOS hat eine -aOption, macht aber etwas ganz anderes. Welche Plattform unterstützt dies?
Tripleee