Wie kann ich eine Datei in einem Befehl verwenden und die Ausgabe in dieselbe Datei umleiten, ohne sie abzuschneiden?

95

Grundsätzlich möchte ich als Eingabetext aus einer Datei nehmen, eine Zeile aus dieser Datei entfernen und die Ausgabe an dieselbe Datei zurücksenden. Etwas in diese Richtung, wenn das klarer wird.

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

Wenn ich dies tue, erhalte ich jedoch eine leere Datei. Irgendwelche Gedanken?

Mike
quelle

Antworten:

84

Sie können dies nicht tun, da bash zuerst die Umleitungen verarbeitet und dann den Befehl ausführt. Wenn grep also auf Dateiname schaut, ist es bereits leer. Sie können jedoch eine temporäre Datei verwenden.

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

Ziehen Sie in Betracht mktemp, das tmpfile zu verwenden , beachten Sie jedoch, dass es sich nicht um POSIX handelt.

c00kiemon5ter
quelle
47
Der Grund, warum Sie das nicht tun können: bash verarbeitet zuerst die Umleitungen und führt dann den Befehl aus. Wenn grep also auf Dateiname schaut, ist es bereits leer.
Glenn Jackman
1
@glennjackman: Mit "Prozessumleitung" meinen Sie, dass im Fall von> die Datei geöffnet und gelöscht wird und im Fall von >> nur die Datei geöffnet wird "?
Razvan
2
Ja, aber in dieser Situation >wird die Datei durch die Umleitung geöffnet und abgeschnitten, bevor die Shell gestartet wird grep.
Glenn Jackman
1
Siehe meine Antwort, wenn Sie keine temporäre Datei verwenden möchten, aber diesen Kommentar nicht positiv bewerten.
Zack Morris
Stattdessen sollte die Antwort mit dem spongeBefehl akzeptiert werden.
vlz
95

Verwenden Sie für diese Art von Aufgaben einen Schwamm . Sein Teil von moreutils.

Versuchen Sie diesen Befehl:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name
Lynchen
quelle
4
Danke für die Antwort. Als möglicherweise hilfreiche Ergänzung, wenn Sie Homebrew auf Mac verwenden, kann verwenden brew install moreutils.
Anthony Panozzo
2
Oder sudo apt-get install moreutilsauf Debian-basierten Systemen.
Jonah
3
Verdammt! Vielen Dank, dass Sie mir moreutils vorgestellt haben =) einige nette Programme dort!
Netigger
Vielen Dank, moreutils für die Rettung! Schwamm wie ein Chef!
Aqquadro
3
Achtung, "Schwamm" ist destruktiv. Wenn Sie also einen Fehler in Ihrem Befehl haben, können Sie Ihre Eingabedatei löschen (wie ich es beim ersten Versuch mit Schwamm getan habe). Stellen Sie sicher, dass Ihr Befehl funktioniert und / oder die Eingabedatei unter Versionskontrolle steht, wenn Sie versuchen, den Befehl iterieren zu lassen.
user107172
18

Verwenden Sie stattdessen sed:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name
Manny D.
quelle
1
iirc -iist nur eine Erweiterung von GNU.
c00kiemon5ter
3
Unter * BSD (und damit auch unter OSX) können Sie sagen, -i ''dass die Erweiterung nicht unbedingt vorgeschrieben ist, die -iOption jedoch einige Argumente erfordert .
Tripleee
13

versuchen Sie dieses einfache

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Ihre Datei wird diesmal nicht leer sein :) und Ihre Ausgabe wird auch auf Ihrem Terminal gedruckt.

Sailesh Ramanam
quelle
1
Ich mag diese Lösung! Und wenn Sie nicht möchten, dass es im Terminal gedruckt wird, können Sie die Ausgabe trotzdem an /dev/nulloder ähnliche Stellen umleiten .
Frozn
4
Dadurch wird auch hier der Dateiinhalt gelöscht. Liegt das an einem Unterschied zwischen GNU und BSD? Ich bin auf macOS ...
ssc
7

