Warum wird der Befehl „ls | file ”arbeiten?

32

Ich habe mich mit der Befehlszeile befasst und gelernt, dass |(Pipeline) die Ausgabe eines Befehls auf die Eingabe eines anderen Befehls umleiten soll. Warum funktioniert der Befehl ls | filealso nicht?

file input ist einer von mehreren Dateinamen wie file filename1 filename2

lsDie Ausgabe ist eine Liste der Verzeichnisse und Dateien in einem Ordner, daher ls | filesollte der Dateityp jeder Datei in einem Ordner angezeigt werden.

Wenn ich es jedoch benutze, ist die Ausgabe:

    Usage: file [-bcEhikLlNnprsvz0] [--apple] [--mime-encoding] [--mime-type]
        [-e testname] [-F separator] [-f namefile] [-m magicfiles] file ...
    file -C [-m magicfiles]
    file [--help]

Da gab es einen Fehler bei der Verwendung des fileBefehls

IanC
quelle
2
Wenn Sie plain verwenden ls, bedeutet dies, dass alle Dateien im aktuellen Verzeichnis mit dem fileBefehl verarbeitet werden sollen. ... Warum also nicht einfach machen file *:, das mit einer Zeile für jede Datei, Ordner antwortet.
Knud Larsen
file *ist der klügste Weg, ich habe mich nur gefragt, warum die lsAusgabe nicht funktioniert. Zweifel beseitigt :)
IanC
6
Die Prämisse ist fehlerhaft: "Dateieingabe ist einer von mehreren Dateinamen, wie z. B. Dateiname1 Dateiname2". Das ist keine Eingabe. Dies sind Befehlszeilenargumente, wie @John Kugelman unten ausführt.
Monty Harder
3
Tangential, Parsenls ist in der Regel eine schlechte Idee.
Kojiro

Antworten:

71

Das grundlegende Problem besteht darin, filedass Dateinamen als Befehlszeilenargumente und nicht als stdin erwartet werden. Beim Schreiben wird ls | filedie Ausgabe von lsals Eingabe an übergeben file. Nicht als Argumente, als Eingabe.

Was ist der Unterschied?

  • Befehlszeilenargumente sind, wenn Sie Flags und Dateinamen nach einem Befehl schreiben, wie in cmd arg1 arg2 arg3. In Shell - Skripten sind diese Argumente als die Variablen $1, $2, $3usw. C Sie sie über den Zugriff hatte char **argvund int argcArgumente main().

  • Die Standardeingabe stdin ist ein Datenstrom. Einige Programme mögen catoder wclesen von stdin, wenn ihnen keine Befehlszeilenargumente gegeben werden. In einem Shell-Skript können Sie readeine einzelne Eingabezeile abrufen. In C können Sie verwenden , scanf()oder getchar(), unter den verschiedenen Optionen.

fileliest normalerweise nicht von stdin. Es wird erwartet, dass mindestens ein Dateiname als Argument übergeben wird. Aus diesem Grund wird die Verwendung beim Schreiben ausgedruckt ls | file, weil Sie kein Argument übergeben haben.

Sie können xargsstdin wie in in Argumente umwandeln ls | xargs file. Dennoch, wie Terdon erwähnt , ist Parsen lseine schlechte Idee. Der direkteste Weg, dies zu tun, ist einfach:

file *
John Kugelman unterstützt Monica
quelle
2
Oder erzwingen Sie filemithilfe von Dateinamen aus der Eingabe ls | file -f -. Immer noch eine schlechte Idee.
Spektren
2
@Braiam> Darum geht es. Und das pfeift lsdie Ausgabe in file's stdin. Versuch es.
Spektren
4
@Braiam> In der Tat ist es verschwenderisch und gefährlich. Aber es funktioniert und es ist schön, es zu haben, um es mit besseren Optionen zu vergleichen, wenn das OP lernt, Umleitungen zu verwenden. Der Vollständigkeit halber könnte ich auch file $(ls)noch auf andere Weise erwähnen , was auch funktioniert.
Spektren
2
Ich denke, nachdem ich alle Antworten gelesen habe, habe ich ein größeres Bild des Problems, obwohl ich denke, dass ich weitere Lektüre brauche, um alles wirklich zu verstehen. Erstens wird die Ausgabe anscheinend nicht als Argumente , sondern als STDIN analysiert , wenn Piping und Redirecting verwendet werden . Was ich habe noch weiter zu lesen , besser zu verstehen, aber eine oberflächliche Suche machen Argumente scheint , wie Text in einem Array , um das Programm analysiert werden und STDIN wie ein Weg , um Informationen für eine Datei von Pooling oder einem Ausgang (nicht alle Programme werden zu Arbeit mit diesem "Pooling")
IanC
3
Zweitens scheint die Verwendung von ls zur Erstellung einer Liste von Dateinamen eine schlechte Idee zu sein, da Sonderzeichen in Dateinamen akzeptiert werden, auf ls jedoch zu einer irreführenden Ausgabe führen können . Da Zeilenumbrüche als Trennzeichen zwischen Dateinamen verwendet werden und Dateinamen Zeilenumbrüche und andere Sonderzeichen enthalten können , ist die endgültige Ausgabe möglicherweise nicht präzise.
IanC
18

Weil, wie Sie sagen, der Eingang filehat seinen Dateinamen . Die Ausgabe von lsist jedoch nur Text. Dass es sich zufällig um eine Liste von Dateinamen handelt, ändert nichts an der Tatsache, dass es sich lediglich um Text handelt und nicht um den Speicherort von Dateien auf der Festplatte.

Wenn Sie die Ausgabe auf dem Bildschirm sehen, sehen Sie Text. Ob dieser Text ein Gedicht oder eine Liste von Dateinamen ist, spielt für den Computer keine Rolle. Alles was es weiß ist, dass es Text ist. Aus diesem Grund können Sie die Ausgabe von lsan Programme übergeben, die Text als Eingabe verwenden (obwohl Sie dies wirklich, wirklich nicht tun sollten ):

$ ls / | grep etc
etc

Um die Ausgabe eines Befehls zu verwenden, der Dateinamen als Text (z. B. lsoder find) als Eingabe für einen Befehl auflistet , der Dateinamen akzeptiert, müssen Sie einige Tricks anwenden. Das typische Werkzeug dafür ist xargs:

$ ls
file1 file2

$ ls | xargs wc
 9  9 38 file1
 5  5 20 file2
14 14 58 total

Wie ich bereits sagte, möchten Sie die Ausgabe von nicht wirklich analysieren ls. Etwas wie findist besser (das gibt nach jedem Dateinamen print0ein \0statt eines neuen aus und das -0von xargslässt es mit solchen Eingaben umgehen; dies ist ein Trick, damit Ihre Befehle mit Dateinamen funktionieren, die Zeilenumbrüche enthalten):

$ find . -type f -print0 | xargs -0 wc
 9  9 38 ./file1
 5  5 20 ./file2
14 14 58 total

Welches hat auch seine eigene Art, dies zu tun, ohne überhaupt zu brauchen xargs:

$ find . -type f -exec wc {} +
 9  9 38 ./file1
 5  5 20 ./file2
14 14 58 total

Schließlich können Sie auch eine Shell-Schleife verwenden. Beachten Sie jedoch, dass dies in den meisten Fällen xargsviel schneller und effizienter ist. Beispielsweise:

$ for file in *; do wc "$file"; done
 9  9 38 file1
 5  5 20 file2
