Verwendung einer generierten Liste von Dateinamen als Argumentliste - mit Leerzeichen

16

Ich versuche, ein Skript mit einer Liste von Dateinamen aufzurufen, die von gesammelt wurden find. Nichts Besonderes, nur so etwas:

$ myscript `find . -name something.txt`

Das Problem ist, dass einige der Pfadnamen Leerzeichen enthalten, sodass sie bei der Argumenterweiterung in zwei ungültige Namen aufgeteilt werden. Normalerweise würde ich die Namen mit Anführungszeichen umgeben, aber hier werden sie durch die Backquote-Erweiterung eingefügt. Ich habe versucht, die Ausgabe findjedes Dateinamens mit Anführungszeichen zu filtern , aber bis Bash sie sieht, ist es zu spät, sie zu entfernen, und sie werden als Teil des Dateinamens behandelt:

$ myscript `find . -name something.txt | sed 's/.*/"&"/'`
No such file or directory: '"./somedir/something.txt"'

Ja, das sind die Regeln für die Verarbeitung der Befehlszeile, aber wie komme ich darum herum?

Das ist peinlich, aber ich finde nicht den richtigen Ansatz. Endlich habe ich herausgefunden, wie es geht xargs -0 -n 10000... aber es ist ein so hässlicher Hack, dass ich immer noch fragen möchte: Wie zitiere ich die Ergebnisse der Backquote-Erweiterung oder erreiche den gleichen Effekt auf eine andere Weise?

Edit: Ich war über die Tatsache verwirrt , dass xargs tut sammle alle Argumente in einer einzigen Argumentliste, sofern es ihm sonst oder Systemgrenzen könnte überschritten werden. Vielen Dank an alle, die mich gerade gesetzt haben! Andere, denken Sie daran, wenn Sie die akzeptierte Antwort lesen, da nicht direkt darauf hingewiesen wird.

Ich habe die Antwort akzeptiert, aber meine Frage bleibt: Gibt es keine Möglichkeit, Leerzeichen bei der Backtick- (oder $(...)) Erweiterung zu schützen ? (Beachten Sie, dass die akzeptierte Lösung keine bash-Antwort ist.)

alexis
quelle
Ich vermute, Sie müssen ändern, was die Shell als Dateinamentrennzeichen verwendet (zum Beispiel, indem Sie mit dem Wert von IFS spielen, besteht eine Möglichkeit darin IFS=", newline, "). Muss das Skript jedoch über alle Dateinamen ausgeführt werden? Wenn nicht, können Sie das Skript für jede Datei mit find selbst ausführen.
njsg
Das IFS zu ändern ist eine großartige Idee, ich habe nicht daran gedacht! Nicht praktisch für die Verwendung in der Befehlszeile, aber dennoch. :-) Und ja, das Ziel ist es, alle Argumente an denselben Aufruf meines Skripts zu übergeben.
Alexis

Antworten:

12

Mit einigen Implementierungen von findund xargswie folgt können Sie Folgendes ausführen .

$ find . -type f -print0 | xargs -r0 ./myscript

oder einfach find:

$ find . -type f -exec ./myscript {} +

Beispiel

Angenommen, ich habe das folgende Beispielverzeichnis.

$ tree
.
|-- dir1
|   `-- a\ file1.txt
|-- dir2
|   `-- a\ file2.txt
|-- dir3
|   `-- a\ file3.txt
`-- myscript

3 directories, 4 files

Sagen wir jetzt, ich habe das für ./myscript.

#!/bin/bash

for i in "$@"; do
    echo "file: $i"
done

Nun, wenn ich den folgenden Befehl ausführen.

$ find . -type f -print0 | xargs -r0 ./myscript 
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Oder wenn ich das 2. Formular so benutze:

$ find . -type f -exec ./myscript {} +
file: ./dir2/a file2.txt
file: ./dir3/a file3.txt
file: ./dir1/a file1.txt
file: ./myscript

Einzelheiten

find + xargs

