Stellen Sie die letzte Zeile von stdin dem gesamten stdin voran

9

Betrachten Sie dieses Skript:

tmpfile=$(mktemp)

cat <<EOS > "$tmpfile"
line 1
line 2
line 3
EOS

cat <(tail -1 "$tmpfile") "$tmpfile"

Dies funktioniert und gibt aus:

line 3
line 1
line 2
line 3

Nehmen wir an, unsere Eingabequelle war keine tatsächliche Datei, sondern stdin:

cat <<EOS | # what goes here now?
line 1
line 2
line 3
EOS

Wie ändern wir den Befehl:

cat <(tail -1 "$tmpfile") "$tmpfile"

Damit es in diesem unterschiedlichen Kontext immer noch die gleiche Ausgabe erzeugt?

HINWEIS: Der spezifische Heredoc, den ich catte, sowie die Verwendung eines Heredoc selbst dienen lediglich der Veranschaulichung. Jede akzeptable Antwort sollte davon ausgehen, dass sie beliebige Daten über stdin empfängt .

Jona
quelle
1
stdin ist immer eine "tatsächliche Datei" (ein fifo / socket / etc ist auch eine Datei; nicht alle Dateien sind durchsuchbar). Die Antwort auf Ihre Frage ist entweder ein triviales "Verwenden einer temporären Datei" oder ein Horror, der die gesamte Datei in den Speicher lädt. "Wie kann ich alte Daten aus einem Stream abrufen, ohne sie irgendwo gespeichert zu haben ?" kann keine gute Antwort haben.
Mosvy
1
@mosvy Das ist eine absolut akzeptable Antwort, wenn Sie sie hinzufügen möchten.
Jonah
2
@mosvy Wie Jonah gesagt hat, sollten Antworten im Antwortfeld veröffentlicht werden. Ich weiß, dass es im Moment schwierig ist, eine der Websites zu lesen, aber bitte ignorieren Sie das Rot, das langsam über Ihre Sicht tropft, und verwenden Sie den unteren Textbereich.
wizzwizz4

Antworten:

7

Versuchen:

awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'

Beispiel

Definieren Sie eine Variable mit unserer Eingabe:

$ input="line 1
> line 2
> line 3"

Führen Sie unseren Befehl aus:

$ echo "$input" | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 3
line 1
line 2
line 3

Alternativ könnten wir natürlich ein here-doc verwenden:

$ cat <<EOS | awk '{x=x $0 ORS}; END{printf "%s", $0 ORS x}'
line 1
line 2
line 3
EOS
line 3
line 1
line 2
line 3

Wie es funktioniert

  • x=x $0 ORS

    Dadurch wird jede Eingabezeile an die Variable angehängt x.

    In awk ORSist das Trennzeichen für den Ausgabedatensatz . Standardmäßig ist es ein Zeilenumbruchzeichen.

  • END{printf "%s", $0 ORS x}

    Nachdem wir die gesamte Datei eingelesen haben, wird die letzte Zeile gedruckt $0, gefolgt vom Inhalt der gesamten Datei x.

Da dies die gesamte Eingabe in den Speicher liest, wäre es für große ( z . B. Gigabyte) Eingaben nicht geeignet .

John1024
quelle
Danke John. Ist es also nicht möglich, dies analog zu meinem Beispiel für eine benannte Datei im OP zu tun? Ich stellte mir vor, dass der Standard irgendwie dupliziert wird ... so wie es der Fall teeist, aber von einem Standard und einer Datei würden wir den gleichen Standard in zwei verschiedene Prozessersetzungen leiten. oder irgendetwas, das dem ungefähr gleichwertig wäre?
Jonah
5

Wenn stdin auf eine durchsuchbare Datei verweist (wie im Fall von Bashs (aber nicht allen anderen Shell-Dokumenten), die mit temporären Dateien implementiert sind), können Sie den Schwanz abrufen und dann zurücksuchen, bevor Sie den vollständigen Inhalt lesen:

suchen Betreiber in den zur Verfügung stehen zshoder ksh93Muscheln, oder Skriptsprachen wie Tcl / Perl / Python, aber nicht in bash. Sie können diese fortgeschrittenen Dolmetscher jedoch jederzeit anrufen, bashwenn Sie sie verwenden müssen bash.

ksh93 -c 'tail -n1; cat <#((0))' <<...

Oder

zsh -c 'zmodload zsh/system; tail -n1; sysseek 0; cat' <<...

