Jak mogę usunąć nową linię, jeśli jest to ostatni znak w pliku?

162

Mam kilka plików, z których chciałbym usunąć ostatni znak nowej linii, jeśli jest to ostatni znak w pliku. od -cpokazuje mi, że polecenie, które wykonuję, zapisuje plik z końcową nową linią:

0013600   n   t  >  \n

Wypróbowałem kilka sztuczek z sedem, ale najlepsze, o czym przyszło mi do głowy, nie działa:

sed -e '$s/\(.*\)\n$/\1/' abc

Jakieś pomysły, jak to zrobić?

Todd Partridge „Gen2ly”
źródło
4
znak nowej linii to tylko jeden znak dla znaków nowej linii w systemie Unix. Nowe linie DOS to dwa znaki. Oczywiście dosłowne „\ n” to dwa znaki. Czego tak naprawdę szukasz?
Wstrzymano do odwołania.
3
Chociaż reprezentacja może być \n, w linuksie jest jedna postać
pavium
10
Czy możesz wyjaśnić, dlaczego chcesz to zrobić? Pliki tekstowe są niby do końca z końcem-of-line, chyba że są one całkowicie pusty. Wydaje mi się dziwne, że chciałbyś mieć taki obcięty plik?
Thomas Padron-McCarthy
Typowym powodem zrobienia czegoś takiego jest usunięcie końcowego przecinka z ostatniej linii pliku CSV. Sed działa dobrze, ale nowe linie muszą być traktowane inaczej.
pavium
9
@ ThomasPadron-McCarthy "W informatyce, z każdego dobrego powodu, aby coś zrobić, istnieje dobry powód, aby tego nie robić i na odwrót." -Jezus - „nie powinieneś tego robić” to okropna odpowiedź bez względu na pytanie. Prawidłowy format to: [jak to zrobić], ale [dlaczego może to być zły pomysł]. #sacrilege
Cory Mawhorter

Odpowiedzi:

223
perl -pe 'chomp if eof' filename >filename2

lub, aby edytować plik w miejscu:

perl -pi -e 'chomp if eof' filename

[Od redakcji: -pi -ebyło pierwotnie -pie, ale, jak zauważyło kilku komentatorów i wyjaśniło @hvd, to drugie nie działa.]

Na stronie awk, którą widziałem, zostało to opisane jako „bluźnierstwo perla”.

Ale w teście zadziałało.

pavium
źródło
11
Możesz uczynić to bezpieczniejszym, używając chomp. I to jest lepsze niż siorbanie pliku.
Sinan Ünür
6
Chociaż to bluźnierstwo, działa bardzo dobrze. perl -i -pe 'chomp if eof' filename. Dziękuję Ci.
Todd Partridge „Gen2ly”
13
Zabawną rzeczą w bluźnierstwie i herezji jest to, że zwykle jest to znienawidzone, ponieważ jest poprawne. :)
Ether
8
Mała poprawka: możesz użyć perl -pi -e 'chomp if eof' filename, do edycji pliku na miejscu zamiast tworzenia pliku tymczasowego
Romuald Brunet
7
perl -pie 'chomp if eof' filename-> Nie można otworzyć skryptu perla "chomp if eof": Brak takiego pliku lub katalogu; perl -pi -e 'chomp if eof' filename-> działa
aditsu zrezygnowało, ponieważ SE jest ZŁEM
56

Możesz wykorzystać fakt, że podstawienia poleceń powłoki usuwają końcowe znaki nowej linii :

Prosta forma, która działa w bash, ksh, zsh:

printf %s "$(< in.txt)" > out.txt

Przenośna (zgodna z POSIX) alternatywa (nieco mniej wydajna):

printf %s "$(cat in.txt)" > out.txt

