Nutzloser Gebrauch von Katze?


101

Dies ist wahrscheinlich in vielen FAQs - anstatt zu verwenden:

cat file | command

(was als nutzloser Gebrauch von Katze bezeichnet wird), korrekter Weg soll sein:

command < file

Auf die zweite, "richtige" Weise - das Betriebssystem muss keinen zusätzlichen Prozess erzeugen.
Obwohl ich das wusste, benutzte ich aus zwei Gründen weiterhin nutzlose Katzen.

  1. ästhetischer - Ich mag es, wenn sich Daten nur von links nach rechts gleichmäßig bewegen. Und es leichter zu ersetzen catmit etwas anderem ( gzcat, echo, ...), fügen Sie eine zweite Datei oder fügen Sie neue Filter ( pv, mbuffer, grep...).

  2. Ich "fühlte", dass es in einigen Fällen schneller sein könnte. Schneller, weil es 2 Prozesse gibt, 1. ( cat) liest und der zweite macht was auch immer. Und sie können parallel ausgeführt werden, was manchmal eine schnellere Ausführung bedeutet.

Ist meine Logik korrekt (aus dem 2. Grund)?


22
catist eine Identitätspfeife . Es überträgt nur seine Eingabe auf seine Ausgabe. Wenn das zweite Programm in der Kette seine Eingabe von demselben Argument übernehmen kann, an das Sie übergeben cat(oder von der Standardeingabe, wenn Sie kein Argument übergeben), catist dies absolut nutzlos und führt nur dazu, dass ein zusätzlicher Prozess gegabelt wird und eine zusätzliche Pipe vorhanden ist erstellt.
Frédéric Hamidi

11
@ FrédéricHamidi Wenn cat keine Argumente hat oder sein Argument ist -, ist es eine Identitätspipe. Wenn es mehr als ein Nicht-Bindestrich-Dateinamenargument hat, wird es jedoch mehr als eine Identitätspipe und beginnt, einen echten Zweck zu erfüllen.
Kojiro

3
Der ehemals beliebte Link zu partmaps.org ist leider tot. Der Inhalt ist jetzt bei pigmailmail.org/era/unix/award.html
Tripleee


2
Ich beobachte, dass Sie, wenn Sie den Datenfluss nach rechts anzeigen möchten (Grund 1), dies tun können, indem Sie die Dateiumleitung vor den Befehl stellen, wie in <file command1 | command2, obwohl es Unstimmigkeiten über die Ästhetik geben würde.
Holdenweb

Antworten:


81

Ich war mir der Auszeichnung erst heute bewusst, als ein Neuling versuchte, mir die UUOC für eine meiner Antworten zuzuweisen. Es war ein cat file.txt | grep foo | cut ... | cut .... Ich gab ihm einen Teil meiner Gedanken und erst nachdem ich den Link besucht hatte, gab er mir einen Hinweis auf die Ursprünge der Auszeichnung und die Praxis, dies zu tun. Weitere Suche führte mich zu dieser Frage. Trotz bewusster Überlegung enthielt leider keine der Antworten meine Begründung.

Ich hatte nicht vorgehabt, defensiv auf ihn zu reagieren. Schließlich hätte ich in meinen jüngeren Jahren den Befehl so geschrieben, als grep foo file.txt | cut ... | cut ...ob Sie, wenn Sie die häufigen Single- greps machen, die Platzierung des Dateiarguments lernen und es ist bekannt, dass das erste das Muster und die späteren Dateinamen sind.

Es war eine bewusste Entscheidung, catals ich die Frage beantwortete, teilweise aus einem Grund des "guten Geschmacks" (in den Worten von Linus Torvalds), aber hauptsächlich aus einem zwingenden Grund der Funktion.

Der letztere Grund ist wichtiger, deshalb werde ich ihn zuerst herausstellen. Wenn ich eine Pipeline als Lösung anbiete, erwarte ich, dass sie wiederverwendbar ist. Es ist sehr wahrscheinlich, dass eine Pipeline am Ende einer anderen Pipeline hinzugefügt oder in eine andere Pipeline gespleißt wird. In diesem Fall beeinträchtigt ein Dateiargument für grep die Wiederverwendbarkeit und dies möglicherweise stillschweigend ohne Fehlermeldung, wenn das Dateiargument vorhanden ist. I. e. grep foo xyz | grep bar xyz | wcgibt an, wie viele Zeilen xyzenthalten sind, barwährend Sie die Anzahl der Zeilen erwarten, die sowohl fooals als auch enthalten bar. Das Ändern von Argumenten in einen Befehl in einer Pipeline, bevor dieser verwendet wird, ist fehleranfällig. Fügen Sie die Möglichkeit stiller Ausfälle hinzu und es wird eine besonders heimtückische Praxis.

Der erstere Grund ist auch nicht unwichtig, da viel " guter Geschmack " lediglich eine intuitive unbewusste Begründung für Dinge wie die oben genannten stillen Fehler ist, an die Sie nicht denken können, wenn eine bildungsbedürftige Person sagt ", aber nicht diese Katze nutzlos ".

