Linux: Wie wird eine Datei gleichzeitig als Eingabe und Ausgabe verwendet?

55

Ich habe gerade Folgendes in bash ausgeführt:

uniq .bash_history > .bash_history

und meine Verlaufsdatei endete völlig leer.

Ich denke, ich brauche eine Möglichkeit, die gesamte Datei zu lesen, bevor ich darauf schreibe. Wie geht das?

PS: Ich habe natürlich darüber nachgedacht, eine temporäre Datei zu verwenden, aber ich suche nach einer eleganteren Lösung.

MilliaLover
quelle
Das liegt daran, dass die Dateien von rechts nach links geöffnet werden. Siehe auch stackoverflow.com/questions/146435/…
WheresAlice
Sie müssen die Ausgabe in eine neue Datei im selben Verzeichnis schreiben und diese über der alten Datei umbenennen. Bei jedem anderen Ansatz besteht die Gefahr, dass Ihre Daten verloren gehen, wenn sie zur Hälfte unterbrochen werden. Einige Tools verbergen diesen Schritt möglicherweise vor Ihnen.
Kasperd
bashWenn Sie HISTCONTROL so einstellen, dass ignorierte Ups eingeschlossen werden , werden keine aufeinanderfolgenden Dupes in den Verlauf aufgenommen. Siehe die Manpage.
Dave_thompson_085

Antworten:

49

Ich empfehle die Verwendung spongevon moreutils . Aus der Manpage:

DESCRIPTION
  sponge  reads  standard  input  and writes it out to the specified file. Unlike
  a shell redirect, sponge soaks up all its input before opening the output file.
  This allows for constructing pipelines that read from and write to the same 
  file.

Um dies auf Ihr Problem anzuwenden, versuchen Sie:

uniq .bash_history | sponge .bash_history
Jldugger
quelle
6
Es ist wie eine Katze, aber mit
Saugfähigkeiten
77

Ich wollte nur eine andere Antwort beisteuern, die einfach ist und keinen Schwamm verwendet (da sie in leichten Umgebungen oft nicht enthalten ist).

echo "$(uniq .bash_history)" > .bash_history

sollte das gewünschte Ergebnis haben. Die Subshell wird ausgeführt, bevor .bash_history zum Schreiben geöffnet wird. Wie in der Antwort von Phil P erläutert, wurde .bash_history im Originalbefehl bereits vom Operator '>' abgeschnitten.

Hart Simha
quelle
15
Normalerweise bin ich kein Fan von Antworten, die eine alte Frage aufgreifen, die bereits eine gültige, akzeptierte Antwort hat - aber diese ist elegant, gut geschrieben und spricht stark für ihre Notwendigkeit (leichte Umgebungen). Für mich erweitert es die bestehenden Antworten um einiges. Willkommen bei SF, Hart (Sie sind seit einem Monat hier, aber ich denke, dies ist Ihre erste inhaltliche Veröffentlichung). Ich hoffe, mehr Antworten von Ihnen wie diese zu lesen!
MadHatter
4
Dies ist die beste Lösung. Ich musste $()wegen einiger Probleme eine Subshell anstelle von Backticks verwenden.
CMCDragonkai
3
Ich frage mich, ob diese Lösung auf große Dateien skaliert, z. B. 20 oder 50 GB.
Amit Naidu
1
Dies sollte wirklich die akzeptierte Antwort sein.
Maxywb
1
Ich habe diese Antwort echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txtheute benutzt. Ich habe lange nach einer eleganten Lösung gesucht. Vielen Dank, @Hart Simha!
Shredalert
12

Das Problem ist, dass Ihre Shell die Befehlspipeline einrichtet, bevor Sie die Befehle ausführen. Es geht nicht um "Ein- und Ausgabe", sondern darum, dass der Inhalt der Datei bereits gelöscht ist, bevor uniq überhaupt ausgeführt wird. Es geht so etwas wie:

  1. Die Shell öffnet die >Ausgabedatei zum Schreiben und schneidet sie ab
  2. Die Shell konfiguriert, dass für diese Ausgabe der Dateideskriptor 1 (für stdout) verwendet wird
  3. Die Shell führt uniq aus, vielleicht so etwas wie execlp ("uniq", "uniq", ".bash_history", NULL)
  4. uniq läuft, öffnet .bash_history und findet dort nichts