terdon
quelle
Ein Neben Problem ist , dass filescheint nicht wirklich zu lesen , es sei denn , eine explizite gegeben stdin -Platzhalter: vergleichen file foo, echo foo | fileund echo foo | file -; Tatsächlich ist dies wahrscheinlich der Grund für die Verwendungsmeldung im OPs-Fall (dh nicht wirklich, weil die Ausgabe von ls"einfach Text" ist, sondern weil die Argumentliste fileleer ist)
steeldriver
@steeldriver ja. AFAIK das ist der Fall für alle Programme, die Dateien und nicht Text als Eingabe erwarten. Standardmäßig ignorieren sie nur stdin. Beachten Sie, dass echo foo | file -dies nicht filefür die Datei, foosondern für den stdin-Stream ausgeführt wird.
Terdon
Nun, es gibt solche seltsamen Enten (?!) Mit catAusnahme von stdin ohne -Ausnahme der angegebenen Dateiargumente, denke ich.
Steeldriver
3
Diese Antwort kann den Unterschied zwischen Standard- und Befehlszeilenargumenten nicht erklären und ist aus dem gleichen Grund immer noch zutiefst irreführend, obwohl sie aussagekräftiger als die akzeptierte Antwort ist.
zwol
5
@terdon Ich denke, das ist ein schwerwiegender Fehler in diesem Fall. "file (1) verwendet die Liste der zu bearbeitenden Dateien als Befehlszeilenargumente und nicht als Standardeingabe" ist von grundlegender Bedeutung für das Verständnis, warum der OP-Befehl nicht funktioniert hat, und die Unterscheidung ist von grundlegender Bedeutung für das Shell-Scripting im Allgemeinen. Sie tun ihnen keinen Gefallen, indem Sie sie beschönigen.
zwol
6

lernte, dass '|' (Pipeline) soll die Ausgabe von einem Befehl an die Eingabe eines anderen umleiten .

Die Ausgabe wird nicht "umgeleitet", sondern die Ausgabe eines Programms wird als Eingabe verwendet, während die Datei keine Eingaben, sondern Dateinamen als Argumente verwendet , die dann getestet werden. Bei Umleitungen werden diese Dateinamen weder als Argumente noch als Piping übergeben , je später Sie dies tun.

Sie können die Dateinamen aus einer Datei mit der --files-fromOption lesen, wenn Sie eine Datei haben, in der alle Dateien aufgelistet sind, die Sie testen möchten. Andernfalls übergeben Sie einfach die Pfade zu Ihren Dateien als Argumente.

Braiam
quelle
6

Die akzeptierte Antwort erklärt, warum der Pipe-Befehl nicht sofort funktioniert und mit dem file *Befehl eine einfache, direkte Lösung bietet.