Ich werde jedoch versuchen, auch den früheren Grund für den "guten Geschmack", den ich erwähnt habe, bewusst zu machen. Dieser Grund hat mit dem orthogonalen Designgeist von Unix zu tun. grepnicht cutund lsnicht grep. Das widerspricht zumindest grep foo file1 file2 file3dem Designgeist. Die orthogonale Vorgehensweise ist cat file1 file2 file3 | grep foo. Jetzt grep foo file1ist nur ein Sonderfall von grep foo file1 file2 file3, und wenn Sie es nicht gleich behandeln, verbrauchen Sie zumindest Gehirnuhrzyklen, um den nutzlosen Katzenpreis zu vermeiden.

Das führt uns zu dem Argument, grep foo file1 file2 file3das verkettet und catverkettet, so dass es angemessen ist, cat file1 file2 file3aber weil cates nicht verkettet, cat file1 | grep fooverletzen wir den Geist sowohl des catals auch des allmächtigen Unix. Wenn dies der Fall wäre, würde Unix einen anderen Befehl benötigen, um die Ausgabe einer Datei zu lesen und in stdout zu spucken (nicht paginieren oder irgendetwas anderes, nur in stdout spucken). Sie hätten also die Situation, in der Sie sagen cat file1 file2oder sagen, dog file1und denken Sie gewissenhaft daran, zu vermeiden cat file1, dass Sie die Auszeichnung nicht erhalten, und vermeiden Sie dies, dog file1 file2da das Design von hoffentlich dogeinen Fehler auslösen würde, wenn mehrere Dateien angegeben werden.

Hoffentlich sympathisieren Sie an dieser Stelle mit den Unix-Designern dafür, dass sie keinen separaten Befehl zum Spucken einer Datei an stdout enthalten und gleichzeitig catnach Verkettung benennen, anstatt ihr einen anderen Namen zu geben. <edit>Das Entfernen falscher Kommentare zu <ist in der Tat <eine effiziente No-Copy-Funktion, mit der eine Datei an stdout gespuckt werden kann, die Sie am Anfang einer Pipeline positionieren können, sodass die Unix-Designer etwas speziell dafür aufgenommen haben</edit>

Die nächste Frage ist, warum es wichtig ist, Befehle zu haben, die lediglich eine Datei spucken oder mehrere Dateien ohne weitere Verarbeitung zu stdout verketten. Ein Grund besteht darin, zu vermeiden, dass jeder einzelne Unix-Befehl, der mit Standardeingaben arbeitet, weiß, wie mindestens ein Befehlszeilendateiargument analysiert und als Eingabe verwendet wird, falls vorhanden. Der zweite Grund besteht darin, zu vermeiden, dass Benutzer sich erinnern müssen: (a) wohin die Dateinamenargumente gehen; und (b) den oben erwähnten stillen Pipeline-Fehler vermeiden.

Das bringt uns zu dem Grund, warum grepes die zusätzliche Logik gibt. Das Grundprinzip besteht darin, die Benutzerfreundlichkeit für Befehle zu ermöglichen, die häufig und eigenständig verwendet werden (und nicht als Pipeline). Es ist ein leichter Kompromiss der Orthogonalität für einen signifikanten Gewinn an Benutzerfreundlichkeit. Nicht alle Befehle sollten auf diese Weise entworfen werden, und Befehle, die nicht häufig verwendet werden, sollten die zusätzliche Logik von Dateiargumenten vollständig vermeiden (denken Sie daran, dass zusätzliche Logik zu unnötiger Fragilität führt (die Möglichkeit eines Fehlers)). Die Ausnahme besteht darin, Dateiargumente wie im Fall von zuzulassen grep. (Übrigens, beachten Sie, dass dies lseinen ganz anderen Grund hat , Dateiargumente nicht nur zu akzeptieren, sondern auch zu erfordern.)

Schließlich hätte es besser gemacht werden können, wenn solche außergewöhnlichen Befehle wie grep(aber nicht unbedingt ls) einen Fehler erzeugen, wenn die Standardeingabe auch verfügbar ist, wenn Dateiargumente angegeben werden.


52
Beachten Sie, dass beim grepAufrufen mit mehreren Dateinamen den gefundenen Zeilen der Name der Datei vorangestellt wird, in der sie gefunden wurden (es sei denn, Sie deaktivieren dieses Verhalten). Es kann auch die Zeilennummern in den einzelnen Dateien melden. Wenn Sie nur catzum Füttern verwenden grep, verlieren Sie die Dateinamen und die Zeilennummern sind über alle Dateien hinweg fortlaufend, nicht pro Datei. Daher gibt es Gründe, grepmehrere Dateien selbst zu verarbeiten, catdie nicht verarbeitet werden können. Die Einzeldatei- und Nulldateifälle sind lediglich Sonderfälle der allgemeinen Mehrdateienutzung von grep.
Jonathan Leffler

38
Wie in der Antwort von Kojiro erwähnt , ist es durchaus möglich und legal, mit der Pipeline zu beginnen < file command1 .... Obwohl die herkömmliche Position für die E / A-Umleitungsoperatoren nach dem Befehlsnamen und seinen Argumenten liegt, ist dies nur die Konvention und keine obligatorische Platzierung. Das <muss vor dem Dateinamen stehen. Es gibt also eine nahezu perfekte Symmetrie zwischen >outputund <inputUmleitungen : <input command1 -opt 1 | command2 -o | command3 >output.
Jonathan Leffler