Uwaga:

  • Jeśli in.txtkończy się wieloma znakami nowej linii, podstawienie polecenia usuwa je wszystkie - dzięki, @Sparhawk. (Nie usuwa białych znaków innych niż końcowe znaki nowej linii).
  • Ponieważ takie podejście odczytuje cały plik wejściowy do pamięci , jest zalecane tylko w przypadku mniejszych plików.
  • printf %szapewnia, że ​​do danych wyjściowych nie zostanie dołączony znak nowej linii (jest to zgodna z POSIX alternatywa dla niestandardowego echo -n; patrz http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html i https: //unix.stackexchange. pl / a / 65819 )

Podręcznik do innych odpowiedzi :

  • Jeśli Perl jest dostępny, wybierz zaakceptowaną odpowiedź - jest prosta i wydajna w pamięci (nie czyta od razu całego pliku wejściowego).

  • W przeciwnym razie rozważ odpowiedź Awk ghostdog74 - jest niejasna, ale także wydajna pod względem pamięci ; równoważne bardziej czytelne (POSIX) stanowi:

    • awk 'NR > 1 { print prev } { prev=$0 } END { ORS=""; print }' in.txt
    • Drukowanie jest opóźnione o jedną linię, dzięki czemu ostatnia linia może być obsłużona w ENDbloku, gdzie jest drukowana bez \nkońca z powodu ustawienia separatora rekordów wyjściowych ( OFS) na pusty łańcuch.
  • Jeśli chcesz uzyskać szczegółowe, ale szybkie i niezawodne rozwiązanie, które naprawdę edytuje w miejscu (w przeciwieństwie do tworzenia pliku tymczasowego, który następnie zastępuje oryginał), rozważ skrypt Perl jrockway .

mklement0
źródło
3
Uwaga: jeśli na końcu pliku znajduje się wiele znaków nowej linii, to polecenie usunie je wszystkie.
Sparhawk
47

Możesz to zrobić z headGNU coreutils, obsługuje argumenty, które są względne do końca pliku. Aby więc zostawić ostatnie użycie bajtu:

head -c -1

Aby przetestować końcową linię nowej linii, możesz użyć taili wc. Poniższy przykład zapisuje wynik w pliku tymczasowym, a następnie zastępuje oryginał:

if [[ $(tail -c1 file | wc -l) == 1 ]]; then
  head -c -1 file > file.tmp
  mv file.tmp file
fi

Możesz również użyć spongefrom, moreutilsaby przeprowadzić edycję „lokalną”:

[[ $(tail -c1 file | wc -l) == 1 ]] && head -c -1 file | sponge file

Możesz również utworzyć ogólną funkcję wielokrotnego użytku, umieszczając ją w swoim .bashrcpliku:

# Example:  remove-last-newline < multiline.txt
function remove-last-newline(){
    local file=$(mktemp)
    cat > $file
    if [[ $(tail -c1 $file | wc -l) == 1 ]]; then
        head -c -1 $file > $file.tmp
        mv $file.tmp $file
    fi
    cat $file
}

Aktualizacja

Jak zauważył KarlWilbur w komentarzach i użyty w odpowiedzi Sorentara , truncate --size=-1może zastępować head -c-1i wspierać edycję w miejscu.

Thor
źródło
3
Jak dotąd najlepsze rozwiązanie. Używa standardowego narzędzia, które ma naprawdę każda dystrybucja Linuksa, i jest zwięzłe i przejrzyste, bez żadnej kreacji seda lub perla.
Dakkaron
2
Niezłe rozwiązanie. Jedną ze zmian jest to, że myślę, że użyłbym truncate --size=-1zamiast tego, head -c -1ponieważ po prostu zmienia rozmiar pliku wejściowego, zamiast czytać plik wejściowy, zapisując go do innego pliku, a następnie zastępując oryginał plikiem wyjściowym.
Karl Wilbur,
1
Zauważ, że head -c -1usunie ostatni znak, niezależnie od tego, czy jest to nowa linia, czy nie, dlatego musisz sprawdzić, czy ostatni znak jest nową linią, zanim go usuniesz.
wisbucky
Niestety nie działa na Macu. Podejrzewam, że nie działa na żadnym wariancie BSD.
Edward Falk,
16
head -n -1 abc > newfile
tail -n 1 abc | tr -d '\n' >> newfile

Edycja 2:

Oto awkwersja (poprawiona) , która nie gromadzi potencjalnie ogromnej tablicy:

awk '{if (line) print line; line = $ 0} END {printf $ 0} 'abc

Wstrzymano do odwołania.
źródło
Dobry oryginalny sposób, aby o tym pomyśleć. Dzięki, Dennis.
Todd Partridge „Gen2ly”
Masz rację. Zdaję się na twoją awkwersję. Bierze dwa przesunięcia (i inny test), a ja użyłem tylko jednego. Możesz jednak użyć printfzamiast ORS.
Wstrzymano do odwołania.
możesz zrobić z wyjścia potok z podstawieniem procesu:head -n -1 abc | cat <(tail -n 1 abc | tr -d '\n') | ...
BCoates
2
Używanie -c zamiast -n dla głowy i ogona powinno być jeszcze szybsze.
rudimeier
1
Dla mnie nagłówek -n -1 abc usunął ostatnią rzeczywistą linię pliku, pozostawiając końcowy znak nowej linii; head -c -1 abc wydawało się działać lepiej
ChrisV
10

gapić się

   awk '{q=p;p=$0}NR>1{print q}END{ORS = ""; print p}' file
ghostdog74
źródło
Wciąż wygląda na to, że wiele postaci jest dla mnie ... powoli się tego uczy :). Ale spełnia swoje zadanie. Dzięki ghostdog.
Todd Partridge 'Gen2ly'
1
awk '{ prev_line = line; line = $0; } NR > 1 { print prev_line; } END { ORS = ""; print line; }' filepowinno być łatwiejsze do odczytania.
Yevhen Pavliuk
Jak o: awk 'NR>1 {print p} {p=$0} END {printf $0}' file.
Izaak
@sorontar Pierwszy argument do printfto argument formatu . Tak więc, jeśli plik wejściowy zawiera coś, co można zinterpretować jako specyfikator formatu, na przykład %d, pojawi się błąd. Rozwiązaniem byłoby zmienić to naprintf "%s" $0
Robin A. Meade,
9

