E / A-Umleitung und der Kopfbefehl

9

Ich habe heute versucht, schnell eine .hgignoreDatei aus der Cygwin-Bash-Shell zu bearbeiten , und eine Zeile hinzugefügt, die ein Fehler war. Ich bin mir nicht sicher, ob dies der beste Weg ist, aber ich dachte schnell darüber head -1 .hgignorenach, die fehlerhafte Zeile zu entfernen (ich hatte zuvor nur eine Zeile in der Datei). Sicher genug, wenn es ausgeführt wird, gibt es die erste Zeile als einzige Ausgabe.

Aber als ich versuchte, die Ausgabe umzuleiten und die Datei mit neu zu schreiben head -1 .hgignore > .hgignore, war die Datei leer. Warum passiert das? Wenn ich stattdessen das Anhängen versuche, head -1 .hgignore >> .hgignorewird es korrekt angehängt, aber dies ist offensichtlich nicht das gewünschte Ergebnis. Warum funktioniert eine Kürzungsumleitung in diesem Fall nicht?

voithos
quelle

Antworten:

10

Wenn die Shell eine Befehlszeile wie folgt erhält: command > file.outDie Shell selbst öffnet (und erstellt möglicherweise) die benannte Datei file.out. Die Shell setzt den Dateideskriptor 0 auf den Dateidateideskriptor, den sie vom Öffnen erhalten hat. So funktioniert die E / A-Umleitung: Jeder Prozess kennt die Dateideskriptoren 0, 1 und 2.

Der schwierige Teil dabei ist, wie man öffnet file.out. Meistens möchten Sie file.outmit Offset 0 zum Schreiben geöffnet (dh abgeschnitten), und dies hat die Shell für Sie getan. Es hat .hgignore abgeschnitten, zum Schreiben geöffnet, den Dateiskriptor auf 0 getäuscht und dann ausgeführt head. Sofortiges Überladen von Dateien.

In der Bash-Shell set noclobberändern Sie dieses Verhalten.

Bruce Ediger
quelle
Aha, ich verstehe. Ich dachte, dass die Shell die Datei abschneidet, bevor der Befehl ausgeführt wird, aber ich wusste nicht warum. Danke für die Erklärung!
Voithos
10

Ich denke, Bruce antwortet mit der Shell-Pipeline, was hier los ist .

Eines meiner kleinen Lieblingsprogramme ist der spongeBefehl von moreutils . Es löst genau dieses Problem, indem es alle verfügbaren Eingaben "aufnimmt", bevor es die Zielausgabedatei öffnet und die Daten schreibt. Damit können Sie Pipelines genau so schreiben, wie Sie es erwartet haben:

$ head -1 .hgignore | sponge .hgignore

Die Lösung für den armen Mann besteht darin, die Ausgabe in eine temporäre Datei weiterzuleiten. Nachdem die Pipline ausgeführt wurde (z. B. der nächste Befehl, den Sie ausführen), wird die temporäre Datei wieder an den ursprünglichen Speicherort der Datei verschoben.

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}
Caleb
quelle
Als ich mir das einige Jahre später ansah, kam mir der Gedanke: Könnten wir das nicht einfach tun head -1 .hgignore | tee .hgignore? teeist in coreutils, und als Vorteil / Nebeneffekt schreibt dies auch an STDOUT
voithos
@voithos Meines Wissens teewird die Datei, in die geschrieben wird, geöffnet und abgeschnitten, wenn sie wie alles andere instanziiert wird, sodass das Hauptproblem der Race-Bedingung beim Lesen des Dateiinhalts vor dem Abschneiden beim Schreiben nicht gelöst wird.
Caleb
Sie sprechen einen Punkt an, von dem ich eigentlich nichts wusste - nämlich, dass Pipe-Befehle sofort und nicht nacheinander gestartet werden. Ist das richtig? Ich habe es jedoch ausprobiert und tee scheint das Gewünschte zu tun. Ich habe eine Version 8.13auf meinem Computer.
Voithos
1
@voithos Ja-Befehle in einer Pipline und alle beteiligten Ein- / Ausgabekanäle werden in umgekehrter Reihenfolge gestartet, sodass die Pipeline bereit ist, Daten zu empfangen, wenn der erste beginnt, sie zu geben. Ich vermute, Ihr Test ist fehlerhaft, weil Sie wahrscheinlich einen zu kleinen Datenblock verwendet haben und das Ganze in einem Lesepuffer zwischengespeichert wurde, bevor Sie es brauchten. Das teeProgramm schneidet Ihre Dateien ab, es ist nicht so eingerichtet, dass sie doppelt gepuffert werden.
Caleb
3