Die beiden oben genannten Methoden sehen zwar unterschiedlich aus, sind jedoch im Wesentlichen gleich. Das erste ist, die Ausgabe von find zu nehmen und sie mit NULLs ( \0) über den -print0Schalter find zu teilen . Der xargs -0ist speziell dafür ausgelegt, mit NULL geteilte Eingaben zu verarbeiten. Diese Nicht-Standard-Syntax wurde von GNU eingeführt findund xargsist heutzutage auch in einigen anderen wie den neuesten BSDs zu finden. Die -rOption wird benötigt, um einen Aufruf zu vermeiden, myscriptwenn findnichts mit GNU, findaber nicht mit BSDs gefunden wird.

ANMERKUNG: Dieser gesamte Ansatz hängt von der Tatsache ab, dass Sie niemals eine Zeichenfolge passieren, die übermäßig lang ist. Wenn dies der ./myscriptFall ist, wird ein zweiter Aufruf von mit dem Rest der nachfolgenden Ergebnisse von find gestartet.

mit + finden

Das ist die Standardmethode (obwohl sie erst vor relativ kurzer Zeit (2005) zur GNU-Implementierung von hinzugefügt wurde find). Die Fähigkeit zu tun, was wir tun, xargsist buchstäblich eingebaut find. So findwird eine Liste der Dateien finden und dann diese Liste als so viele Argumente übergeben , wie auf den Befehl passen kann nach Angabe -exec(beachten Sie, dass {}nur zuletzt kurz vor sein kann , +in diesem Fall), die Befehle mehrmals ausgeführt wird, wenn nötig.

Warum kein Zitat?

Im ersten Beispiel nehmen wir eine Abkürzung, indem wir die Probleme mit dem Zitieren vollständig vermeiden und die Argumente durch NULL trennen. Wenn xargsdiese Liste angezeigt wird, wird sie angewiesen, die NULL-Werte aufzuteilen, um die einzelnen Befehlsatome effektiv zu schützen.

Im zweiten Beispiel behalten wir die internen Ergebnisse bei findund wissen so, was jedes Dateiatom ist, und garantieren, dass es angemessen behandelt wird, wodurch die Quotierung der Ergebnisse vermieden wird.

Maximale Größe der Kommandozeile?

Diese Frage wird von Zeit zu Zeit gestellt, daher füge ich sie als Bonus zu dieser Antwort hinzu, hauptsächlich, damit ich sie in Zukunft finden kann. Sie können Folgendes verwenden xargs, um die Grenzen der Umgebung zu ermitteln:

$ xargs --show-limits
Your environment variables take up 4791 bytes
POSIX upper limit on argument length (this system): 2090313
POSIX smallest allowable upper limit on argument length (all systems): 4096
Maximum length of command we could actually use: 2085522
Size of command buffer we are actually using: 131072
slm
quelle
1
Vielen Dank, aber ich muss alle Argumente an denselben Aufruf meines Skripts übergeben. Das ist in der Problembeschreibung, aber ich denke, ich habe nicht klargestellt, dass es nicht zufällig ist.
Alexis
@alexis - lesen Sie die Antworten noch einmal, sie übergeben alle Argumente an einen einzelnen Aufruf Ihres Skripts.
SLM
Ich werde verdammt sein! Ich wusste nichts über das +Argument dafür find(und du verwendest es auch +in der Prosa, also habe ich deine Erklärung das erste Mal verpasst). Aber mehr auf den Punkt gebracht, ich hätte falsch verstanden, was xargsstandardmäßig tut !!! In drei Jahrzehnten, in denen ich Unix verwendet habe, hatte ich bis jetzt noch nie eine Verwendung dafür, aber ich dachte, ich kenne meine Toolbox ...
alexis
@alexis - Ich dachte, du hättest das verpasst, was wir gesagt haben. Ja xargsist ein Teufel eines Befehls. Man muss es und seine findManpages viele Male durchlesen, um herauszufinden, was sie können. Mai der Schalter sind gegensätzlich, so dass die Verwirrung beiträgt.
SLM
@alexis - auch eine weitere Sache, die Sie zur Toolbox hinzufügen sollten. Verwenden Sie nicht die Anführungszeichen / Backticks, um verschachtelte Befehle auszuführen, $(..)sondern verwenden Sie jetzt. Es behandelt automatisch das Verschachteln von Anführungszeichen usw. Backticks werden nicht mehr empfohlen.
SLM
3
find . -name something.txt -exec myscript {} +

