Jak usunąć wiele nowych linii z EOF?

25

Mam pliki, które kończą się na jednej lub więcej linii i powinny kończyć się tylko na jednej linii. Jak mogę to zrobić za pomocą narzędzi Bash / Unix / GNU?

Przykład nieprawidłowego pliku:

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

Przykład poprawionego pliku:

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

Innymi słowy: powinna istnieć dokładnie jedna nowa linia między EOF a ostatnim nie-nowym znakiem pliku.

Wdrożenie referencyjne

Odczytaj zawartość pliku, odetnij jedną nową linię, aż na końcu nie będą już więcej dwóch nowych linii, zapisz ją ponownie:

#! /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)

Wyjaśnienie: Oczywiście, orurowanie jest dozwolone, jeśli jest to bardziej eleganckie.

Bengt
źródło

Odpowiedzi:

16
awk '/^$/ {nlstack=nlstack "\n";next;} {printf "%s",nlstack; nlstack=""; print;}' file
Hauke ​​Laging
źródło
2
+1: rozwiązania awk są (prawie) zawsze eleganckie i czytelne!
Olivier Dulac
@OlivierDulac Rzeczywiście. Kiedy zobaczyłem sedpropozycję, pomyślałem po prostu OMG ...
Hauke ​​Laging
1
to nie działa na OSX Mavericks przy użyciu najnowszego dostępnego awk od Homebrew. Błąd z awk: illegal statement. brew install mawki zmieniając polecenie na mawkdziałające.
tjmcewan
@noname Nawet nie rozumiem pytania ...
Hauke ​​Laging
Każdy awk, w którym skrypt nie działa, jest mocno zepsutym awk - przestań go używać i zdobądź nowy awk, ponieważ jeśli nie może tego zrobić, to kto wie, jakie inne awarie ma.
Ed Morton
21

Od przydatnych skryptów jednowierszowych dla sed .

# Delete all trailing blank lines at end of file (only).
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' file
Aleksiej Shmalko
źródło
4
Dzięki, wykorzystałem następujące polecenie, aby zrobić to w miejscu dla wielu plików: find . -type f -name '*.js' -exec sed --in-place -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
jakub.g
@ jakub.g w miejscu i rekurencyjny jest dokładnie tym, czego potrzebowałem. Dziękuję Ci.
Buttle Butkus,
Aby dodać do doskonałego komentarza z @ jakub.g, możesz wywołać następującą komendę w systemie OS X:find . -type f -name '*.js' -exec sed -i '' -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
davejagoda
18

Ponieważ masz już odpowiedzi za pomocą bardziej odpowiednich narzędzi sed i awk; możesz skorzystać z faktu, że $(< file)usuwa on końcowe puste linie.

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

Ten tani hack nie działałby w celu usunięcia końcowych pustych linii, które mogą zawierać spacje lub inne znaki niedrukowalne, a jedynie w celu usunięcia końcowych pustych linii. Nie zadziała również, jeśli plik zawiera null bajty.

W powłokach innych niż bash i zsh użyj $(cat file)zamiast $(<file).

llua
źródło
+1, aby wskazać, co dla mnie wygląda na błąd: $ (<plik) tak naprawdę nie czyta pliku? dlaczego odrzuca końcowe znaki nowej linii? (tak, właśnie przetestowałem, dziękuję za zwrócenie na to uwagi!)
Olivier Dulac
2
@OlivierDulac $()odrzuca końcowe znaki nowej linii. To decyzja projektowa. Zakładam, że ułatwi to integrację z innymi łańcuchami: echo "On $(date ...) we will meet."byłoby złe z nową linią, którą wypuszcza prawie każde polecenie powłoki na końcu.
Hauke ​​Laging
@HaukeLaging: Dobra uwaga, prawdopodobnie jest to źródło tego zachowania
Olivier Dulac
Dodałem szczególny przypadek, aby uniknąć dołączania „\ n” opróżnić pliki: [[ $a == '' ]] || printf '%s\n' "$a" >"$file".
davidchambers
Aby usunąć wiele nowych linii z początku pliku, włóż tac do procesu (używam gnu coreutils na Macu, więc gtac dla mnie):a=$(gtac file.txt); printf '%s\n' "$a" | gtac > file.txt
r_alex_hall
5

