it-swarm.com.de

Warum behebt Shell nicht automatisch die "nutzlose Verwendung von Katzen"?

Viele Leute verwenden Oneliner und Skripte, die Code in diesem Sinne enthalten

cat "$MYFILE" | command1 | command2 > "$OUTPUT"

Das erste cat wird oft als "nutzlose Verwendung von Katze" bezeichnet, da technisch ein neuer Prozess gestartet werden muss (oft /usr/bin/cat) wo dies vermieden werden könnte, wenn der Befehl gewesen wäre

< "$MYFILE" command1 | command2 > "$OUTPUT"

denn dann muss Shell nur noch command1 und zeigen Sie einfach mit stdin auf die angegebene Datei.

Warum führt die Shell diese Konvertierung nicht automatisch durch? Ich bin der Meinung, dass die Syntax "nutzlose Verwendung von cat" einfacher zu lesen ist und Shell über genügend Informationen verfügen sollte, um nutzlose cat automatisch zu entfernen. Das cat ist im POSIX-Standard definiert, daher sollte Shell die Möglichkeit haben, es intern zu implementieren, anstatt eine Binärdatei im Pfad zu verwenden. Die Shell könnte sogar nur eine Implementierung für genau eine Argumentversion enthalten und auf einen binären Pfad zurückgreifen.

28

Die 2 Befehle sind nicht gleichwertig: Berücksichtigen Sie die Fehlerbehandlung:

cat <file that doesn't exist> | less erzeugt einen leeren Stream, der an das Pipe-Programm übergeben wird. Als solches erhalten Sie eine Anzeige, die nichts anzeigt.

< <file that doesn't exist> less kann die Leiste nicht öffnen und öffnet dann überhaupt nicht weniger.

Der Versuch, Ersteres in Letzteres zu ändern, kann eine beliebige Anzahl von Skripten beschädigen, die das Programm mit einer möglicherweise leeren Eingabe ausführen sollen.

25
UKMonkey

Bei der "nutzlosen Verwendung von cat" geht es mehr darum, wie Sie Ihren Code schreiben, als darum, was tatsächlich ausgeführt wird, wenn Sie das Skript ausführen. Es ist eine Art Design Anti-Pattern , eine Art, etwas zu tun, das wahrscheinlich effizienter gemacht werden könnte. Es ist ein Fehler beim Verständnis, wie die angegebenen Werkzeuge am besten kombiniert werden können, um ein neues Werkzeug zu erstellen. Ich würde argumentieren, dass das Aneinanderreihen mehrerer sed und/oder awk Befehle in einer Pipeline manchmal auch als Symptom für dasselbe Anti-Muster bezeichnet werden kann.

Das Beheben von Instanzen der "nutzlosen Verwendung von cat" in einem Skript ist in erster Linie eine manuelle Korrektur des Quellcodes des Skripts. Ein Tool wie ShellCheck kann dabei helfen, indem es auf die offensichtlichen Fälle hinweist:

$ cat script.sh
#!/bin/sh
cat file | cat
$ shellcheck script.sh

In script.sh line 2:
cat file | cat
    ^-- SC2002: Useless cat. Consider 'cmd < file | ..' or 'cmd file | ..' instead.

Es wäre aufgrund der Art der Shell-Skripte schwierig, die Shell dazu zu bringen, dies automatisch zu tun. Die Ausführung eines Skripts hängt von der Umgebung ab, die von seinem übergeordneten Prozess geerbt wurde, und von der spezifischen Implementierung der verfügbaren externen Befehle.

Die Shell weiß nicht unbedingt, was cat ist. Es kann sich möglicherweise um einen beliebigen Befehl von einer beliebigen Stelle in Ihrem $PATH Oder um eine Funktion handeln.

Wenn es sich um einen integrierten Befehl handelt (der in einigen Shells enthalten sein kann), kann er die Pipeline so reorganisieren, wie es bekannt ist die Semantik des eingebauten Befehls cat. Zuvor müsste es zusätzlich Annahmen über den nächsten Befehl in der Pipeline nach dem ursprünglichen cat treffen.

Beachten Sie, dass sich das Lesen von Standardeingaben etwas anders verhält, wenn es mit einer Pipe verbunden ist und wenn es mit einer Datei verbunden ist. Eine Pipe ist nicht durchsuchbar. Je nachdem, was der nächste Befehl in der Pipeline tut, kann sie sich anders verhalten oder nicht, wenn die Pipeline neu angeordnet wurde (sie erkennt möglicherweise, ob die Eingabe suchbar ist, und entscheidet sich, die Dinge anders zu machen, wenn dies der Fall ist oder nicht es ist nicht so, auf jeden Fall würde es sich dann anders verhalten).