15
Ich denke, ein Grund, warum Leute (einschließlich mir) den UUoC-Stein werfen, ist in erster Linie die Bildung. Manchmal verarbeiten die Leute riesige Textdateien mit Gigabyte. In diesem Fall ist die Minimierung von Pipes (UUoC, Zusammenlegen von sequentiellen Greps zu einer, ua) von entscheidender Bedeutung, und oft kann davon ausgegangen werden, dass das OP wirklich nicht weiß, dass kleine Optimierungen vorliegen könnten enorme Auswirkungen auf die Leistung. Ich stimme Ihrem Standpunkt zu Gehirnzyklen voll und ganz zu, und deshalb benutze ich regelmäßig Katzen, auch wenn sie nicht benötigt werden. Aber es ist wichtig zu wissen, dass es nicht benötigt wird.
Adrian Frühwirth

13
Bitte verstehe; Ich sage in keiner Weise, dass catdas nutzlos ist. Es ist nicht so, dass cates nutzlos ist; Es ist so, dass ein bestimmtes Konstrukt nicht die Verwendung von benötigt cat. Wenn Sie möchten , beachten Sie, dass es sich um UUoC (nutzlose Verwendung von cat) und nicht um UoUC (nutzlose Verwendung von cat) handelt. Es gibt viele Fälle, in denen catdas richtige Werkzeug verwendet wird. Ich habe kein Problem damit, dass es verwendet wird, wenn es das richtige Werkzeug ist (und erwähne in meiner Antwort tatsächlich einen Fall).
Jonathan Leffler

6
@randomstring Ich höre dich, aber ich denke, es hängt wirklich vom Anwendungsfall ab. Bei Verwendung in der Befehlszeile ist eine zusätzliche catin der Pipe abhängig von den Daten möglicherweise keine große Sache. Bei Verwendung als Programmierumgebung kann es jedoch unbedingt erforderlich sein, diese leistungskritischen Dinge zu implementieren. vor allem, wenn es darum geht, was in Bezug auf bashdie Leistung wie ein rechteckig geformtes Rad ist (im Vergleich zu kshsowieso. Ich spreche hier bis zu 10x langsamer - kein Scherz). Sie haben wollen , um Ihre Gabeln (und nicht nur das) zu optimieren , wenn mit größeren Skripten oder großen Schleifen zu tun.
Adrian Frühwirth

58

Nee!

Zunächst spielt es keine Rolle, wo in einem Befehl die Umleitung erfolgt. Wenn Sie also Ihre Umleitung links von Ihrem Befehl mögen, ist das in Ordnung:

< somefile command

ist das gleiche wie

command < somefile

Zweitens gibt es n + 1 Prozesse und eine Unterschale, wenn Sie eine Pipe verwenden. Es ist entschieden langsamer. In einigen Fällen wäre n Null gewesen (z. B. wenn Sie zu einer integrierten Shell umleiten). Wenn Sie also verwenden cat, fügen Sie einen neuen Prozess völlig unnötig hinzu.

Als Verallgemeinerung lohnt es sich, wenn Sie ein Rohr verwenden, 30 Sekunden zu nehmen, um zu sehen, ob Sie es beseitigen können. (Aber es lohnt sich wahrscheinlich nicht, länger als 30 Sekunden zu dauern.) Hier einige Beispiele, bei denen Rohre und Prozesse häufig unnötig verwendet werden:

for word in $(cat somefile);  # for word in $(<somefile); … (or better yet, while read < somefile)

grep something | awk stuff; # awk '/something/ stuff' (similar for sed)

echo something | command; # command <<< something (although echo would be necessary for pure POSIX)

Fühlen Sie sich frei zu bearbeiten, um weitere Beispiele hinzuzufügen.


2
Nun, die Geschwindigkeitssteigerung wird nicht viel sein.
Dakkaron

9
Wenn Sie das "<somefile" vor "command" platzieren, erhalten Sie technisch gesehen von links nach rechts, aber es führt zu mehrdeutigem Lesen, da es keine syntaktische Abgrenzung gibt: < cat grep dogDies ist ein erfundenes Beispiel, das zeigt, dass Sie nicht einfach zwischen der Eingabedatei und dem Befehl unterscheiden können das empfängt die Eingabe und die Argumente für den Befehl.
Nekromant

2
Als Faustregel ich angenommen habe für die Entscheidung , wo die STDIN Umleitung geht , ist zu tun , was das minimiert Aussehen der Mehrdeutigkeit / Potenzial für Überraschung. Wenn man dogmatisch sagt, dass es vorher geht, wird das Problem des Nekromanten angesprochen, aber wenn man dogmatisch sagt, dass es nachher geht, kann man dasselbe tun. Bedenken Sie : stdout=$(foo bar -exec baz <qux | ENV=VAR quux). F : Hat <quxgelten foo, oder baz, das ist -exec‚d durch foo? A. Es gilt für foo, kann aber mehrdeutig erscheinen. Putting , <qux bevor foo in diesem Fall ist klarer, wenn auch weniger häufig, und ist an den hinteren analog ENV=VAR quux.
Mark G.

3
@necromancer, <"cat" grep dogist dort leichter zu lesen. (Ich bin normalerweise Pro-Whitespace, aber dieser spezielle Fall ist eine Ausnahme).
Charles Duffy

1
@kojiro "Es ist entschieden langsamer." Sie können das nicht schreiben, ohne das mit Zahlen zu sichern. Meine Zahlen sind hier: oletange.blogspot.com/2013/10/useless-use-of-cat.html (und sie zeigen, dass es nur langsamer ist, wenn Sie einen hohen Durchsatz haben) Wo sind Ihre?
Ole Tange