Możesz użyć tej sztuczki z cat& printf:

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

Na przykład

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

$Oznacza końca linii.

Referencje

slm
źródło
4

To pytanie jest oznaczone jako , ale nikt nie zaproponował edrozwiązania.

Tutaj jest jeden:

ed -s file <<'ED_END'
a

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

lub równoważnie

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

ed po uruchomieniu domyślnie umieści cię w ostatnim wierszu bufora edycji.

Pierwsze polecenie ( a) dodaje pustą linię na końcu bufora (pusta linia w skrypcie edycyjnym to ta linia, a kropka ( .) służy tylko do powrotu do trybu komend).

Drugie polecenie ( ?) wyszukuje najbliższy poprzedni wiersz, który zawiera coś (nawet znaki spacji), a następnie usuwa wszystko do końca bufora od następnego wiersza.

Trzecie polecenie ( w) zapisuje plik z powrotem na dysk.

Dodana pusta linia chroni resztę pliku przed usunięciem w przypadku, gdy na końcu oryginalnego pliku nie ma żadnych pustych linii.

Kusalananda
źródło
3

Oto rozwiązanie Perla, które nie wymaga odczytywania więcej niż jednej linii do pamięci na raz:

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

lub jako jedna linijka:

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

To czyta plik po linii na raz i sprawdza każdą linię, aby sprawdzić, czy zawiera znak inny niż nowy wiersz. Jeśli nie, zwiększa licznik; jeśli tak, drukuje liczbę nowych linii wskazanych przez licznik, a następnie samą linię, a następnie resetuje licznik.

Technicznie, nawet buforowanie pojedynczej linii w pamięci nie jest konieczne; możliwe byłoby rozwiązanie tego problemu przy użyciu stałej ilości pamięci przez odczytanie pliku we fragmentach o stałej długości i przetworzenie go znak po znaku za pomocą automatu stanów. Podejrzewam jednak, że byłoby to niepotrzebnie skomplikowane w typowym przypadku użycia.

Ilmari Karonen
źródło
1

Jeśli twój plik jest wystarczająco mały, aby zmieścić się w pamięci, możesz go użyć

perl -e 'local($/);$f=<>; $f=~s/\n*$/\n/;print $f;' file
terdon
źródło
0

W Pythonie (wiem, że nie jest to, czego chcesz, ale jest o wiele lepsze, ponieważ jest zoptymalizowany i stanowi preludium do wersji bash) bez przepisywania pliku i bez czytania całego pliku (co jest dobre, jeśli plik jest bardzo duży):

#!/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()

Zauważ, że nie działa na plikach, w których znak EOL nie jest „\ n”.

jfg956
źródło
0

Wersja bashowa, implementująca algorytm pythonowy, ale mniej wydajna, ponieważ wymaga wielu procesów:

#!/bin/bash
n=1
while test "$(tail -n $n "$1")" == ""; do
  ((n++))
done
((n--))
truncate -s $(($(stat -c "%s" "$1") - $n)) "$1"
jfg956
źródło
0

Ten jest szybki do pisania, a jeśli znasz sed, łatwy do zapamiętania:

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

Używa skryptu sed, aby usunąć wiodące puste wiersze z przydatnych skryptów jednowierszowych dla sed , do których odwołują się Alexey, powyżej i tac (reverse cat).

W szybkim teście na pliku o wielkości 18 MB i 64 000 linii podejście Aleksieja było szybsze (0,036 vs 0,046 sekundy).

freeB
źródło