Diese Frage ähnelt (im sehr allgemeinen Sinne)) " Gibt es Compiler, die versuchen, Syntaxfehler selbst zu beheben? "(auf der Software Engineering StackExchange-Site), obwohl es sich bei dieser Frage offensichtlich um Syntaxfehler handelt, nicht um nutzlose Entwurfsmuster. Die Idee, den Code basierend auf der Absicht automatisch zu ändern, ist jedoch weitgehend dieselbe.

51
Kusalananda

Weil es nicht nutzlos ist.

Im Fall von cat file | cmd Ist das fd 0 (Stdin) von cmd eine Pipe, und im Fall von cmd <file Kann es eine reguläre sein Datei, Gerät usw.

Eine Pipe hat eine andere Semantik als eine reguläre Datei, und ihre Semantik ist keine Teilmenge derjenigen einer regulären Datei:

  • eine reguläre Datei kann nicht auf sinnvolle Weise select(2) ed oder poll(2) ed bearbeitet werden. Ein select(2) darauf gibt immer "ready" zurück. Erweiterte Schnittstellen wie epoll(2) unter Linux funktionieren einfach nicht mit normalen Dateien.

  • unter Linux gibt es Systemaufrufe (splice(2), vmsplice(2), tee(2)), die nur für Pipes funktionieren [1]

Da cat so häufig verwendet wird, könnte es als integrierte Shell implementiert werden, wodurch ein zusätzlicher Prozess vermieden wird. Sobald Sie jedoch auf diesem Pfad begonnen haben, können Sie mit den meisten Befehlen dasselbe tun - das Transformieren des Shell in eine langsamere und klobigere Perl oder python. Es ist wahrscheinlich besser, stattdessen eine andere Skriptsprache mit einer einfach zu verwendenden Pipe-ähnlichen Syntax für Fortsetzungen zu schreiben ;-)

[1] Wenn Sie ein einfaches Beispiel wünschen, das für diesen Anlass nicht erfunden wurde, können Sie sich meine "exec binary from stdin" git Gist mit einigen Erklärungen im Kommentar hier ansehen . Die Implementierung von cat darin, damit es ohne UUoC funktioniert, hätte es zwei- oder dreimal größer gemacht.

36
mosvy

Weil es wirklich sehr schwer ist, nutzlose Katzen zu entdecken.

Ich hatte ein Shell-Skript, in dem ich geschrieben habe

cat | (somecommand <<!
...
/proc/self/fd/3
...
!) 0<&3

Das Shell-Skript schlug in der Produktion fehl, wenn cat entfernt wurde, weil es über su -c 'script.sh' someuser Aufgerufen wurde. Das anscheinend überflüssige cat führte dazu, dass der Besitzer der Standardeingabe zu dem Benutzer wechselte, als den das Skript ausgeführt wurde, sodass das erneute Öffnen über /proc Funktionierte.

17
Joshua

tl; dr: Shells tun dies nicht automatisch, da die Kosten die überschreiten wahrscheinliche Vorteile.

Andere Antworten haben auf den technischen Unterschied zwischen stdin als Pipe und einer Datei hingewiesen. Vor diesem Hintergrund könnte die Shell Folgendes tun:

  1. Implementieren Sie cat als eingebautes Element, wobei die Unterscheidung zwischen Datei und Pipe beibehalten wird. Dies würde die Kosten eines Exec und möglicherweise einer Gabel sparen.
  2. Führen Sie eine vollständige Analyse der Pipeline mit Kenntnis der verschiedenen Befehle durch, um festzustellen, ob Datei/Pipe wichtig ist, und handeln Sie dann basierend darauf.

Als nächstes müssen Sie die Kosten und den Nutzen jedes Ansatzes berücksichtigen. Die Vorteile sind einfach genug:

  1. Vermeiden Sie in beiden Fällen eine Exec (von cat).
  2. Im zweiten Fall, wenn eine Umleitungssubstitution möglich ist, Vermeidung einer Gabel.
  3. In Fällen, in denen Sie ein Rohr verwenden müssen, kann es manchmal möglich sein, eine Gabel/Gabel zu vermeiden, aber oft nicht. Das liegt daran, dass das Katzenäquivalent gleichzeitig mit dem Rest der Pipeline ausgeführt werden muss.