30

Ich bin mit den meisten Fällen des übermäßig selbstgefälligen UUOC-Preises nicht einverstanden, da er beim Unterrichten eines anderen catein geeigneter Platzhalter für jeden Befehl oder jede knusprige, komplizierte Pipeline von Befehlen ist, die eine Ausgabe liefern, die für das besprochene Problem oder die diskutierte Aufgabe geeignet ist.

Dies gilt insbesondere für Sites wie Stack Overflow, ServerFault, Unix & Linux oder eine der SE-Sites.

Wenn jemand speziell nach der Optimierung fragt oder wenn Sie zusätzliche Informationen hinzufügen möchten, sprechen Sie darüber, wie ineffizient die Verwendung von Katzen ist. Aber beschimpfen Sie die Leute nicht, weil sie sich dafür entschieden haben, in ihren Beispielen Einfachheit und Verständlichkeit anzustreben, anstatt mich anzuschauen, wie cool ich bin! Komplexität.

Kurz gesagt, weil Katze nicht immer Katze ist.

Auch, weil die meisten Leute, die gerne UUOCs vergeben, dies tun, weil sie mehr daran interessiert sind, zu zeigen, wie „klug“ sie sind, als Menschen zu helfen oder sie zu unterrichten. In Wirklichkeit zeigen sie, dass sie wahrscheinlich nur ein weiterer Neuling sind, der einen winzigen Stock gefunden hat, mit dem er seine Kollegen schlagen kann.


Aktualisieren

Hier ist eine weitere UUOC, die ich in einer Antwort unter https://unix.stackexchange.com/a/301194/7696 gepostet habe :

sqlq() {
  local filter
  filter='cat'

  # very primitive, use getopts for real option handling.
  if [ "$1" == "--delete-blank-lines" ] ; then
    filter='grep -v "^$"'
    shift
  fi

  # each arg is piped into sqlplus as a separate command
  printf "%s\n" "$@" | sqlplus -S sss/eee@sid | $filter
}

UUOC-Pedanten würden sagen, dass dies ein UUOC ist, da es leicht möglich ist, $filterdie leere Zeichenfolge als Standard festzulegen und die ifAnweisung zu tun, filter='| grep -v "^$"'aber IMO, indem das Pipe-Zeichen nicht eingebettet wird $filter, dient dieses "nutzlose" catdem äußerst nützlichen Zweck, die Tatsache selbst zu dokumentieren Das $filterin der printfZeile ist nicht nur ein weiteres Argument sqlplus, sondern ein optionaler vom Benutzer auswählbarer Ausgabefilter.

Wenn es erforderlich ist , mehrere optionalen Ausgabefilter haben, könnte die Option Verarbeitung nur append | whateverzu $filterso oft wie nötig - ein extra catin der Pipeline wird nicht weh tut nichts oder verursacht eine merklichen Leistungseinbußen.


11
Nebenbei bemerkt - ==inside [ ]wird von POSIX nicht angegeben, und nicht alle Implementierungen akzeptieren dies. Der standardisierte Operator ist gerecht =.
Charles Duffy

26

Bei der UUoC-Version catmuss die Datei in den Speicher eingelesen und dann in die Pipe geschrieben werden, und der Befehl muss die Daten aus der Pipe lesen, sodass der Kernel die gesamte Datei drei kopieren muss Mal während im umgeleiteten Fall Der Kernel muss die Datei nur einmal kopieren. Es ist schneller, etwas einmal als dreimal zu tun.

Verwenden von:

cat "$@" | command

ist eine ganz andere und nicht unbedingt nutzlose Verwendung von cat. Es ist immer noch nutzlos, wenn der Befehl ein Standardfilter ist, der null oder mehr Dateinamenargumente akzeptiert und diese nacheinander verarbeitet. Betrachten Sie den trBefehl: Es ist ein reiner Filter, der Dateinamenargumente ignoriert oder ablehnt. Um mehrere Dateien zu füttern, müssen Sie catwie gezeigt verwenden. (Natürlich gibt es eine separate Diskussion, deren Design trnicht sehr gut ist. Es gibt keinen wirklichen Grund, warum es nicht als Standardfilter entworfen werden könnte.) Dies kann auch gültig sein, wenn der Befehl alle Eingaben als behandeln soll einzelne Datei statt als mehrere separate Dateien, selbst wenn der Befehl mehrere separate Dateien akzeptieren würde: Dies wcist beispielsweise ein solcher Befehl.

Es ist der cat single-fileFall, der bedingungslos nutzlos ist.


26

Zur Verteidigung der Katze:

Ja,

   < input process > output 

oder

   process < input > output 

ist effizienter, aber viele Aufrufe haben keine Leistungsprobleme, sodass es Ihnen egal ist.

ergonomische Gründe:

Wir sind es gewohnt, von links nach rechts zu lesen, also ein Befehl wie

    cat infile | process1 | process2 > outfile

ist trivial zu verstehen.

    process1 < infile | process2 > outfile

muss über process1 springen und dann von links nach rechts lesen. Dies kann geheilt werden durch:

    < infile process1 | process2 > outfile

sieht irgendwie so aus, als ob ein Pfeil nach links zeigt, wo nichts ist. Verwirrender und sieht aus wie ausgefallenes Zitieren:

    process1 > outfile < infile

