Wie entferne ich mehrere Zeilenumbrüche bei EOF?

25

Ich habe Dateien, die in einer oder mehreren Zeilenumbrüchen enden und nur in einer Zeile enden sollten. Wie kann ich das mit Bash / Unix / GNU-Tools machen?

Beispiel für eine fehlerhafte Datei:

1\n
\n
2\n
\n
\n
3\n
\n
\n
\n

Beispiel korrigierte Datei:

1\n
\n
2\n
\n
\n
3\n

Mit anderen Worten: Zwischen dem EOF und dem letzten Nicht-Newline-Zeichen der Datei sollte genau eine Newline stehen.

Referenzimplementierung

Dateiinhalt lesen, eine neue Zeile abschneiden, bis keine neuen Zeilen mehr vorhanden sind, zurückschreiben:

#! /bin/python

import sys

with open(sys.argv[1]) as infile:
    lines = infile.read()

while lines.endswith("\n\n"):
    lines = lines[:-1]

with open(sys.argv[2], 'w') as outfile:
    for line in lines:
        outfile.write(line)

Klarstellung: Natürlich sind Rohrleitungen erlaubt, wenn diese eleganter sind.

Bengt
quelle

Antworten:

16
awk '/^$/ {nlstack=nlstack "\n";next;} {printf "%s",nlstack; nlstack=""; print;}' file
Hauke ​​Laging
quelle
2
+1: awks Lösungen sind (fast) immer elegant und lesbar!
Olivier Dulac
@OlivierDulac In der Tat. Als ich den sedVorschlag sah, dachte ich nur OMG ...
Hauke ​​Laging
1
Dies funktioniert unter OSX Mavericks mit der neuesten verfügbaren awk von Homebrew nicht. Es Fehler mit awk: illegal statement. brew install mawkund ändern Sie den Befehl auf mawkfunktioniert.
tjmcewan
@noname Ich verstehe nicht einmal die Frage ...
Hauke ​​Laging
Jede Awk, in der das Skript nicht funktioniert, ist eine schlecht kaputte Awk. Verwenden Sie sie nicht weiter und holen Sie sich eine neue Awk, denn wenn dies nicht möglich ist, wer weiß, welche anderen Schäden sie hat.
Ed Morton
21

Aus nützlichen einzeiligen Skripten für sed .

# Delete all trailing blank lines at end of file (only).
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' file
Alexey Shmalko
quelle
4
Danke, ich habe Folgendes verwendet, um es für mehrere Dateien zu tun: find . -type f -name '*.js' -exec sed --in-place -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
jakub.g
@ jakub.g vorhanden und rekursiv ist genau das, was ich brauchte. Danke dir.
Buttle Butkus
Um den hervorragenden Kommentar von @ jakub.g zu ergänzen, können Sie den Befehl unter OS X folgendermaßen find . -type f -name '*.js' -exec sed -i '' -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
aufrufen
17

Da hast du schon Antworten mit den passenderen Werkzeugen sed und awk; Sie können die Tatsache ausnutzen, dass $(< file)nachgestellte Leerzeilen entfernt werden.

a=$(<file); printf '%s\n' "$a" > file

Dieser billige Hack würde nicht funktionieren, um nachgestellte Leerzeilen zu entfernen, die Leerzeichen oder andere nicht druckbare Zeichen enthalten könnten, sondern nur, um nachgestellte Leerzeilen zu entfernen. Es funktioniert auch nicht, wenn die Datei null Bytes enthält.

Verwenden Sie in anderen Shells als bash und zsh $(cat file)anstelle von $(<file).

llua
quelle
+1, um darauf hinzuweisen, wie ein Fehler für mich aussieht: $ (<Datei) liest die Datei nicht wirklich? Warum werden nachfolgende Zeilenumbrüche verworfen? (es funktioniert, ich habe gerade getestet, danke für den Hinweis!)
Olivier Dulac
2
@OlivierDulac $()verwirft nachfolgende Zeilenumbrüche. Das ist eine Designentscheidung. Ich echo "On $(date ...) we will meet."gehe davon aus, dass dies die Integration in andere Strings erleichtern wird: wäre böse mit der Newline, die fast jeder Shell-Befehl am Ende ausgibt.
Hauke ​​Laging
@HaukeLaging: Guter Punkt, es ist wahrscheinlich die Quelle dieses Verhaltens
Olivier Dulac
Ich habe einen Sonderfall „\ n“ zu vermeiden Anhängen von Dateien zu leeren: [[ $a == '' ]] || printf '%s\n' "$a" >"$file".
Davidchambers
Um mehrere Zeilenumbrüche am Anfang einer Datei zu entfernen, fügen Sie tac in den Prozess ein (ich verwende gnu coreutils auf dem Mac, also gtac für mich):a=$(gtac file.txt); printf '%s\n' "$a" | gtac > file.txt
r_alex_hall
5