Sie können den Umleitungsoperator ( >oder >>) nicht für dieselbe Datei verwenden, da dieser eine höhere Priorität hat und die Datei erstellt / abgeschnitten wird, bevor der Befehl überhaupt aufgerufen wird. Um zu vermeiden , dass, sollten Sie geeignete Tools wie tee, sponge, sed -ioder ein anderes Werkzeug , welche Ergebnisse in die Datei schreiben kann (zBsort file -o file ).

Grundsätzlich ist es nicht sinnvoll, Eingaben in dieselbe Originaldatei umzuleiten, und Sie sollten dafür geeignete In-Place-Editoren verwenden, z. B. den Ex-Editor (Teil von Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

wo:

  • '+cmd'/ -c- Führen Sie einen beliebigen Ex / Vim-Befehl aus
  • g/pattern/d- Entfernen Sie Linien, die einem Muster entsprechen, mit global ( help :g)
  • -s- stiller Modus ( man ex)
  • -c wq- ausführen :writeund :quitBefehle

Sie können verwendet werden, seddas gleiche zu erreichen (wie bereits in anderen Antworten gezeigt), aber an Ort und Stelle ( -i) ist Nicht-Standard - FreeBSD - Erweiterung (unterschiedlich zwischen Unix / Linux arbeiten) und im Grunde ist es eine s tream ed itor, keine Datei - Editor . Siehe: Hat der Ex-Modus einen praktischen Nutzen?

Kenorb
quelle
6

Eine Liner-Alternative - Legen Sie den Inhalt der Datei als Variable fest:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name
w00t
quelle
4

Da diese Frage das Top-Ergebnis in Suchmaschinen ist, ist hier ein Einzeiler basierend auf https://serverfault.com/a/547331 , der anstelle einer Subshell verwendet sponge(was häufig nicht Teil einer Vanilla-Installation wie OS X ist). ::

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

Der allgemeine Fall ist:

echo "$(cat file_name)" > file_name

Bearbeiten, die obige Lösung hat einige Einschränkungen:

  • printf '%s' <string>sollte stattdessen verwendet werden, echo <string>damit Dateien enthalten-n , kein unerwünschtes Verhalten verursachen.
  • Befehlsersetzungsstreifen werden nach Zeilenumbrüchen entfernt ( dies ist ein Fehler / eine Funktion von Shells wie bash ), daher sollten wir ein Postfix-Zeichen wie xan die Ausgabe anhängen und es außen durch Parametererweiterung einer temporären Variablen wie entfernen ${v%x}.
  • Durch die Verwendung einer temporären Variablen wird $vder Wert einer vorhandenen Variablen $vin der aktuellen Shell-Umgebung gestampft. Daher sollten wir den gesamten Ausdruck in Klammern verschachteln, um den vorherigen Wert beizubehalten.
  • Ein weiterer Fehler / Merkmal von Shells wie bash ist, dass durch das Ersetzen von Befehlen nicht druckbare Zeichen wie nullaus der Ausgabe entfernt werden. Ich habe dies überprüft, indem ich es aufgerufen dd if=/dev/zero bs=1 count=1 >> file_nameund hexadezimal mit angezeigt habe cat file_name | xxd -p. Ist echo $(cat file_name) | xxd -paber ausgezogen. Daher sollte diese Antwort nicht für Binärdateien oder andere Dateien mit nicht druckbaren Zeichen verwendet werden, wie Lynch betonte .

Die allgemeine Lösung (albiet etwas langsamer, speicherintensiver und immer noch nicht druckbare Zeichen entfernt) lautet:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

Test von https://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Sollte drucken:

hello
world

Während Sie cat file_uniquely_named.txt > file_uniquely_named.txtdie aktuelle Shell aufrufen :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

Druckt eine leere Zeichenfolge.

Ich habe dies nicht an großen Dateien getestet (wahrscheinlich über 2 oder 4 GB).

Ich habe diese Antwort von Hart Simha und kos ausgeliehen .

Zack Morris
quelle
2
Natürlich funktioniert es nicht mit großen Dateien. Dies kann unmöglich eine gute Lösung sein oder die ganze Zeit funktionieren. Was passiert ist, dass bash zuerst den Befehl ausführt und dann das stdout von lädt catund es als erstes Argument an setzt echo. Natürlich werden nicht druckbare Variablen nicht richtig ausgegeben und die Daten werden beschädigt. Versuchen Sie nicht, eine Datei zurück zu sich selbst zu leiten, es kann einfach nicht gut sein.
Lynch
1

Es gibt auch ed(als Alternative zu sed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name
Nerx
quelle
1

Sie können dies durch Prozessersetzung tun .

Es ist allerdings ein bisschen sleephackig, da Bash alle Pipes asynchron öffnet und wir das mit so YMMV umgehen müssen.

In Ihrem Beispiel:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) Erstellt eine temporäre Datei, die die Ausgabe von grep empfängt
  • sleep 1 Verzögerungen für eine Sekunde, um grep Zeit zum Parsen der Eingabedatei zu geben
  • cat > file_nameschreibt schließlich die Ausgabe
Laktak
quelle
1

Sie können Slurp mit POSIX Awk verwenden:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

Beispiel

Steven Penny
quelle
1
Es sollte vielleicht darauf hingewiesen werden, dass "schlürfen" "die gesamte Datei in den Speicher lesen" bedeutet. Wenn Sie eine große Eingabedatei haben, möchten Sie dies möglicherweise vermeiden.
Tripleee
0

Versuche dies

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC
Виктор Пупкин
quelle
Eine kurze Erklärung oder sogar Kommentare können hilfreich sein.
Rich
Ich denke, es funktioniert, weil String-Extrapolation vor dem Umleitungsoperator ausgeführt wird, aber ich weiß nicht genau
Виктор Пупкин
0

Mit den folgenden Funktionen wird das Gleiche spongeerreicht, ohne dass dies erforderlich ist moreutils:

    shuf --output=file --random-source=/dev/zero 

Der --random-source=/dev/zeroTeil Tricksshuf versucht, seine Sache zu erledigen, ohne überhaupt zu mischen, sodass er Ihre Eingabe puffert, ohne sie zu ändern.

Es ist jedoch richtig, dass die Verwendung einer temporären Datei aus Leistungsgründen am besten ist. Hier ist eine Funktion, die ich geschrieben habe und die dies allgemein für Sie erledigt:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}
Mike Nakis
quelle
0