und das Generieren von Skripten ist oft ein iterativer Prozess.

    cat file 
    cat file | process1
    cat file | process1 | process2 
    cat file | process1 | process2 > outfile

wo Sie Ihren Fortschritt schrittweise sehen, während

    < file 

funktioniert nicht einmal. Einfache Methoden sind weniger fehleranfällig und die ergonomische Befehlsverkettung ist bei cat einfach.

Ein weiteres Thema ist, dass die meisten Menschen lange vor der Verwendung eines Computers und bei der Verwendung eines Computers als Programmierer> und <als Vergleichsoperatoren ausgesetzt waren und diesen als solche weitaus häufiger ausgesetzt waren.

Der Vergleich zweier Operanden mit <und> ist kontrakommutativ, was bedeutet

(a > b) == (b < a)

Ich erinnere mich an das erste Mal, als ich <für die Umleitung von Eingaben verwendete, befürchtete ich

a.sh < file 

könnte das gleiche bedeuten wie

file > a.sh

und überschreibe irgendwie mein a.sh Skript. Vielleicht ist dies ein Problem für viele Anfänger.

seltene Unterschiede

wc -c journal.txt
15666 journal.txt
cat journal.txt | wc -c 
15666

Letzteres kann direkt in Berechnungen verwendet werden.

factor $(cat journal.txt | wc -c)

Natürlich kann hier auch das <anstelle eines Dateiparameters verwendet werden:

< journal.txt wc -c 
15666
wc -c < journal.txt
15666
    

aber wen interessiert das - 15k?

Wenn ich gelegentlich auf Probleme stoßen würde, würde ich sicherlich meine Gewohnheit ändern, Katzen anzurufen.

Wenn Sie sehr große oder viele, viele Dateien verwenden, ist es in Ordnung, cat zu vermeiden. Bei den meisten Fragen ist die Verwendung von Katze orthogonal, kein Thema, kein Problem.

Es ist nur ärgerlich und langweilig, diese nutzlose nutzlose Verwendung der Katzendiskussion zu jedem zweiten Shell-Thema zu beginnen. Holen Sie sich ein Leben und warten Sie auf Ihre Minute des Ruhms, wenn Sie sich mit Leistungsfragen befassen.


5
+11111 .. Als Autor der aktuell akzeptierten Antwort empfehle ich diese wunderbare Ergänzung. Die konkreten Beispiele erläutern meine oft abstrakten und wortreichen Argumente, und das Lachen, das Sie durch die frühe Besorgnis des Autors erhalten, file > a.shist allein die Zeit wert, dies zu lesen :) Vielen Dank für das Teilen!
Nekromant

In diesem Aufruf cat file | wc -c, wcbis EOF lesen muss stdin, Zählen Bytes. Aber in diesem wc -c < fileFall wird nur stdin angezeigt, es wird herausgefunden, dass es sich um eine reguläre Datei handelt, und es wird st_size gedruckt, anstatt Eingaben zu lesen. Bei einer großen Datei wäre der Leistungsunterschied deutlich sichtbar.
Oguz Ismail

18

Ein weiteres Problem besteht darin, dass das Rohr eine Unterschale stillschweigend maskieren kann. Für dieses Beispiel werde ich ersetzen catmit echo, aber das gleiche Problem existiert.

echo "foo" | while read line; do
    x=$line
done

echo "$x"

Sie könnten erwarten, xzu enthalten foo, aber es tut nicht. Das von xIhnen festgelegte wurde in einer Subshell erzeugt, um die whileSchleife auszuführen . xIn der Shell, die die Pipeline gestartet hat, hat die Pipeline einen nicht verwandten Wert oder ist überhaupt nicht festgelegt.

In bash4 können Sie einige Shell-Optionen so konfigurieren, dass der letzte Befehl einer Pipeline in derselben Shell ausgeführt wird wie derjenige, der die Pipeline startet. Dann können Sie dies jedoch versuchen

echo "foo" | while read line; do
    x=$line
done | awk '...'

und xist wieder lokal für die whileUnterschale des.


5
In rein POSIX-Shells kann dies ein heikles Problem sein, da Sie hier keine Zeichenfolgen oder Prozessersetzungen haben, um die Pipe zu vermeiden. BashFAQ 24 bietet auch in diesem Fall einige nützliche Lösungen.
Kojiro

4
In einigen Schalen erstellt die abgebildete Pipe keine Unterschale. Beispiele sind Korn und Z. Sie unterstützen auch die Prozessersetzung und hier Zeichenfolgen. Natürlich sind sie nicht ausschließlich POSIX. Bash 4 muss shopt -s lastpipedas Erstellen der Subshell vermeiden.
Bis auf weiteres angehalten.

13

Als jemand, der regelmäßig auf dieses und eine Reihe anderer Antipattern für die Shell-Programmierung hinweist, fühle ich mich verpflichtet, verspätet abzuwägen.

Shell-Skript ist eine Kopier- / Einfügesprache. Für die meisten Leute, die Shell-Skripte schreiben, sind sie nicht darin, die Sprache zu lernen; Es ist nur ein Hindernis, das sie überwinden müssen, um weiterhin Dinge in der Sprache (n) zu tun, mit der sie tatsächlich etwas vertraut sind.