Sie sparen also ein wenig CPU-Zeit und Speicher, insbesondere wenn Sie die Gabelung vermeiden können. Natürlich sparen Sie diese Zeit und diesen Speicher nur, wenn die Funktion tatsächlich verwendet wird. Und Sie sparen nur wirklich die Gabel-/Ausführungszeit; Bei größeren Dateien ist die Zeit meistens die E/A-Zeit (d. h. Katze liest eine Datei von der Festplatte). Sie müssen sich also fragen: Wie oft wird cat (nutzlos) in Shell-Skripten verwendet, bei denen die Leistung tatsächlich eine Rolle spielt? Vergleichen Sie es mit anderen gängigen Shell-Buildins wie test - es ist schwer vorstellbar, dass cat (nutzlos) sogar ein Zehntel so oft verwendet wird, wie test an wichtigen Stellen verwendet wird. Das ist eine Vermutung, die ich nicht gemessen habe, was Sie vor jedem Implementierungsversuch tun möchten. (Oder in ähnlicher Weise jemanden bitten, z. B. eine Funktionsanforderung zu implementieren.)

Als nächstes fragen Sie: Was sind die Kosten. Die beiden Kosten, die in den Sinn kommen, sind: (a) zusätzlicher Code in der Shell, der die Größe (und damit möglicherweise die Speichernutzung) erhöht, mehr Wartungsarbeiten erfordert, ein weiterer Punkt für Fehler usw.; und (b) Abwärtskompatibilitätsüberraschungen, POSIX cat lässt viele Funktionen von z. B. GNU coreutils cat weg, sodass Sie genau vorsichtig sein müssen was das eingebaute cat implementieren würde.

  1. Die zusätzliche eingebaute Option ist wahrscheinlich nicht so schlecht - fügen Sie eine weitere eingebaute hinzu, wenn bereits ein Haufen vorhanden ist. Wenn Sie Profildaten hätten, die zeigen, dass dies hilfreich ist, könnten Sie wahrscheinlich die Autoren Ihrer Lieblings-Shell davon überzeugen, sie hinzuzufügen.

  2. Was die Analyse der Pipeline angeht, glaube ich nicht, dass Shells derzeit so etwas tun (einige erkennen das Ende einer Pipeline und können eine Gabelung vermeiden). Im Wesentlichen würden Sie der Shell einen (primitiven) Optimierer hinzufügen. Optimierer erweisen sich oft als komplizierter Code und als Quelle vieler Fehler. Und diese Fehler können überraschend sein - geringfügige Änderungen im Shell-Skript können dazu führen, dass der Fehler vermieden oder ausgelöst wird.

Postscript: Sie können eine ähnliche Analyse auf Ihre nutzlosen Verwendungen von Katze anwenden. Vorteile: einfacher zu lesen (obwohl wenn command1 eine Datei als Argument verwendet, wahrscheinlich nicht). Kosten: extra Fork und Exec (und wenn Befehl1 eine Datei als Argument nehmen kann, wahrscheinlich verwirrendere Fehlermeldungen). Wenn Ihre Analyse Ihnen sagt, dass Sie Katze nutzlos verwenden sollen, fahren Sie fort.

13
derobert

Der Befehl cat kann - als Marker für stdin . ( POSIX , " Wenn eine Datei '-' ist, muss das Dienstprogramm cat an diesem Punkt aus der Standardeingabe lesen in der Reihenfolge. ") Dies ermöglicht die einfache Behandlung einer Datei oder stdin , wenn dies sonst nicht zulässig wäre.

Betrachten Sie diese beiden trivialen Alternativen, wobei das Shell-Argument $1 ist -:

cat "$1" | nl    # Works completely transparently
nl < "$1"        # Fails with 'bash: -: No such file or directory'

Ein anderes Mal, wenn cat nützlich ist, wird es absichtlich als No-Op verwendet, um einfach die Shell-Syntax beizubehalten:

file="$1"
reader=cat
[[ $file =~ \.gz$ ]] && reader=zcat
[[ $file =~ \.bz2$ ]] && reader=bzcat
"$reader" "$file"

Schließlich glaube ich, dass UUOC nur dann wirklich korrekt aufgerufen werden kann, wenn cat mit einem Dateinamen verwendet wird, von dem bekannt ist, dass er eine reguläre Datei ist (dh kein Gerät oder eine Named Pipe), und dass keine Flags vorhanden sind werden dem Befehl gegeben:

cat file.txt

In jeder anderen Situation können die Oropertien von cat selbst erforderlich sein.

10
roaima