Sie können diesen Trick mit cat& verwenden printf:

$ printf '%s\n' "`cat file`"

Beispielsweise

$ printf '%s\n' "`cat ifile`" > ofile
$ cat -e ofile
1$
$
2$
$
$
3$

Das $kennzeichnet das Ende einer Zeile.

Verweise

slm
quelle
4

Diese Frage ist mit markiert , aber niemand hat eine edLösung vorgeschlagen .

Hier ist eine:

ed -s file <<'ED_END'
a

.
?^..*?+1,.d
w
ED_END

oder äquivalent,

printf '%s\n' a '' . '?^..*?+1,.d' w | ed -s file

ed platziert Sie beim Start standardmäßig in der letzten Zeile des Bearbeitungspuffers.

Der erste Befehl ( a) fügt eine leere Zeile an das Ende des Puffers an (die leere Zeile im Bearbeitungsskript ist diese Zeile, und der Punkt ( .) dient nur zum Zurückkehren in den Befehlsmodus).

Der zweite Befehl ( ?) sucht nach der nächsten vorherigen Zeile, die etwas enthält (sogar Leerzeichen), und löscht dann ab der nächsten Zeile alles bis zum Ende des Puffers.

Der dritte Befehl ( w) schreibt die Datei zurück auf die Festplatte.

Die hinzugefügte Leerzeile schützt den Rest der Datei vor dem Löschen, falls am Ende der Originaldatei keine Leerzeilen vorhanden sind.

Kusalananda
quelle
3

Hier ist eine Perl-Lösung, bei der nicht mehr als eine Zeile gleichzeitig in den Speicher eingelesen werden muss:

my $n = 0;
while (<>) {
    if (/./) {
        print "\n" x $n, $_;
        $n = 0;
    } else {
        $n++;
    }
}

oder als Einzeiler:

perl -ne 'if (/./) { print "\n" x $n, $_; $n = 0 } else { $n++ }'

Dadurch wird die Datei zeilenweise gelesen und jede Zeile daraufhin überprüft, ob sie ein Nicht-Zeilenumbruchzeichen enthält. Wenn dies nicht der Fall ist, wird ein Zähler inkrementiert. In diesem Fall wird die Anzahl der vom Zähler angegebenen Zeilenumbrüche gefolgt von der Zeile selbst gedruckt und der Zähler anschließend zurückgesetzt.

Selbst das Puffern einer einzelnen Zeile im Speicher ist technisch nicht erforderlich. Es wäre möglich, dieses Problem unter Verwendung einer konstanten Speichermenge zu lösen, indem die Datei in Blöcken fester Länge gelesen und zeichenweise unter Verwendung einer Zustandsmaschine verarbeitet wird. Ich vermute jedoch, dass dies für den typischen Anwendungsfall unnötig kompliziert wäre.

Ilmari Karonen
quelle
1

Wenn Ihre Datei klein genug ist, um in den Speicher zu gelangen, können Sie dies verwenden

perl -e 'local($/);$f=<>; $f=~s/\n*$/\n/;print $f;' file
terdon
quelle
0

In Python (ich weiß, es ist nicht das, was Sie wollen, aber es ist viel besser, da es optimiert ist und ein Vorspiel für die Bash-Version), ohne die Datei neu zu schreiben und ohne die gesamte Datei zu lesen (was eine gute Sache ist, wenn die Datei ist sehr groß):

#!/bin/python
import sys
infile = open(sys.argv[1], 'r+')
infile.seek(-1, 2)
while infile.read(1) == '\n':
  infile.seek(-2, 1)
infile.seek(1, 1)
infile.truncate()
infile.close()

Beachten Sie, dass es nicht für Dateien funktioniert, bei denen das EOL-Zeichen nicht '\ n' ist.

jfg956
quelle
0

Eine Bash-Version, die den Python-Algorithmus implementiert, aber weniger effizient ist, da sie viele Prozesse benötigt:

#!/bin/bash
n=1
while test "$(tail -n $n "$1")" == ""; do
  ((n++))
done
((n--))
truncate -s $(($(stat -c "%s" "$1") - $n)) "$1"
jfg956
quelle
0

Dieser ist schnell zu tippen und, wenn Sie sed kennen, leicht zu merken:

tac < file | sed '/[^[:blank:]]/,$!d' | tac

Mit dem sed-Skript werden führende Leerzeilen aus nützlichen einzeiligen Skripten für sed , auf die Alexey oben verweist, und tac (reverse cat) gelöscht .

In einem Schnelltest mit einer Datei mit 18 MB und 64.000 Zeilen war Alexeys Ansatz schneller (0,036 gegenüber 0,046 Sekunden).

freeB
quelle