In diesem Zusammenhang sehe ich es als störend und möglicherweise sogar destruktiv an, verschiedene Shell-Scripting-Anti-Patterns zu verbreiten. Der Code, den jemand bei Stack Overflow findet, sollte idealerweise mit minimalen Änderungen und unvollständigem Verständnis in seine Umgebung kopiert / eingefügt werden können.

Unter den vielen Shell-Skriptressourcen im Internet ist der Stapelüberlauf insofern ungewöhnlich, als Benutzer die Qualität der Website durch Bearbeiten der Fragen und Antworten auf der Website verbessern können. Allerdings Code Änderungen können problematisch sein , , weil es einfach Änderungen vornehmen, die nicht durch den Code Autor bestimmt waren. Daher neigen wir dazu, Kommentare zu hinterlassen, um Änderungen am Code vorzuschlagen.

Die UUCA und verwandte Antimuster-Kommentare sind nicht nur für die Autoren des Codes bestimmt, den wir kommentieren. sie sind genauso ein Vorbehalt , um den Lesern zu helfen der Website zu , auf Probleme im Code aufmerksam zu werden, den sie hier finden.

Wir können nicht hoffen, eine Situation zu erreichen, in der keine Antworten auf Stack Overflow nutzlose cats (oder nicht zitierte Variablen oder) empfehlenchmod 777 oder eine Vielzahl anderer Antipattern-Plagen) , aber wir können zumindest dazu beitragen, den Benutzer aufzuklären, der kopieren soll / Fügen Sie diesen Code in die innerste enge Schleife ihres Skripts ein, die millionenfach ausgeführt wird.

Aus technischen Gründen ist die traditionelle Weisheit, dass wir versuchen sollten, die Anzahl externer Prozesse zu minimieren. Dies gilt weiterhin als gute allgemeine Anleitung beim Schreiben von Shell-Skripten.


1
Auch bei großen Dateien bedeutet das Durchleiten cateine Menge zusätzlicher Kontextwechsel und Speicherbandbreite (und die Verschmutzung des L3-Cache durch zusätzliche Kopien von Daten im catLesepuffer und den Pipe-Puffern). Insbesondere auf einem großen Multi-Core-Computer (wie bei vielen Hosting-Setups) ist die Cache- / Speicherbandbreite eine gemeinsam genutzte Ressource.
Peter Cordes

1
@PeterCordes Bitte posten Sie Ihre Messungen. Wir können also, ob es in der Praxis wirklich wichtig ist. Meine Erfahrung ist, dass es normalerweise keine Rolle spielt: oletange.blogspot.com/2013/10/useless-use-of-cat.html
Ole Tange

1
Ihr eigenes Blog zeigt eine 50% ige Verlangsamung für einen hohen Durchsatz, und Sie sehen nicht einmal die Auswirkungen auf den Gesamtdurchsatz (wenn Sie Dinge hatten, die die anderen Kerne beschäftigen). Wenn ich dazu komme, führe ich möglicherweise Ihre Tests durch, während x264 oder x265 ein Video mit allen Kernen codieren, und sehe, wie sehr dies die Videocodierung verlangsamt. bzip2und gzipKomprimierung sind beide sehr langsam im Vergleich zu dem Overhead cat, der allein dazu beiträgt (wenn die Maschine ansonsten im Leerlauf ist). Es ist schwer, Ihre Tabellen zu lesen (Zeilenumbruch in der Mitte einer Zahl?). sysDie Zeit nimmt stark zu, aber immer noch klein im Vergleich zum Benutzer oder echt?
Peter Cordes

8