Ich würde gerne eine andere Alternative vorschlagen, die sich irgendwann als nützlich erweisen könnte. Der Trick ist die Verwendung des Backtick- (`)Zeichens. Das Backtick wird hier ausführlich erklärt . Kurz gesagt, es nimmt die Ausgabe des Befehls, der in den Backticks enthalten ist, und ersetzt ihn als Zeichenfolge im verbleibenden Befehl.

Nimmt also find `ls`die Ausgabe des lsBefehls und ersetzt sie als Argumente für den findBefehl. Dies ist länger und komplizierter als die akzeptierte Lösung, aber Varianten davon können in anderen Situationen hilfreich sein.

Schmuddi
quelle
Ich lese ein Buch über die Verwendung der Befehlszeile unter Linux (der Zweifel kam von mir, als ich damit experimentierte), und zufällig habe ich gerade über "Befehlsersetzung" gelesen. Sie können entweder $ (Befehl) oder command(den Backslash-Code auf meinem Telefon nicht finden) verwenden, um die Ausgabe eines Befehls in der Bash zu erweitern und ihn als Parameter für andere Befehle zu verwenden. Wirklich nützlich, auch wenn die Verwendung in diesem Fall (mit ls ) aufgrund der Sonderzeichen in einigen Dateinamen zu Problemen führen würde.
IanC
@IanC Leider sind die meisten Bücher und Tutorials über Bash Müll, der mit schlechten Praktiken, veralteter Syntax und subtilen Fehlern behaftet ist. (die einzigen) vertrauenswürdigen Referenzen sind die Bash-Entwickler, dh das Handbuch und der IRC-Channel #bash auf freenode (sehen Sie sich auch die Ressourcen an, die im Channel-Thema verlinkt sind).
Ignis
1
Die Verwendung der Befehlssubstitution kann manchmal sehr hilfreich sein, ist aber in diesem Zusammenhang ziemlich pervers - insbesondere bei ls.
Joe
5

Die Ausgabe von lsüber eine Pipe ist ein solider Datenblock mit 0x0a, der jede Zeile - dh ein Zeilenvorschubzeichen - trennt, und filewird als ein Parameter abgerufen, bei dem erwartet wird, dass mehrere Zeichen gleichzeitig an einem arbeiten.

lsGenerieren Sie in der Regel niemals eine Datenquelle für andere Befehle - eines Tages wird sie weitergeleitet ... rmund Sie haben dann Probleme!

Verwenden Sie besser eine Schleife, for i in *; do file "$i" ; donedie vorhersehbar die gewünschte Ausgabe erzeugt. Die Anführungszeichen stehen bei Dateinamen mit Leerzeichen.

Mark Williams
quelle
8
einfacher: file *;-)
Wayne_Yux
3
@IanC Ich kann wirklich nicht genug betonen, dass das Parsen der Ausgabe von lseine sehr, sehr schlechte Idee ist . Nicht nur, weil Sie es möglicherweise an etwas Schädliches weitergeben, wie zum Beispiel rm, was noch wichtiger ist, weil es bei nicht standardmäßigen Dateinamen bricht.
Terdon
5
Der erste Absatz liegt irgendwo zwischen irreführendem und reinem Unsinn. Zeilenumbrüche haben keine Relevanz. Der zweite Absatz ist aus dem falschen Grund richtig. Es ist schlecht, ls zu analysieren, aber nicht, weil es auf magische Weise zu rm "geleitet" werden könnte.
John Kugelman unterstützt Monica
1
Übernimmt rmDateinamen von der Standardeingabe? Ich denke nicht. Auch ist in der Regel lseines der wichtigsten Beispiele für eine Datenquelle für die Verwendung von Unix-Pipelines seit Beginn von Unix. Aus diesem Grund wird standardmäßig ein einfacher Dateiname pro Zeile ohne Attribute oder Verzierungen verwendet, wenn es sich bei der Ausgabe um eine Pipe handelt, anders als bei der üblichen Standardformatierung, wenn die Ausgabe das Terminal ist.
Davidbak
2
@DewiMorgan Diese Website richtet sich hauptsächlich an ein nicht-technisches Publikum. Das Verbreiten / Fördern von schlechten Gewohnheiten hier schadet also und tut nichts Gutes. Auf unix.SE oder einer anderen Tech-Community, deren Benutzer das Wissen / die Mittel haben, sehr nahe an ihre Füße zu zielen, ohne selbst auf die Füße zu schießen, mag Ihr Standpunkt (in Bezug auf andere Praktiken) zutreffen, aber hier lässt er Ihren Kommentar nicht schlau aussehen.
Ignis
4

Wenn Sie eine Pipe zum Feeden fileverwenden möchten, verwenden Sie die Option, auf -fdie normalerweise ein Dateiname folgt. Sie können jedoch auch einen einzelnen Bindestrich -zum Lesen von stdin verwenden

$ ls
cow.pdf  some.txt
$ ls | file -f -
cow.pdf:       PDF document, version 1.4
some.txt:        ASCII text

Der Trick mit dem Bindestrich -funktioniert mit vielen Standard-Befehlszeilenprogrammen (obwohl dies --manchmal der Fall ist ), daher ist es immer einen Versuch wert.

Das Tool xargist viel leistungsfähiger und wird in den meisten Fällen nur benötigt, wenn die Argumentliste zu lang ist ( Details finden Sie in diesem Beitrag ).

deamentiaemundi
quelle
Wann ist es -- ? Ich habe das noch nie gesehen. --ist in der Regel der Indikator "Ende der Flags".
John Kugelman unterstützt Monica
Ja, aber ich habe es in einigen Fällen (ab) gefunden, die vom Programmierer auf diese Weise verwendet wurden. Ich kann mich nicht genau erinnern, wo (werde einen Kommentar hinzufügen, wenn ich das tue), aber ich erinnere mich an die Flüche, die ich aussprach, als ich es herausfand und diese Flüche waren definitiv NSFW ;-)
deamentiaemundi
2

Es funktioniert mit dem folgenden Befehl

ls | xargs file

Es wird für mich besser funktionieren

SuperKrish
quelle