Das funktioniert nicht, wenn stdin auf nicht durchsuchbare Dateien wie eine Pipe oder einen Socket verweist. Dann besteht die einzige Möglichkeit darin, die gesamte Eingabe zu lesen und zu speichern (im Speicher oder in einer temporären Datei ...).

Einige Lösungen zum Speichern im Speicher wurden bereits angegeben.

Mit einem Tempfile zshkönnten Sie es tun mit:

seq 10 | zsh -c '{ cat =(sed \$w/dev/fd/3); } 3>&1'

Wenn Sie unter Linux mit bashoder zsheiner Shell arbeiten, die temporäre Dateien für Here-Dokumente verwendet, können Sie die temporäre Datei, die von einem Here-Dokument erstellt wurde, tatsächlich zum Speichern der Ausgabe verwenden:

seq 10 | {
  chmod u+w /dev/fd/3 # only needed in bash5+
  cat > /dev/fd/3
  tail -n1 /dev/fd/3
  cat <&3
} 3<<EOF
EOF
Stéphane Chazelas
quelle
4
cat <<EOS | sed -ne '1{h;d;}' -e 'H;${G;p;}'
line 1
line 2
line 3
EOS

Das Problem bei der Übersetzung in etwas, das verwendet tailwird, besteht darin, dass taildie gesamte Datei gelesen werden muss, um das Ende zu finden. Um das in Ihrer Pipeline zu verwenden, müssen Sie

  1. Stellen Sie den vollständigen Inhalt des Dokuments bereit tail.
  2. Stellen Sie es erneut zur Verfügung cat.
  3. In dieser Reihenfolge.

Das schwierige Bit besteht nicht darin, den Inhalt des Dokuments zu duplizieren ( teetut dies), sondern die Ausgabe von tailzu erhalten, bevor der Rest des Dokuments ausgegeben wird, ohne eine temporäre Zwischendatei zu verwenden.

Durch die Verwendung sed(oder awkwie bei John1024 ) werden das doppelte Parsen der Daten und das Ordnungsproblem durch Speichern der Daten im Speicher beseitigt .

Die sedLösung, die ich vorschlage, ist zu

  1. 1{h;d;}Speichern Sie die erste Zeile unverändert im Haltebereich und fahren Sie mit der nächsten Zeile fort.
  2. HFügen Sie einander eine Zeile mit einer eingebetteten neuen Zeile an den Haltebereich an.
  3. ${G;p;}Fügen Sie den Haltebereich mit einer eingebetteten neuen Zeile an die letzte Zeile an und drucken Sie die resultierenden Daten.

Dies ist eine wörtliche Übersetzung der Lösung von John1024 in sed, mit der Einschränkung, dass der POSIX-Standard nur garantiert, dass der Speicherplatz mindestens 8192 Bytes (8 KiB) beträgt. Es wird jedoch empfohlen , diesen Puffer nach Bedarf dynamisch zuzuweisen und zu erweitern, was beide GNU sedund BSD sedmacht).


Wenn Sie sich erlauben, eine Named Pipe zu verwenden:

mkfifo mypipe
cat <<EOS | tee mypipe | cat <( tail -n 1 mypipe ) -
line 1
line 2
line 3
EOS
rm -f mypipe

Dies dient teezum Senden der Daten nach unten mypipeund gleichzeitig an cat. Das catDienstprogramm tailliest zuerst die Ausgabe von (die von liest, von mypipeder geschrieben teewird) und hängt dann die Kopie des Dokuments an, von der direkt stammt tee.

Dies ist jedoch ein schwerwiegender Fehler: Wenn das Dokument zu groß ist (größer als die Puffergröße der Pipe), wird in das Dokument teegeschrieben mypipeund catblockiert, während darauf gewartet wird, dass die (unbenannte) Pipe leer wird. Es würde nicht geleert werden, bis es daraus catgelesen wird. catwürde nicht davon lesen, bis tailes fertig war. Und tailwürde nicht fertig werden, bis teefertig war. Dies ist eine klassische Deadlock-Situation.

Die Variation

tee >( tail -n 1 >mypipe ) | cat mypipe -

hat das gleiche Problem.

Kusalananda
quelle
2
Der sedfunktioniert nicht, wenn der Eingang nur eine Zeile hat (vielleicht sed '1h;1!H;$!d;G'). Beachten Sie auch, dass einige sedImplementierungen eine niedrige Grenze für die Größe ihres Musters und des Speicherplatzes haben.
Stéphane Chazelas
Die genannte Rohrlösung ist das, wonach ich gesucht habe. Die Einschränkung ist eine Schande. Ich habe Ihre Erklärung verstanden, außer "Und der Schwanz würde nicht fertig werden, bis der Abschlag fertig ist" - können Sie näher erläutern, warum dies der Fall ist?
Jonah
2