Im

head -n 1 file > file

filewird abgeschnitten, bevor headgestartet wird, aber wenn Sie es schreiben:

head -n 1 file 1<> file

Es ist nicht so, wie filees im Lese- / Schreibmodus geöffnet ist. Wenn Sie mit dem headSchreiben fertig sind, wird die Datei jedoch nicht abgeschnitten, sodass die obige Zeile ein No-Op ist ( headschreiben Sie einfach die erste Zeile über sich selbst und lassen Sie die anderen Zeilen unberührt).

Nach headder Rückkehr und solange das fdnoch geöffnet ist, können Sie einen anderen Befehl aufrufen, der das ausführt truncate.

Zum Beispiel:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

Was hier wichtig ist, ist, dass truncateoben headnur der Cursor für fd 1 innerhalb der Datei direkt nach der ersten Zeile bewegt wird. Es schreibt die erste Zeile neu, für die wir sie nicht brauchten, aber das ist nicht schädlich.

Mit einem POSIX-Kopf könnten wir tatsächlich davonkommen, ohne die erste Zeile neu zu schreiben:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

Hier verwenden wir die Tatsache, dass headdie Cursorposition in ihrem Standard verschoben wird. Während headPOSIX normalerweise seine Eingaben von großen Blöcken lesen würde, um die Leistung zu verbessern, würde es (wo möglich) erfordern, dass es seekkurz nach der ersten Zeile zurückgesetzt wird, wenn es darüber hinausgegangen wäre. Beachten Sie jedoch, dass dies nicht bei allen Implementierungen der Fall ist.

Alternativ können Sie readin diesem Fall stattdessen den Befehl der Shell verwenden :

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file
Stéphane Chazelas
quelle
1
Stephane, kennen Sie einen Standard- oder Coreutils-Befehl, der STDINähnlich wie perloben beschrieben abgeschnitten werden kann
iruvar
2
@ 1_CR, nein. ddkann jedoch bei jedem beliebigen absoluten Versatz in der Datei abgeschnitten werden . So können Sie den Byte-Offset der zweiten Zeile bestimmen und von dort mitdd bs=1 seek="$offset" of=file
Stéphane Chazelas
1

Die Lösung des echten Mannes ist

ed .hgignore
$d
wq

oder als Einzeiler

printf '%s\n' '$d' 'wq' | ed .hgignore

Oder mit GNU sed:

sed -i '$d' .hgignore

(Nein, ich mache Witze. Ich würde einen interaktiven Editor verwenden. vi .hgignore GddZZ)

Gilles 'SO - hör auf böse zu sein'
quelle
Ich habe mich gefragt, ob die Verwendung von :wqOver einen Vorteil hat ZZ.
Voithos
Auch :xdas ist, was meine Finger automatisch tun
Glenn Jackman
und ZQist das gleiche wie:q!
Glenn Jackman
ZZ und: x schreiben nur, wenn etwas zu schreiben ist ...: w synchronisiert die Datei immer auf die Festplatte, unabhängig davon, ob sie benötigt wird. Ich benutze: xa, weil ich Tabs benutze.
Xenoterracide
1

Sie können Vim im Ex-Modus verwenden:

ex -sc '2,d|x' .hgignore
  1. 2, Wählen Sie die Zeilen 2 bis zum Ende

  2. d löschen

  3. x speichern und schließen

Steven Penny
quelle
0

Für die direkte Dateibearbeitung können Sie auch den von Jürgen Hötzel in der Umleitungsausgabe von sed 's / c / d /' myFile nach myFile gezeigten Trick zum Öffnen von Dateien verwenden .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed
dan55
quelle
2
Und kurz nachdem rm .hgignoreIhr Strom ausfällt, nehmen Sie Stunden harter Arbeit weg. Ok, das spielt keine Rolle .hgignore, aber warum sollten Sie etwas so Kompliziertes tun? Also meine Ablehnung: technisch korrekt, aber eine sehr schlechte Idee.
Gilles 'SO - hör auf böse zu sein'
@ Gilles, vielleicht keine so gute Idee, aber perl -igenau das macht zum Beispiel (für die Inplace-Bearbeitung), und ich wäre nicht überrascht, wenn einige Implementierungen dies auch sed -itun würden (obwohl die neueste Version von GNU dies sednicht zu tun scheint).
Stéphane Chazelas