Im obigen findfindet alle die passenden Dateinamen und stellt sich als Argument an myscript. Dies funktioniert mit Dateinamen unabhängig von Leerzeichen oder anderen ungeraden Zeichen.

Wenn alle Dateinamen in eine Zeile passen, wird myscript einmal ausgeführt. Wenn die Liste zu lang ist, um von der Shell verarbeitet zu werden, führt find myscript nach Bedarf mehrmals aus.

MEHR: Wie viele Dateien passen auf eine Befehlszeile? man findsagt, dass findes Befehlszeilen erstellt, "ähnlich wie xargs seine erstellt". Und man xargsdass die Grenzwerte systemabhängig sind und dass Sie sie durch Ausführen bestimmen können xargs --show-limits. ( getconf ARG_MAXist auch eine möglichkeit). Unter Linux liegt das Limit normalerweise (aber nicht immer) bei 2 Millionen Zeichen pro Befehlszeile.

John1024
quelle
2

Ein paar Ergänzungen zu @ slms feiner Antwort.

Die Beschränkung der Größe der Argumente hängt vom execve(2)Systemaufruf ab (tatsächlich hängt sie von der kumulativen Größe der Argument- und Umgebungszeichenfolgen und -zeiger ab). Wenn myscriptes in einer Sprache geschrieben ist, die Ihre Shell interpretieren kann, müssen Sie es möglicherweise nicht ausführen . Sie können Ihre Shell einfach interpretieren lassen, ohne einen anderen Interpreter ausführen zu müssen.

Wenn Sie das Skript ausführen als:

(. myscript x y)

Es ist wie:

myscript x y

Außer, dass es von einem untergeordneten Element der aktuellen Shell interpretiert wird, anstatt es auszuführen (was schließlich das Ausführen sh (oder was auch immer in der She-Bang-Zeile angegeben ist, falls vorhanden) mit noch mehr Argumenten beinhaltet).

Jetzt können Sie find -exec {} +den .Befehl natürlich nicht mehr verwenden , da .er ein integrierter Befehl der Shell ist und von der Shell und nicht von ausgeführt werden muss find.

Mit zshist es einfach:

IFS=$'\0'
(. myscript $(find ... -print0))

Oder:

(. myscript ${(ps:\0:)"$(find ... -print0)"}

Allerdings zshwürden Sie es nicht findin erster Linie brauchen, da die meisten Funktionen in zshGlobbing integriert sind.

bashVariablen dürfen jedoch keine NUL-Zeichen enthalten, daher müssen Sie einen anderen Weg finden. Ein Weg könnte sein:

files=()
while IFS= read -rd '' -u3 file; do
  files+=("$file")
done 3< <(find ... -print0)
(. myscript "${files[@]}")

Sie können auch rekursives Globbing globstarim zsh-Stil mit der Option in bash4.0 und höher verwenden:

shopt -s globstar failglob dotglob
(. myscript ./**/something.txt)

Beachten Sie, dass **Symlinks zu Verzeichnissen folgten, bis es in bash4.3 behoben wurde . Beachten Sie auch, dass Globbing-Qualifizierer bashnicht implementiert werden, zshsodass Sie dort nicht alle Funktionen nutzen findkönnen.

Eine andere Alternative wäre die Verwendung von GNU ls:

eval "files=(find ... -exec ls -d --quoting-style=shell-always {} +)"
(. myscript "${files[@]}")

Die obigen Verfahren können auch verwendet werden , wenn Sie sicher machen wollen myscriptwird ausgeführt nur einmal (andernfalls , wenn die Argumentliste zu groß ist). In neueren Linux-Versionen können Sie diese Einschränkung in der Argumentliste wie folgt erhöhen und sogar aufheben:

ulimit -s 1048576

(1 GB Stack-Größe, von der ein Viertel für die arg + env-Liste verwendet werden kann).

ulimit -s unlimited

(keine Begrenzung)

Stéphane Chazelas
quelle
1

In den meisten Systemen ist die Länge einer Befehlszeile, die mit xargsoder an ein Programm übergeben wird, begrenzt -exec command {} +. Von man find:

-exec command {} +
      This  variant  of the -exec action runs the specified command on
      the selected files, but the command line is built  by  appending
      each  selected file name at the end; the total number of invoca
      tions of the command will  be  much  less  than  the  number  of
      matched  files.   The command line is built in much the same way
      that xargs builds its command lines.  Only one instance of  `{}'
      is  allowed  within the command.  The command is executed in the
      starting directory.

Invocations werden viel weniger, aber nicht garantiert sein. Was Sie tun sollten, ist, die durch NUL getrennten Dateinamen im Skript von stdin zu lesen, was auf der Grundlage eines Befehlszeilenarguments möglich ist -o -. Ich würde etwas machen wie:

$ find . -name something.txt -print0 | myscript -0 -o -

und implementieren Sie die Optionsargumente myscriptentsprechend.

Timo
quelle
Ja, das Betriebssystem beschränkt die Anzahl / Größe der Argumente, die übergeben werden können. Auf modernen Linux-Systemen ist dies (gigantisch) ( linux.die.net/man/2/execve ) (1/4 der Stapelgröße, 0x7FFFFFFF-Argumente). Die AFAIK-Bash selbst setzt keine Grenzen. Meine Listen sind viel kleiner und mein Problem wurde durch Missverständnisse oder falsche Erinnerungen an die Funktionsweise verursacht xargs. Ihre Lösung ist zwar die robusteste, aber in diesem Fall übertrieben.
Alexis
0

Gibt es keine Möglichkeit, Leerzeichen in der Backtick- (oder $ (...)) - Erweiterung zu schützen?

Nein, das gibt es nicht. Warum das?

Bash hat keine Möglichkeit zu wissen, was geschützt werden sollte und was nicht.

Es gibt keine Arrays in der Unix-Datei / Pipe. Es ist nur ein Bytestream. Der Befehl innerhalb von ``oder $()gibt einen Stream aus, der schluckt und als einzelne Zeichenfolge behandelt. In diesem Fall haben Sie nur zwei Möglichkeiten: Setzen Sie es in Anführungszeichen, um es als eine Zeichenfolge zu behalten, oder setzen Sie es nackt, damit die Bash es entsprechend dem konfigurierten Verhalten aufteilt.

Wenn Sie also ein Array definieren möchten, müssen Sie ein Byte-Format definieren, das ein Array enthält, und das ist, was Werkzeuge mögen xargsund findtun: Wenn Sie sie mit dem -0Argument ausführen , arbeiten sie nach einem binären Array-Format, mit dem Elemente abgeschlossen werden das Null-Byte, das dem ansonsten undurchsichtigen Byte-Stream Semantik hinzufügt.

bashKann leider nicht so konfiguriert werden, dass Zeichenfolgen auf dem Null-Byte aufgeteilt werden. Vielen Dank an /unix//a/110108/17980, dass Sie uns das gezeigt haben zsh.

xargs

Sie möchten, dass Ihr Befehl einmal ausgeführt wird, und Sie sagten, dies xargs -0 -n 10000löst Ihr Problem. Wenn dies nicht der Fall ist, wird sichergestellt, dass Ihr Befehl mehr als einmal ausgeführt wird, wenn Sie mehr als 10000 Parameter haben.

Wenn Sie möchten, dass es nur einmal ausgeführt wird oder fehlschlägt, müssen Sie das -xArgument und ein -nArgument angeben, das größer als das -sArgument ist (wirklich: groß genug, dass eine ganze Reihe von Argumenten mit der Länge Null plus dem Namen des Befehls nicht hineinpassen die -sGröße). ( Mann Xargs , siehe Auszug weit unten)

Das System, auf dem ich mich gerade befinde, hat einen Stack, der auf ca. 8 Millionen beschränkt ist. Hier ist mein Limit:

$ printf '%s\0' -- {1..1302582} | xargs -x0n 2076858 -s 2076858 /bin/true
xargs: argument list too long
$ printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true
(no output)

Bash

Wenn Sie keinen externen Befehl einbeziehen möchten, ist die while-read-Schleife, die ein Array speist, wie in /unix//a/110108/17980 gezeigt , die einzige Möglichkeit für die Bash, Dinge zu trennen das Null-Byte.

Die Idee, das Skript als Quelle ( . ... "$@" )zu verwenden, um die Stapelgrößenbeschränkung zu umgehen, ist cool (ich habe es ausprobiert, es funktioniert!), Aber wahrscheinlich nicht wichtig für normale Situationen.

Die Verwendung eines speziellen fd für die Prozessleitung ist wichtig, wenn Sie etwas anderes von stdin lesen möchten, es sonst aber nicht benötigen.

Der einfachste "native" Weg für den täglichen Haushaltsbedarf:

files=()
while IFS= read -rd '' file; do
    files+=("$file")
done <(find ... -print0)

myscriptornonscript "${files[@]}"

Wenn Sie möchten, dass Ihr Prozessbaum sauber und ansprechend aussieht, können Sie mit dieser Methode exec mynonscript "${files[@]}"den Bash-Prozess aus dem Speicher entfernen und ihn durch den aufgerufenen Befehl ersetzen. xargsbleibt immer im Speicher, während der aufgerufene Befehl ausgeführt wird, auch wenn der Befehl nur einmal ausgeführt wird.


Was gegen die native Bash-Methode spricht, ist Folgendes:

$ time { printf '%s\0' -- {1..1302581} | xargs -x0n 2076858 -s 2076858 /bin/true; }

real    0m2.014s
user    0m2.008s
sys     0m0.172s

$ time {
  args=()
  while IFS= read -rd '' arg; do
    args+=( "$arg" )
  done < <(printf '%s\0' -- $(echo {1..1302581}))
  /bin/true "${args[@]}"
}
bash: /bin/true: Argument list too long

real    107m51.876s
user    107m38.532s
sys     0m7.940s

bash ist nicht für das Array-Handling optimiert.


Mann Xargs :

-n max-args

Verwenden Sie höchstens max-args-Argumente pro Befehlszeile. Es werden weniger als max-args-Argumente verwendet, wenn die Größe (siehe Option -s) überschritten wird, sofern die Option -x nicht angegeben ist. In diesem Fall wird xargs beendet.

-s max-Zeichen

Verwenden Sie höchstens Zeichen mit maximaler Zeichenanzahl pro Befehlszeile, einschließlich des Befehls und der Anfangsargumente sowie der abschließenden Nullen an den Enden der Argumentzeichenfolgen. Der größte zulässige Wert ist systemabhängig und wird als Argumentlängenbeschränkung für exec abzüglich der Größe Ihrer Umgebung abzüglich 2048 Byte Headroom berechnet. Wenn dieser Wert größer als 128 KB ist, wird 128 KB als Standardwert verwendet. Andernfalls ist der Standardwert das Maximum. 1 KB sind 1024 Byte.

-x

Beenden Sie, wenn die Größe (siehe Option -s) überschritten wird.

klappern
quelle
Vielen Dank für die Mühe, aber Ihre Grundvoraussetzung ignoriert die Tatsache, dass bash normalerweise ein ausgeklügeltes System zur Angebotsabwicklung verwendet. Aber nicht im Backquote-Ausbau. Vergleichen Sie die folgende (die beide geben Fehler, sondern zeigen den Unterschied): ls "what is this"vs ls `echo '"what is this"'` . Jemand hat es versäumt, die Angebotsverarbeitung für das Ergebnis von Backquotes zu implementieren.
Alexis
Ich bin froh, dass Backquotes keine Angebotsverarbeitung durchführen. Die Tatsache, dass sie sogar Worttrennungen durchführen, hat in der modernen Computergeschichte zu Verwirrung, Kopfkratzern und Sicherheitsmängeln geführt.
Klackern Sie
Die Frage lautet: "Gibt es keine Möglichkeit, Leerzeichen in der Backtick- (oder $(...)) Expansion zu schützen ?". Es erscheint daher angebracht, die Verarbeitung zu ignorieren, die in dieser Situation nicht erfolgt.
Klackern Sie
Das nullterminierte Elementarrayformat ist die einfachste und daher sicherste Möglichkeit, ein Array auszudrücken. Es ist nur eine Schande, dass bashes nicht von Haus aus unterstützt wird, wie es anscheinend der zshFall ist.
Klackern
Tatsächlich habe ich gerade diese Woche eine Zitierungssituation verwendet printf "%s\0"und xargs -0umgangen, in der ein Zwischenwerkzeug Parameter durch einen von einer Shell analysierten String übergeben hat. Das Zitieren kommt immer zurück, um Sie zu beißen.
Klackern Sie