Patrick Rudin <
tax...@gmx.ch>:
>Helmut Waitzmann wrote:
>> Den von dir vermuteten Unterschied zwischen den Programmen «grep»
>> und «pdfgrep» gibt es nicht. Der Unterschied zwischen den beiden
>> Anwendungsfällen oben besteht darin, dass einem Programm (hier:
>> «(pdf‐)grep») Daten in einem Fall (ohne «xargs») als Eingabe
>> geliefert und im anderen Fall (mit «xargs») als Aufrufparameter
>> übergeben werden sollen.
>
>Ah. Die Rohre unterscheiden also verschiedene Kategorien von
>Parametern, während bei der manuellen Eingabe aus Komfortgründen die
>Syntax nicht so eng gesehen wird?
Ich bin mir jetzt nicht sicher, ob ich dich richtig verstehe: Pipes
sind dazu da, zwei Prozesse (also laufende Programme) so miteinander
zu verbinden, dass der erste seine Ausgabe nicht auf den Bildschirm
gibt und der zweite seine Eingabe nicht von der Tastatur erhält,
sondern der erste seine Ausgabe direkt an den zweiten reicht, der
sie als Eingabe verwendet.
Pipes kümmern sich um die Daten, die durch sie laufen, nicht. Es
ist ihnen egal, ob man ein PDF‐Dokument, einen Plain‐Text,
irgendwelche Binär‐Daten, Bilder, … durchschiebt.
Das Shell‐Kommando
erstes_Programm | zweites_Programm
veranlasst den Shell, den Ausgabekanal des ersten Programms mit dem
Eingabekanal des zweiten Programms zu verbinden: Was das erste
Programm schreibt, kann das zweite lesen.
Beispiel:
Das Shell‐Kommando
printf '%s\n' 'Hallo, Welt!' 'Hello, world!'
gibt eine zweizeilige Ausgabe, bestehend aus der ersten Zeile
«Hallo, Welt!» und der zweiten «Hello, world!'», im Terminal aus.
Verbindet man seinen Ausgabekanal über ein Pipe mit dem Eingabekanal
von «grep», geht die Ausgabe nicht mehr ins Terminal sondern ins
Pipe und von dort zum Programm «grep»:
printf '%s\n' 'Hallo, Welt!' 'Hello, world!' | grep -F -- 'Hallo'
«grep» liest also die Zeile «Hallo, Welt!» und schaut, ob in ihr das
Wort «Hallo», das es als Parameter erhalten hat, vorkommt. Kommt es
darin vor, gibt es die Zeile seinerseits aus; kommt es nicht darin
vor, gibt es die Zeile nicht aus sondern wirft sie einfach weg.
Dasselbe macht «grep» auch mit der zweiten Zeile, «Hello, world!».
Die Folge: «grep» gibt nur die Zeile «Hallo, Welt!» aus und
verschluckt die Zeile «Hello, world!».
Wenn man «grep» die Option «-l» verpasst, spuckt es die mit dem
Suchmuster übereinstimmenden Zeilen nicht aus sondern nur die Namen
der Dateien, in denen es die übereinstimmenden Zeilen gefunden hat.
Beispiel: Lasse in einem leeren Verzeichnis das folgende Kommando
laufen:
(
set -- foo bar &&
for muster
do
printf '%s\n' "$1" > "$1".txt
printf '%s\n' "$1" > "no $2".txt
printf '%s\n' "${1} ${2}" > "${1} ${2}".txt
set -- "$@" "$1"
shift
done
)
Das legt dir in dem Verzeichnis die Dateien «foo.txt», «no foo.txt»,
«foo bar.txt», «bar.txt», «no bar.txt» und «bar foo.txt» an.
Lass dir den (einzeiligen) Inhalt einer jeden Datei zeigen:
(
for datei in *.txt
do
printf '\nInhalt der Datei %s:\n' "$datei"
cat -- "$datei"
done
)
Wenn du jetzt das Kommando
grep -l -- foo *.txt
aufrufst, spuckt «grep» wegen der Option «-l» nicht die
Dateiinhalte, die zum Suchmuster «foo» passen, sondern nur den Namen
einer jeden Datei, deren Inhalt zum Suchmuster «foo» passt, aus.
Und zwar spuckt «grep» jeden Dateinamen in einer eigenen Zeile aus,
genauer: «grep» spuckt die Zeichen des Dateinamens und anschließend
ein Newline‐Zeichen aus:
bar foo.txt
foo bar.txt
foo.txt
no bar.txt
Jetzt schau dir «xargs» an: «xargs» liest einen Datenstrom aus
seinem Eingabekanal und zerlegt ihn nach gewissen Regeln in einzelne
Wörter. Die Wörter sammelt er auf und, wenn er genug gesammelt hat,
startet er das ihm angegebene Programm und gibt ihm die Wörter als
Aufrufparameter mit.
Probiere das in Kombination mit «grep» wie oben aus:
grep -l -- foo *.txt | xargs -- printf 'Dateiname: %s\n'
Du erhältst das folgende Ergebnis:
Dateiname: bar
Dateiname: foo.txt
Dateiname: foo
Dateiname: bar.txt
Dateiname: foo.txt
Dateiname: no
Dateiname: bar.txt
Die Dateinamen, die «grep» (wie oben) zeilenweise geliefert hat,
werden von «xargs» dort, wo Leerzeichen in ihnen enthalten sind,
zerbrochen. Das kommt von den «gewissen Regeln», nach den «xargs»
seinen Eingabedatenstrom zersägt.
So etwas ist natürlich bei Dateinamen inakzeptabel, wenn man die
anschließend noch weiterverwenden will.
Um das Problem zu beheben, kommen die GNU‐«grep»‐Option «-Z» und die
GNU‐«xargs»‐Option «-0» ins Spiel:
Die «grep»‐Option «-Z» befiehlt «grep», die Dateinamen bei der
Ausgabe nicht mit Newline‐ sondern mit NUL‐Zeichen zu trennen. Das
NUL‐Zeichen ist das einzige Zeichen, das bereits vom Betriebssystem
her in Dateinamen nicht vorkommen kann. Es ist das einzige Zeichen,
das sich für die Trennung von Dateinamen in einem Datenstrom eignet.
Die «xargs»‐Option «-0» befiehlt «xargs», seinen Eingabedatenstrom
nicht mehr nach den «gewissen Regeln» in Einzelteile zu zersägen,
sondern die Säge nur noch an den Stellen im Eingabedatenstrom, an
denen ein NUL‐Zeichen sitzt, anzusetzen.
Die «grep»‐Option «-Z» und die «xargs»‐Option «-0» sind (wie)
geschaffen für einander. Probiere das folgende Kommando aus:
grep -lZ -- foo *.txt | xargs -0 -- printf 'Dateiname: %s\n'
Man erhält die folgende Ausgabe: Die Dateinamen sind nicht
zerbrochen: Jeder steht unverletzt in einer eigenen Zeile da.
Dateiname: bar foo.txt
Dateiname: foo bar.txt
Dateiname: foo.txt
Dateiname: no bar.txt
Langer Rede kurzer Sinn: Wenn man die «grep»‐Option «-Z» und die
«xargs»‐Option «-0» zur Verfügung hat, verwendet man sie, wenn man
von «grep» ausgespuckte Dateinamen an «xargs» verfüttern will.
Um es vollends sicher zu machen, gibt man «xargs» noch die Option
«-r». Sie bewirkt, dass «xargs» darauf verzichtet, das ihm
angegebene Programm zu starten, wenn im Eingabedatenstrom überhaupt
keine Daten ankommen. Das ist ja beispielsweise beim Kommando
grep -lZ -- foobar *.txt | xargs -0 -- printf 'Dateiname: %s\n'
der Fall. Ohne «-r» startet «xargs» das Programm «printf» trotzdem
ein einziges Mal ohne einen Dateinamenparameter, obwohl es richtig
wäre, es gar nicht zu starten. Das Ergebnis ist, dass «printf» eine
Zeile ohne Dateinamen ausgibt, obwohl gar keine Ausgabe sinnvoller
wäre:
Dateiname:
Deshalb empfiehlt es sich,
grep -lZ -- foobar *.txt | xargs -0r -- printf 'Dateiname: %s\n'
zu verwenden. Dann wird überhaupt nichts ausgegeben.
>Ich habe mich die letzten Monate intensiv mit R auseinandergesetzt,
>dort kann man (mit tidyverse) simpelste Ketten bilden. Aber dabei
>werden faktisch immer Vektoren übergeben, formal also immer
>dasselbe. Möglicherweise resultiert meine Verwirrung daher.
Mit R kenne ich mich nicht aus, kann dir daher nicht sagen, ob und
wo es da Vergleichbares gibt.
>
>> Wenn du ein «xargs» aus einem Pipe fütterst und damit
>> «(pdf‐)grep» startest, liest «xargs» Daten aus dem Pipe (in
>> deinem Anwendungsfall: Dateinamen) und startet damit «(pdf‐)grep»
>> mit den Dateinamen als Parametern. Das tut «xargs» allerdings
>> nur dann zuverlässig, wenn du, wie Tim und Christian dir
>> nahegelegt haben, bei «(pdf‐)grep» die Optionen «-l» und «-Z» und
>> bei «xargs» die Optionen «-0» und «-r» verwendest.
>
>Die Zusatzoptionen habe ich auf Anhieb nicht ohne Syntaxfehler zum
>Laufen gebracht, aber das muss ich gelegentlich in Ruhe anschauen.
Schau das Beispiel oben an. Da gibt es eigentlich nicht viel falsch
zu machen: Im «grep»‐Handbuch wird dieser Anwendungsfall sogar
ausdrücklich erwähnt:
`-Z'
`--null'
Output a zero byte (the ASCII `NUL' character) instead of
the character that normally follows a file name. For
example, `grep -lZ' outputs a zero byte after each file name
instead of the usual newline. This option makes the output
unambiguous, even in the presence of file names containing
unusual characters like newlines. This option can be used
with commands like `find -print0', `perl -0', `sort -z', and
`xargs -0' to process arbitrary file names, even those that
contain newline characters.
Nochmal auf Deutsch zusammengefasst: Wenn «grep» Dateinamen der
Dateien, die es durchsucht hat, zur Weiterverarbeitung ausspucken
soll (die Option «-l»), gib ihm die Option «-Z» dazu. Wenn «find»
Dateinamen zur Weiterverarbeitung ausspucken soll, verwende bei
«find» das Prädikat «-print0». Wenn «perl» Dateinamen aus seinem
Eingabekanal lesen und verarbeiten soll, gib ihm die Option «-0».
Wenn «sort» Dateinamen aus seinem Eingabekanal lesen und sortieren
soll, gib ihm die Option «-z». Wenn «xargs» Dateinamen aus seinem
Eingabekanal lesen und verwenden soll, gib ihm die Option «-0».
>> Lies ein gutes Buch über die Funktionsweise bzw. die
>> Systemaufruf‐Schnittstelle (hier speziell die Systemaufrufe
>> «pipe», «fork» und «execve») von Unix oder Linux.
>
>Ich bin für konkrete Literaturtipps dankbar. Die Fülle an
>Unix-Büchern jeglichen Jahrgangs in den hiesigen Bibliotheken ist
>unüberschaubar.
Ich habe mal das Buch von Maurice J. Bach: «The Design of the UNIX
Operating System» (von 1986) gelesen. Es soll auch eine Fassung auf
Deutsch (ich meine, mit dem Titel «UNIX – Wie funktioniert das
Betriebssystem?») geben.
Natürlich ist es nicht mehr aktuell, d. h., zu Unix oder Linux ist
inzwischen viel Neues hinzugekommen. Und sicher gibt es inzwischen
neuere Bücher. Aber seit dem weiß ich, warum ich vor hard links,
file descriptor redirections, Dateinamen, Aufrufparametern,
Shell‐Kommandozeilen, login shells … keine Angst haben muss.