Bardzo prosta metoda dla plików jednowierszowych, wymagająca echa GNU z coreutils:

/bin/echo -n $(cat $file)
inny
źródło
To przyzwoity sposób, jeśli nie jest zbyt drogi (powtarzalny).
To ma problemy, gdy \njest obecny. Gdy zostanie przekonwertowany na nową linię.
Chris Stryczynski 19.07.17
Wydaje się, że działa również dla plików wieloliniowych, o czym $(...)cytuję
Thor
zdecydowanie muszę to zacytować ... /bin/echo -n "$(cat infile)" Poza tym nie jestem pewien, jaka echobyłaby maksymalna długość lub powłoka w wersjach systemu operacyjnego / powłoki / dystrybucji (właśnie to szukałem w Google i była to królicza nora), więc jestem Nie jestem pewien, jak przenośny (lub wydajny) byłby w rzeczywistości dla czegokolwiek innego niż małe pliki - ale dla małych plików, świetnie.
michael
8

Jeśli chcesz to zrobić dobrze, potrzebujesz czegoś takiego:

use autodie qw(open sysseek sysread truncate);

my $file = shift;
open my $fh, '+>>', $file;
my $pos = tell $fh;
sysseek $fh, $pos - 1, 0;
sysread $fh, my $buf, 1 or die 'No data to read?';

if($buf eq "\n"){
    truncate $fh, $pos - 1;
}

Otwieramy plik do odczytu i dołączenia; otwarcie do dołączenia oznacza, że ​​jesteśmy już seekna końcu pliku. Następnie otrzymujemy numeryczną pozycję końca pliku za pomocą tell. Używamy tej liczby do wyszukiwania wstecz jednego znaku, a następnie czytamy ten jeden znak. Jeśli jest to znak nowej linii, skracamy plik do znaku przed tą linią, w przeciwnym razie nic nie robimy.

Działa to w stałym czasie i stałej przestrzeni dla dowolnego wejścia i nie wymaga też więcej miejsca na dysku.

jrockway
źródło
2
ale ma tę wadę, że nie resetuje prawa własności / uprawnień do pliku ... błąd, czekaj ...
ysth
1
Rozległy, ale zarówno szybki, jak i solidny - wydaje się być jedyną prawdziwą odpowiedzią dotyczącą edycji plików w miejscu (a ponieważ może nie być oczywiste dla wszystkich: to jest skrypt Perla ).
mklement0
6

Oto ładne, uporządkowane rozwiązanie w języku Python. Nie próbowałem tu być lakoniczny.

To modyfikuje plik na miejscu, zamiast tworzyć kopię pliku i usuwać znak nowej linii z ostatniego wiersza kopii. Jeśli plik jest duży, będzie to znacznie szybsze niż rozwiązanie Perla, które zostało wybrane jako najlepsza odpowiedź.

Obcina plik o dwa bajty, jeśli ostatnie dwa bajty to CR / LF, lub o jeden bajt, jeśli ostatni bajt to LF. Nie podejmuje próby modyfikacji pliku, jeśli ostatnie bajty nie są (CR) LF. Obsługuje błędy. Przetestowano w Pythonie 2.6.

Umieść to w pliku o nazwie „striplast” i chmod +x striplast.

#!/usr/bin/python

# strip newline from last line of a file


import sys

def trunc(filename, new_len):
    try:
        # open with mode "append" so we have permission to modify
        # cannot open with mode "write" because that clobbers the file!
        f = open(filename, "ab")
        f.truncate(new_len)
        f.close()
    except IOError:
        print "cannot write to file:", filename
        sys.exit(2)

# get input argument
if len(sys.argv) == 2:
    filename = sys.argv[1]
else:
    filename = "--help"  # wrong number of arguments so print help