Ich benutze oft cat file | myprogramin Beispielen. Manchmal werde ich des nutzlosen Gebrauchs von Katzen beschuldigt ( http://porkmail.org/era/unix/award.html ). Ich bin aus folgenden Gründen nicht einverstanden:

  • Es ist leicht zu verstehen, was los ist.

    Beim Lesen eines UNIX-Befehls erwarten Sie einen Befehl, gefolgt von Argumenten, gefolgt von einer Umleitung. Es ist möglich, die Umleitung an einer beliebigen Stelle zu platzieren, sie wird jedoch selten angezeigt. Daher fällt es den Benutzern schwerer, das Beispiel zu lesen. Ich glaube

    cat foo | program1 -o option -b option | program2

    ist leichter zu lesen als

    program1 -o option -b option < foo | program2

    Wenn Sie die Umleitung an den Anfang verschieben, verwirren Sie Personen, die an diese Syntax nicht gewöhnt sind:

    < foo program1 -o option -b option | program2

    und Beispiele sollten leicht zu verstehen sein.

  • Es ist leicht zu ändern.

    Wenn Sie wissen, dass das Programm lesen kann cat, können Sie normalerweise davon ausgehen, dass es die Ausgabe von jedem Programm lesen kann, das an STDOUT ausgegeben wird. Auf diese Weise können Sie es an Ihre eigenen Bedürfnisse anpassen und vorhersehbare Ergebnisse erzielen.

  • Es wird betont, dass das Programm nicht fehlschlägt, wenn STDIN keine Datei ist.

    Es ist nicht sicher anzunehmen, dass wenn es program1 < foofunktioniert, cat foo | program1es auch funktioniert. Es ist jedoch sicher, das Gegenteil anzunehmen. Dieses Programm funktioniert, wenn STDIN eine Datei ist, schlägt jedoch fehl, wenn die Eingabe eine Pipe ist, da es seek verwendet:

    # works
    < foo perl -e 'seek(STDIN,1,1) || die;print <STDIN>'
    
    # fails
    cat foo | perl -e 'seek(STDIN,1,1) || die;print <STDIN>'

Leistungskosten

Die zusätzlichen Kosten fallen an cat. Um eine Vorstellung davon zu geben, wie oft ich einige Tests durchgeführt habe, um Baseline ( cat), niedrigen Durchsatz ( bzip2), mittleren Durchsatz ( gzip) und hohen Durchsatz ( grep) zu simulieren .

cat $ISO | cat
< $ISO cat
cat $ISO | bzip2
< $ISO | bzip2
cat $ISO | gzip
< $ISO gzip
cat $ISO | grep no_such_string
< $ISO grep no_such_string

Die Tests wurden auf einem Low-End-System (0,6 GHz) und einem normalen Laptop (2,2 GHz) durchgeführt. Sie wurden 10 Mal auf jedem System ausgeführt und das beste Timing wurde ausgewählt, um die optimale Situation für jeden Test nachzuahmen. Die $ ISO war ubuntu-11.04-desktop-i386.iso. (Schönere Tabellen hier: http://oletange.blogspot.com/2013/10/useless-use-of-cat.html )

CPU                       0.6 GHz ARM
Command                   cat $ISO|                        <$ISO                            Diff                             Diff (pct)
Throughput \ Time (ms)    User       Sys        Real       User       Sys        Real       User       Sys        Real       User       Sys        Real
Baseline (cat)                     55      14453      33090         23       6937      33126         32       7516        -36        239        208         99
Low (bzip2)                   1945148      16094    1973754    1941727       5664    1959982       3420      10430      13772        100        284        100
Medium (gzip)                  413914      13383     431812     407016       5477     416760       6898       7906      15052        101        244        103
High (grep no_such_string)      80656      15133      99049      79180       4336      86885       1476      10797      12164        101        349        114

CPU                       Core i7 2.2 GHz
Command                   cat $ISO|           <$ISO             Diff          Diff (pct)
Throughput \ Time (ms)    User     Sys Real   User   Sys Real   User Sys Real User       Sys Real
Baseline (cat)                    0 356    215      1  84     88    0 272  127          0 423  244
Low (bzip2)                  136184 896 136765 136728 160 137131 -545 736 -366         99 560   99
Medium (gzip)                 26564 788  26791  26332 108  26492  232 680  298        100 729  101
High (grep no_such_string)      264 392    483    216  84    304   48 308  179        122 466  158

Die Ergebnisse zeigen, dass bei geringem und mittlerem Durchsatz die Kosten in der Größenordnung von 1% liegen. Dies liegt innerhalb der Unsicherheit der Messungen, sodass in der Praxis kein Unterschied besteht.

Bei hohem Durchsatz ist der Unterschied größer und es gibt einen deutlichen Unterschied zwischen den beiden.

Das führt zu der Schlussfolgerung: Sie sollten <anstelle von cat |if verwenden:

  • Die Komplexität der Verarbeitung ähnelt einer einfachen Grep
  • Leistung ist wichtiger als Lesbarkeit.

Ansonsten spielt es keine Rolle, ob Sie <oder verwendencat | .

Und deshalb sollten Sie einen UUoC-Award nur dann vergeben, wenn:

  • Sie können einen signifikanten Unterschied in der Leistung messen (veröffentlichen Sie Ihre Messungen, wenn Sie die Auszeichnung vergeben).
  • Leistung ist wichtiger als Lesbarkeit.

-3

Ich denke, dass (auf traditionelle Weise) die Verwendung von Rohren etwas schneller ist; auf meiner Box habe ich benutztstrace Befehl um zu sehen, was los ist:

Ohne Rohr:

toc@UnixServer:~$ strace wc -l < wrong_output.c
execve("/usr/bin/wc", ["wc", "-l"], [/* 18 vars */]) = 0
brk(0)                                  = 0x8b50000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ad000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=29107, ...}) = 0
mmap2(NULL, 29107, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb77a5000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1552584, ...}) = 0
mmap2(NULL, 1563160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb7627000
mmap2(0xb779f000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x178) = 0xb779f000
mmap2(0xb77a2000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb77a2000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7626000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb76268d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb779f000, 8192, PROT_READ)   = 0
mprotect(0x804f000, 4096, PROT_READ)    = 0
mprotect(0xb77ce000, 4096, PROT_READ)   = 0
munmap(0xb77a5000, 29107)               = 0
brk(0)                                  = 0x8b50000
brk(0x8b71000)                          = 0x8b71000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=5540198, ...}) = 0
mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7426000
mmap2(NULL, 1507328, PROT_READ, MAP_PRIVATE, 3, 0x2a8) = 0xb72b6000
close(3)                                = 0
open("/usr/share/locale/locale.alias", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=2570, ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb77ac000
read(3, "# Locale name alias data base.\n#"..., 4096) = 2570
read(3, "", 4096)                       = 0
close(3)                                = 0
munmap(0xb77ac000, 4096)                = 0
open("/usr/share/locale/fr_FR.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr_FR.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr_FR/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale/fr/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr_FR.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr_FR.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr_FR/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr.UTF-8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr.utf8/LC_MESSAGES/coreutils.mo", O_RDONLY) = -1 ENOENT (No such file or directory)
open("/usr/share/locale-langpack/fr/LC_MESSAGES/coreutils.mo", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=316721, ...}) = 0
mmap2(NULL, 316721, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7268000
close(3)                                = 0
open("/usr/lib/i386-linux-gnu/gconv/gconv-modules.cache", O_RDONLY) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=26064, ...}) = 0
mmap2(NULL, 26064, PROT_READ, MAP_SHARED, 3, 0) = 0xb7261000
close(3)                                = 0
read(0, "#include<stdio.h>\n\nint main(int "..., 16384) = 180
read(0, "", 16384)                      = 0
fstat64(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(136, 2), ...}) = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb7260000
write(1, "13\n", 313
)                     = 3
close(0)                                = 0
close(1)                                = 0
munmap(0xb7260000, 4096)                = 0
close(2)                                = 0
exit_group(0)                           = ?