Der Befehl cat kann Dinge tun, die die Shell nicht unbedingt tun kann (oder zumindest nicht einfach tun kann). Angenommen, Sie möchten Zeichen drucken, die ansonsten möglicherweise unsichtbar wären, z. B. Tabulatoren, Zeilenumbrüche oder Zeilenumbrüche. Es gibt * möglicherweise * eine Möglichkeit, dies nur mit integrierten Shell-Befehlen zu tun, aber ich kann mir keine vorstellen, die mir auf den Kopf kommt. Die GNU -Version von cat kann dies mit dem Argument -A Oder den Argumenten -v -E -T Tun (ich weiß jedoch nichts über andere Versionen von cat). Sie können jeder Zeile auch eine Zeilennummer mit -n Voranstellen (erneut IDK, wenn Nicht-GNU-Versionen dies können).

Ein weiterer Vorteil von cat ist, dass es problemlos mehrere Dateien lesen kann. Dazu kann man einfach cat file1 file2 file3 Eingeben. Dasselbe mit einer Shell zu tun, würde schwierig werden, obwohl eine sorgfältig ausgearbeitete Schleife höchstwahrscheinlich das gleiche Ergebnis erzielen könnte. Wollen Sie sich wirklich die Zeit nehmen, eine solche Schleife zu schreiben, wenn es eine so einfache Alternative gibt? Ich nicht!

Das Lesen von Dateien mit cat würde wahrscheinlich weniger CPU verbrauchen als die Shell, da cat ein vorkompiliertes Programm ist (die offensichtliche Ausnahme ist jede Shell, die eine eingebaute Katze hat). Beim Lesen einer großen Gruppe von Dateien kann dies offensichtlich werden, aber ich habe dies noch nie auf meinen Computern getan, daher kann ich nicht sicher sein.

Der Befehl cat kann auch nützlich sein, um einen Befehl zu zwingen, Standardeingaben in Fällen zu akzeptieren, in denen dies möglicherweise nicht der Fall ist. Folgendes berücksichtigen:

echo 8 | sleep

Die Zahl "8" wird vom Befehl "sleep" nicht akzeptiert, da sie eigentlich keine Standardeingabe akzeptieren sollte. Daher wird der Schlaf diese Eingabe ignorieren, sich über einen Mangel an Argumenten beschweren und beenden. Wenn man jedoch Folgendes eingibt:

echo 8 | sleep $(cat)

Viele Shells erweitern dies auf sleep 8 Und der Schlaf wartet 8 Sekunden, bevor er beendet wird. Mit ssh können Sie auch etwas Ähnliches tun:

command | ssh 1.2.3.4 'cat >> example-file'

Dieser Befehl mit Append-Beispieldatei auf dem Computer mit der Adresse 1.2.3.4 mit allem, was von "Befehl" ausgegeben wird.

Und das kratzt (wahrscheinlich) nur an der Oberfläche. Ich bin sicher, ich könnte mehr Beispiele für Katzen finden, die nützlich sind, wenn ich wollte, aber dieser Beitrag ist so lang wie er ist. Abschließend möchte ich Folgendes sagen: Es ist nicht wirklich machbar, die Shell zu bitten, all diese Szenarien (und mehrere andere) vorwegzunehmen.

6
TSJNachos117

Denken Sie daran, dass ein Benutzer ein cat in seinem $PATH das ist nicht genau das POSIX cat (aber vielleicht eine Variante, die irgendwo etwas protokollieren könnte). In diesem Fall soll die Shell sie nicht entfernen.

Das PATH könnte sich dynamisch ändern, und dann ist cat nicht das, was Sie glauben. Es wäre ziemlich schwierig, eine Shell zu schreiben, die die Optimierung ausführt, von der Sie träumen.

In der Praxis ist cat auch ein recht schnelles Programm. Es gibt nur wenige praktische Gründe (außer der Ästhetik), um dies zu vermeiden.

Siehe auch den ausgezeichneten Parsing POSIX [s] hell Vortrag von Yann Regis-Gianas auf der FOSDEM2018. Es gibt andere gute Gründe, um nicht zu versuchen, das zu tun, wovon Sie in einer Shell träumen.

Wenn die Leistung wirklich ein Problem für Shells wäre, hätte jemand eine Shell vorgeschlagen, die ausgefeilte Compileroptimierung für das gesamte Programm, statische Quellcode-Analyse und Just-in-Time-Kompilierungstechniken verwendet (alle diese drei Bereiche haben jahrzehntelangen Fortschritt und wissenschaftliche Veröffentlichungen und sind dediziert Konferenzen, zB unter SIGPLAN ). Leider wird dies selbst als interessantes Forschungsthema derzeit nicht von Forschungsagenturen oder Risikokapitalgebern finanziert, und ich schließe daraus, dass sich die Mühe einfach nicht lohnt. Mit anderen Worten, es gibt wahrscheinlich keinen signifikanten Markt für die Optimierung von Schalen . Wenn Sie eine halbe Million Euro für solche Forschungen ausgeben müssen, werden Sie leicht jemanden finden, der dies tut, und ich glaube, das würde zu lohnenden Ergebnissen führen.