if filename == "--help" or filename == "-h" or filename == "/?":
    print "Usage: %s <filename>" % sys.argv[0]
    print "Strips a newline off the last line of a file."
    sys.exit(1)


try:
    # must have mode "b" (binary) to allow f.seek() with negative offset
    f = open(filename, "rb")
except IOError:
    print "file does not exist:", filename
    sys.exit(2)


SEEK_EOF = 2
f.seek(-2, SEEK_EOF)  # seek to two bytes before end of file

end_pos = f.tell()

line = f.read()
f.close()

if line.endswith("\r\n"):
    trunc(filename, end_pos)
elif line.endswith("\n"):
    trunc(filename, end_pos + 1)

PS W duchu „Perl golfa”, oto moje najkrótsze rozwiązanie w Pythonie. Siorbi cały plik ze standardowego wejścia do pamięci, usuwa wszystkie znaki nowej linii z końca i zapisuje wynik na standardowe wyjście. Nie tak zwięzły jak Perl; po prostu nie możesz pokonać Perla w takich drobnych, szybkich rzeczach.

Usuń znak „\ n” z wywołania do .rstrip()i usunie cały biały znak na końcu pliku, w tym wiele pustych wierszy.

Umieść to w „slurp_and_chomp.py” i uruchom python slurp_and_chomp.py < inputfile > outputfile.

import sys

sys.stdout.write(sys.stdin.read().rstrip("\n"))
steveha
źródło
os.path.isfile () poinformuje Cię o obecności pliku. Używanie try / except może spowodować wiele różnych błędów :)
Denis Barmenkov
5

Szybkim rozwiązaniem jest użycie narzędzia gnu truncate:

[ -z $(tail -c1 file) ] && truncate -s-1 file

Test będzie prawdziwy, jeśli plik ma na końcu nową linię.

Usuwanie jest bardzo szybkie, naprawdę na miejscu, nie jest potrzebny nowy plik, a wyszukiwanie również odczytuje od końca tylko jeden bajt ( tail -c1).

Izaak
źródło
1
truncate: missing file operand
Brian Hannay
2
w przykładzie brakuje tylko końcowej nazwy pliku, tj. [ -z $(tail -c1 filename) ] && truncate -s -1 filename(również w odpowiedzi na inny komentarz truncatepolecenie nie działa ze standardowym wejściem, wymagana jest nazwa pliku)
michael
4

Jeszcze inny perl WTDI:

perl -i -p0777we's/\n\z//' filename
ysth
źródło
3
$ perl -e 'local $ /; $ _ = <>; s / \ n $ //; print 'a-plik-tekstowy.txt

Zobacz także Dopasuj dowolny znak (w tym znaki nowej linii) w sed .

Sinan Ünür
źródło
1
To usuwa wszystkie nowe linie. Odpowiedniktr -d '\n'
wstrzymany do odwołania.
To też działa dobrze, prawdopodobnie mniej bluźniercze niż pawilony.
Todd Partridge 'Gen2ly'
Sinan, chociaż Linux i Unix mogą definiować pliki tekstowe tak, aby kończyły się znakiem nowej linii, Windows nie stawia takich wymagań. Na przykład Notatnik zapisze tylko wpisywane znaki bez dodawania na końcu niczego więcej. Kompilatory C mogą wymagać, aby plik źródłowy kończył się podziałem wiersza, ale pliki źródłowe w C nie są „tylko” plikami tekstowymi, więc mogą mieć dodatkowe wymagania.
Rob Kennedy
w tym duchu większość minifier javascript / css usuwa końcowe znaki nowej linii, a mimo to tworzy pliki tekstowe.
ysth
@Rob Kennedy i @ysth: Jest tam interesujący argument, dlaczego takie pliki nie są w rzeczywistości plikami tekstowymi i tym podobne.
Sinan Ünür
2

Korzystanie z dd:

file='/path/to/file'
[[ "$(tail -c 1 "${file}" | tr -dc '\n' | wc -c)" -eq 1 ]] && \
    printf "" | dd  of="${file}" seek=$(($(stat -f "%z" "${file}") - 1)) bs=1 count=1
    #printf "" | dd  of="${file}" seek=$(($(wc -c < "${file}") - 1)) bs=1 count=1
cpit
źródło
2
perl -pi -e 's/\n$// if(eof)' your_file
Vijay
źródło
Skutecznie to samo, co zaakceptowana odpowiedź, ale prawdopodobnie jaśniejsza koncepcja dla użytkowników nie korzystających z Perla. Zauważ, że nie ma potrzeby za glub nawiasów wokół eof: perl -pi -e 's/\n$// if eof' your_file.
mklement0
2