Und mit Pfeife:

toc@UnixServer:~$ strace cat wrong_output.c | wc -l
execve("/bin/cat", ["cat", "wrong_output.c"], [/* 18 vars */]) = 0
brk(0)                                  = 0xa017000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
mmap2(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb774b000
access("/etc/ld.so.preload", R_OK)      = -1 ENOENT (No such file or directory)
open("/etc/ld.so.cache", O_RDONLY)      = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=29107, ...}) = 0
mmap2(NULL, 29107, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb7743000
close(3)                                = 0
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY) = 3
read(3, "\177ELF\1\1\1\0\0\0\0\0\0\0\0\0\3\0\3\0\1\0\0\0p\222\1\0004\0\0\0"..., 512) = 512
fstat64(3, {st_mode=S_IFREG|0755, st_size=1552584, ...}) = 0
mmap2(NULL, 1563160, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xb75c5000
mmap2(0xb773d000, 12288, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_DENYWRITE, 3, 0x178) = 0xb773d000
mmap2(0xb7740000, 10776, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_FIXED|MAP_ANONYMOUS, -1, 0) = 0xb7740000
close(3)                                = 0
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xb75c4000
set_thread_area({entry_number:-1 -> 6, base_addr:0xb75c48d0, limit:1048575, seg_32bit:1, contents:0, read_exec_only:0, limit_in_pages:1, seg_not_present:0, useable:1}) = 0
mprotect(0xb773d000, 8192, PROT_READ)   = 0
mprotect(0x8051000, 4096, PROT_READ)    = 0
mprotect(0xb776c000, 4096, PROT_READ)   = 0
munmap(0xb7743000, 29107)               = 0
brk(0)                                  = 0xa017000
brk(0xa038000)                          = 0xa038000
open("/usr/lib/locale/locale-archive", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0644, st_size=5540198, ...}) = 0
mmap2(NULL, 2097152, PROT_READ, MAP_PRIVATE, 3, 0) = 0xb73c4000
mmap2(NULL, 1507328, PROT_READ, MAP_PRIVATE, 3, 0x2a8) = 0xb7254000
close(3)                                = 0
fstat64(1, {st_mode=S_IFIFO|0600, st_size=0, ...}) = 0
open("wrong_output.c", O_RDONLY|O_LARGEFILE) = 3
fstat64(3, {st_mode=S_IFREG|0664, st_size=180, ...}) = 0
read(3, "#include<stdio.h>\n\nint main(int "..., 32768) = 180
write(1, "#include<stdio.h>\n\nint main(int "..., 180) = 180
read(3, "", 32768)                      = 0
close(3)                                = 0
close(1)                                = 0
close(2)                                = 0
exit_group(0)                           = ?
13

Sie können einige Tests mit straceund timeBefehle mit mehr und längeren Befehlen durchführen, um ein gutes Benchmarking zu erzielen.


9
Ich verstehe nicht, was Sie unter (der traditionellen Art) der Verwendung von Pipe verstehen oder warum Sie denken strace, dass dies zeigt, dass es schneller ist - im zweiten Fall wird stracedie wc -lAusführung nicht verfolgt . Hier wird nur der erste Befehl der Pipeline verfolgt.
Kojiro

@kojiro: Ich meine mit traditionellem Weg = dem am häufigsten verwendeten Weg (ich denke, wir verwenden Pipe mehr als Indirektion), ich kann nicht bestätigen, dass es schneller ist oder nicht, in meiner Spur habe ich mehr Systemaufrufe für Indirektion gesehen. Sie können ein Wechselstromprogramm und eine Schleife verwenden, um zu sehen, ob einer mehr Zeit verbraucht. Wenn Sie interessiert sind, können wir es hier setzen :)
Inhaltsverzeichnis

3
Ein Äpfel-zu-Äpfel-Vergleich würde strace -f sh -c 'wc -l < wrong_output.c'daneben stehen strace -f sh -c 'cat wrong_output.c | wc -l'.
Charles Duffy

5
Hier sind Ergebnisse von ideone.com, die derzeit eindeutig für ohne sind cat: ideone.com/2w1W42#stderr
Tripleee

1
@CharlesDuffy: mkfifoerstellt eine benannte Pipe. Eine anonyme Pipe wird mit pipe(2)und dann gegabelt eingerichtet, wobei Eltern und Kind unterschiedliche Enden der Pipe schließen. Aber ja, diese Antwort ist totaler Unsinn und hat nicht einmal versucht, die Systemaufrufe zu zählen oder zu verwenden strace -O, um den Overhead zu messen oder -rjeden Anruf relativ zum letzten zu zeitstempeln ...
Peter Cordes
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.