Es gibt ein Tool, das peein einer Sammlung von Befehlszeilendienstprogrammen mit dem Namen "moreutils" (oder auf andere Weise von der Homepage abrufbar ) benannt ist.

Wenn Sie es auf Ihrem System haben können, lautet das Äquivalent für Ihr Beispiel wie folgt:

cat <<EOS | pee 'tail -1' cat 
line 1
line 2
line 3
EOS

peeDie Reihenfolge der durchlaufenden Befehle ist wichtig, da sie in der angegebenen Reihenfolge ausgeführt werden.

LL3
quelle
1

Versuchen:

cat <<EOS # | what goes here now? Nothing!
line 3
line 1
line 2
line 3
EOS

Da es sich bei dem Ganzen um Literaldaten handelt (ein "Hier-ist-Dokument") und der Unterschied zwischen dieser und der gewünschten Ausgabe trivial ist, massieren Sie diese Literaldaten genau dort, um sie an die Ausgabe anzupassen.

Nehmen wir nun an, es line 3kommt von irgendwoher und wird in einer Variablen namens gespeichert lastline:

cat <<EOS # | what goes here now? Nothing!
$lastline
line 1
line 2
$lastline
EOS

In einem hier gezeigten Dokument können wir Text durch Ersetzen von Variablen generieren. Darüber hinaus können wir Text mithilfe der Befehlssubstitution berechnen:

cat <<EOS
this is template text
here we have a hex conversion: $(printf "%x" 42)
EOS

Wir können mehrere Zeilen interpolieren:

cat <<EOS
multi line
preamble
$(for x in 3 1 2 3; do echo line $x ; done)
epilog
EOS

Vermeiden Sie im Allgemeinen die Textverarbeitung der hier vorgelegten Dokumentvorlage. Versuchen Sie, es mit interpoliertem Code zu generieren.

Kaz
quelle
1
Ich kann ehrlich gesagt nicht sagen, ob dies ein Witz ist oder nicht. Das cat <<EOS...im OP war nur ein Beispiel für "Catting einer beliebigen Datei", um den Beitrag spezifisch und die Frage klar zu machen. War Ihnen das wirklich nicht klar, oder dachten Sie nur, es wäre klug, die Frage wörtlich zu interpretieren?
Jonah
@Jonah Die Frage lautet eindeutig: "[l] et's sagen, dass unsere Eingabequelle, anstatt eine tatsächliche Datei zu sein, stattdessen stdin war:". Nichts über "beliebige Dateien"; Es geht um hier Dokumente. Ein hier doc ist nicht willkürlich. Es ist keine Eingabe für Ihr Programm, sondern ein Teil der Syntax, die der Programmierer auswählt.
Kaz
1
Ich denke, der Kontext und die vorhandenen Antworten haben deutlich gemacht, dass dies der Fall war, schon allein deshalb, weil Sie für eine korrekte Interpretation buchstäblich davon ausgehen mussten, dass weder ich noch eines der anderen Poster, die geantwortet haben, erkannt haben, dass es möglich ist, a zu kopieren und einzufügen Codezeile. Trotzdem werde ich die Frage bearbeiten, um sie explizit zu machen.
Jonah
1
Kaz, danke für die Antwort, aber beachte, dass dir trotz deiner Bearbeitung die Absicht der Frage fehlt. Sie erhalten eine beliebige mehrzeilige Eingabe über eine Pipe . Sie haben keine Ahnung, was es sein wird. Ihre Aufgabe ist es, die letzte Eingabezeile gefolgt von der gesamten Eingabe auszugeben.
Jonah
1
Kaz, die Eingabe gibt es nur als Beispiel. Die meisten Menschen, einschließlich ich, finden es hilfreich, ein Beispiel für echte Eingabe und erwartete Ausgabe zu haben, und nicht nur die abstrakte Frage. Sie sind der einzige, der davon verwirrt war.
Jonah
0

Wenn Sie sich nicht für die Bestellung interessieren. Dann wird das funktionieren cat lines | tee >(tail -1). Wie andere gesagt haben. Sie müssen die Datei zweimal lesen oder die gesamte Datei puffern, um dies in der von Ihnen gewünschten Reihenfolge zu tun.

Strg-Alt-Delor
quelle