Zakładając typ pliku Unix i chcesz, aby tylko ostatnia nowa linia działała.

sed -e '${/^$/d}'

Nie będzie działać na wielu nowych liniach ...

* Działa tylko wtedy, gdy ostatnia linia jest pusta.

LoranceStinson
źródło
Oto sedrozwiązanie, które działa nawet w przypadku niepustego ostatniego wiersza: stackoverflow.com/a/52047796
wisbucky
1

Jeszcze inna odpowiedź FTR (i moja ulubiona!): Echo / cat to, co chcesz rozebrać i przechwycić dane wyjściowe za pomocą backticks. Ostatnia nowa linia zostanie usunięta. Na przykład:

# Sadly, outputs newline, and we have to feed the newline to sed to be portable
echo thingy | sed -e 's/thing/sill/'

# No newline! Happy.
out=`echo thingy | sed -e 's/thing/sill/'`
printf %s "$out"

# Similarly for files:
file=`cat file_ending_in_newline`
printf %s "$file" > file_no_newline
Nicholas Wilson
źródło
1
Znalazłem kombinację cat-printf przez przypadek (próbowałem uzyskać odwrotne zachowanie). Zauważ, że usunie to WSZYSTKIE końcowe znaki nowej linii, nie tylko ostatnią.
technosaurus
1

POSIX SED:

„$ {/ ^ $ / d}”

$ - match last line


{ COMMANDS } - A group of commands may be enclosed between { and } characters. This is particularly useful when you want a group of commands to be triggered by a single address (or address-range) match.
Oleg Mazko
źródło
Myślę, że spowoduje to usunięcie go tylko wtedy, gdy ostatnia linia jest pusta. Nie usunie końcowego znaku nowej linii, jeśli ostatnia linia nie jest pusta. Na przykład echo -en 'a\nb\n' | sed '${/^$/d}'niczego nie usunie. echo -en 'a\nb\n\n' | sed '${/^$/d}'zostanie usunięty, ponieważ cała ostatnia linia jest pusta.
wisbucky
1

Jest to dobre rozwiązanie, jeśli chcesz, aby działało z potokami / przekierowaniami zamiast odczytywania / wyprowadzania z lub do pliku. Działa to z jedną lub wieloma liniami. Działa niezależnie od tego, czy jest końcowy znak nowej linii, czy nie.

# with trailing newline
echo -en 'foo\nbar\n' | sed '$s/$//' | head -c -1

# still works without trailing newline
echo -en 'foo\nbar' | sed '$s/$//' | head -c -1

# read from a file
sed '$s/$//' myfile.txt | head -c -1

Detale:

  • head -c -1obcina ostatni znak ciągu, niezależnie od tego, jaki to znak. Więc jeśli ciąg nie kończy się znakiem nowej linii, tracisz znak.
  • Tak, aby adres ten problem, możemy dodać kolejną komendę, która doda końcowym znakiem nowej linii, jeśli nie jest jeden: sed '$s/$//'. Pierwszy $oznacza zastosowanie polecenia tylko do ostatniej linii. s/$//oznacza zastąpienie „końca wiersza” słowem „nic”, co oznacza nic nie robienie. Ale ma to efekt uboczny dodania końcowego znaku nowej linii, jeśli go nie ma.

Uwaga: domyślnie Mac headnie obsługuje tej -copcji. Możesz to zrobić brew install coreutilsi użyć gheadzamiast tego.

wisbucky
źródło
0

Chciałem to zrobić tylko dla kodu golfa, a potem po prostu skopiowałem kod z pliku i wkleiłem go do echo -n 'content'>fileinstrukcji.

dlamblin
źródło
W połowie drogi; kompletne podejście tutaj .
mklement0
0
sed ':a;/^\n*$/{$d;N;};/\n$/ba' file
ghostdog74
źródło
Działa, ale usuwa wszystkie końcowe znaki nowej linii.
mklement0
0

Miałem podobny problem, ale pracowałem z plikiem Windows i muszę zachować te CRLF - moje rozwiązanie w systemie Linux:

sed 's/\r//g' orig | awk '{if (NR>1) printf("\r\n"); printf("%s",$0)}' > tweaked
cadrian
źródło
0
sed -n "1 x;1 !H
$ {x;s/\n*$//p;}
" YourFile

Powinno usunąć ostatnie wystąpienie \ nw pliku. Nie działa na dużym pliku (z powodu ograniczenia bufora seda)

NeronLeVelu
źródło
0

rubin:

ruby -ne 'print $stdin.eof ? $_.strip : $_'

lub:

ruby -ane 'q=p;p=$_;puts q if $.>1;END{print p.strip!}'
szczyt
źródło