Es gibt verschiedene Lösungen, einschließlich der In-Place-Bearbeitung und der Verwendung temporärer Dateien, die von anderen erwähnt werden. Der Schlüssel ist jedoch, das Problem zu verstehen, was tatsächlich schief geht und warum.

Phil P
quelle
9

Ein weiterer Trick, der ohne Verwendung von ausgeführt werden kann sponge, ist der folgende Befehl:

{ rm .bash_history && uniq > .bash_history; } < .bash_history

Dies ist einer der Cheats, die im hervorragenden Artikel "In-Place" -Bearbeitung von Dateien auf backreference.org beschrieben sind.

Es öffnet im Grunde die Datei zum Lesen und "entfernt" sie dann. Es wird jedoch nicht wirklich entfernt: Es gibt einen offenen Dateideskriptor, der darauf verweist, und solange dieser offen bleibt, ist die Datei noch vorhanden. Dann erstellt es eine neue Datei mit demselben Namen und schreibt die eindeutigen Zeilen hinein.

Nachteil dieser Lösung: Wenn uniqaus irgendeinem Grund ein Fehler auftritt, ist Ihr Verlauf nicht mehr vorhanden.

scy
quelle
6

Verwenden Sie einen Schwamm von moreutils

uniq .bash_history | sponge .bash_history
Justin
quelle
3

Dieses sedSkript entfernt benachbarte Duplikate. Mit dieser -iOption wird die Änderung direkt vorgenommen. Es ist aus der sed infoDatei:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history
Dennis Williamson
quelle
sed verwendet immer noch die temporäre Datei, fügte eine Antwort mit straceIllustration hinzu (nicht, dass es wirklich wichtig ist) :-)
Kyle Brandt
3
@Kyle: Stimmt, aber "außer Sicht, außer Verstand". Persönlich würde ich die explizite temporäre Datei verwenden, da so etwas process input > tmp && mv tmp inputviel einfacher und lesbarer ist als das Verwenden von sedTricks, nur um eine temporäre Datei zu vermeiden, und mein Original wird nicht überschrieben, wenn es fehlschlägt (ich weiß nicht, ob sed -ies ordnungsgemäß fehlschlägt - ich würde es tun) denke, es würde aber). Außerdem gibt es eine Menge Dinge, die Sie mit der Ausgabe-in-Temp-Datei-Methode tun können, die nicht ohne etwas noch Komplizierteres als dieses sedSkript an Ort und Stelle durchgeführt werden können. Ich weiß, dass Sie das alles wissen, aber es kann einigen Zuschauern nützen.
Dennis Williamson
3

Als interessanten Leckerbissen verwendet sed auch eine temporäre Datei (dies erledigt es nur für Sie):

$ strace sed -i 's/foo/bar/g' foo    
open("foo", O_RDONLY|O_LARGEFILE)       = 3
...
open("./sedPmPv9z", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = 4
...
read(3, "foo\n"..., 4096)               = 4
write(4, "bar\n"..., 4)                 = 4
read(3, ""..., 4096)                    = 0
close(3)                                = 0
close(4)                                = 0
rename("./sedPmPv9z", "foo")            = 0
close(1)                                = 0
close(2)                                = 0

Beschreibung:
Die Datei tempfile ./sedPmPv9zwird zu fd 4 und die fooDateien werden zu fd 3. Die Leseoperationen werden auf fd 3 ausgeführt und die Schreiboperationen auf fd 4 (die temporäre Datei). Die foo-Datei wird dann beim Umbenennungsaufruf mit der temporären Datei überschrieben.

Kyle Brandt
quelle
1

Eine andere Lösung:

uniq file 1<> líneas.txt
Antonio Lebrón
quelle
0

Eine temporäre Datei ist so ziemlich alles, es sei denn, der betreffende Befehl unterstützt die direkte Bearbeitung ( uniqnicht - manche sedtun dies ( sed -i)).

Douglas Leeder
quelle
0

Sie können Vim im Ex-Modus verwenden:

ex -sc '%!uniq' -cx .bash_history
  1. % Wählen Sie alle Zeilen aus

  2. ! Führen Sie den Befehl aus

  3. x speichern und schließen

Steven Penny
quelle
-1

Sie können auch tee verwenden und die Ausgabe von uniq als Eingabe verwenden:

uniq .bash_history | tee .bash_history
Saul Vargas
quelle
Nein, das kannst du nicht. Askubuntu.com/a/752451
Steven Penny