Praktisch gesehen wird üblicherweise ein kleines (einhundert Zeilen) Shell-Skript in einer besseren Skriptsprache (Python, AWK, Guile, ...) neu geschrieben, um die Leistung zu verbessern. Und es ist (aus vielen Gründen der Softwareentwicklung) nicht sinnvoll, große Shell-Skripte zu schreiben: Wenn Sie ein Shell-Skript mit mehr als hundert Zeilen schreiben, müssen Sie in Betracht ziehen, es (auch aus Gründen der Lesbarkeit und Wartung) in einer geeigneteren Sprache neu zu schreiben : als Programmiersprache die Shell ist sehr schlecht. Es gibt jedoch viele große generiert Shell-Skripte und das aus guten Gründen (z. B. GNU autoconf generierte configure Skripte).

In Bezug auf große Textdateien ist es keine gute Praxis, sie als single Argument an cat zu übergeben, und die meisten Systemadministratoren wissen dies (wenn die Ausführung eines Shell-Skripts länger als eine Minute dauert). Sie erwägen, es zu optimieren. Für große Gigabyte-Dateien ist cat nie das gute Werkzeug, um sie zu verarbeiten.

Neben der Antwort von @Kusalananda (und dem Kommentar von @alephzero) könnte die Katze alles sein:

alias cat='gcc -c'
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

oder

echo 'echo 1' > /usr/bin/cat
cat "$MYFILE" | command1 | command2 > "$OUTPUT"

Es gibt keinen Grund, warum cat (allein) oder/usr/bin/cat im System tatsächlich cat das Verkettungstool ist.

2
Rob

Zwei "nutzlose" Anwendungen für Katzen:

sort file.txt | cat header.txt - footer.txt | less

... hier wird cat verwendet, um Datei- und Pipe-Eingaben zu mischen.

find . -name '*.info' -type f | sh -c 'xargs cat' | sort

... hier kann xargs eine praktisch unendliche Anzahl von Dateinamen akzeptieren und cat so oft wie nötig ausführen, während sich alles wie ein Stream verhält. Dies funktioniert also für große Dateilisten, bei denen die direkte Verwendung von xargs sort Nicht möglich ist.

1
tasket

Abgesehen von anderen Dingen würde die Überprüfung von cat- zusätzlichen Leistungsaufwand und Verwirrung darüber verursachen, welche Verwendung von cat tatsächlich nutzlos ist, IMHO, da solche Überprüfungen ineffizient sein und Probleme mit der Legitimität verursachen können cat Verwendung.

Wenn Befehle mit den Standard-Streams arbeiten, müssen sie sich nur um das Lesen/Schreiben in die Standard-Dateideskriptoren kümmern. Befehle können erkennen, ob stdin seekable/lseekable ist oder nicht, was auf eine Pipe oder Datei hinweist.

Wenn wir der Mischung hinzufügen, um zu überprüfen, welcher Prozess tatsächlich diesen Standardinhalt liefert, müssen wir den Prozess auf der anderen Seite des Rohrs finden und eine entsprechende Optimierung anwenden. Dies kann in Bezug auf Shell selbst geschehen, wie im Beitrag SuperUser von Kyle Jones gezeigt, und in Bezug auf Shell

(find /proc -type l | xargs ls -l | fgrep 'pipe:[20043922]') 2>/dev/null

wie im verlinkten Beitrag gezeigt. Dies sind 3 weitere Befehle (also zusätzliche fork()s und exec()s) und rekursive Durchquerungen (also eine ganze Reihe von readdir() Aufrufen).

In Bezug auf C- und Shell-Quellcode kennt die Shell den untergeordneten Prozess bereits, sodass keine Rekursion erforderlich ist. Woher wissen wir jedoch, wann optimiert werden muss und wann cat tatsächlich nutzlos ist? Es gibt tatsächlich nützliche Verwendungen von cat , wie z

# adding header and footer to file
( cmd; cat file; cmd ) | cmd
# tr command does not accept files as arguments
cat log1 log2 log3 | tr '[:upper:]' '[:lower:]'

Es wäre wahrscheinlich Verschwendung und unnötiger Aufwand, der Shell eine solche Optimierung hinzuzufügen. Wie in Kusalandas Antwort bereits erwähnt, geht es bei UUOC eher um das mangelnde Verständnis des Benutzers, wie Befehle am besten kombiniert werden können, um die besten Ergebnisse zu erzielen.

0