Dies ist sehr gut möglich. Sie müssen lediglich sicherstellen, dass Sie die Ausgabe zum Zeitpunkt des Schreibens in eine andere Datei schreiben. Dies kann erreicht werden, indem die Datei nach dem Öffnen eines Dateideskriptors entfernt wird, aber bevor Sie darauf schreiben:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

Oder Zeile für Zeile, um es besser zu verstehen:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

Es ist immer noch eine riskante Sache, denn wenn COMMAND nicht richtig ausgeführt wird, verlieren Sie den Dateiinhalt. Dies kann durch Wiederherstellen der Datei verringert werden, wenn COMMAND einen Exit-Code ungleich Null zurückgibt:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

Wir können auch eine Shell-Funktion definieren, um die Verwendung zu vereinfachen:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

Beispiel:

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

Beachten Sie außerdem, dass dadurch eine vollständige Kopie der Originaldatei erhalten bleibt (bis der dritte Dateideskriptor geschlossen wird). Wenn Sie Linux verwenden und die Datei, die Sie verarbeiten, zu groß ist, um zweimal auf die Festplatte zu passen, können Sie dieses Skript auschecken, mit dem die Datei blockweise an den angegebenen Befehl weitergeleitet wird, während die Zuordnung der bereits verarbeiteten Datei aufgehoben wird Blöcke. Lesen Sie wie immer die Warnungen auf der Verwendungsseite.

Pistache
quelle
-2

Normalerweise benutze ich dazu das Tee- Programm:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

Es erstellt und entfernt selbst ein Tempfile.

Carlos Fanelli
quelle
Entschuldigung, es teewird nicht garantiert, dass es funktioniert. Siehe askubuntu.com/a/752451/335781 .
Studgeek