Come aggiungere una nuova riga alla fine di un file?


190

Usando i sistemi di controllo della versione mi infastidisce il rumore quando dice il diff No newline at end of file.

Quindi mi chiedevo: come aggiungere una nuova riga alla fine di un file per sbarazzarsi di quei messaggi?



1
Bella soluzione in basso che disinfetta tutti i file in modo ricorsivo. Risposta di @Patrick Oscity
Qwerty


In futuro, gli editor di testo hanno spesso opzioni per garantire che ci sia una nuova riga finale che tu e i tuoi collaboratori potreste usare per mantenere pulite.
Nick T

Risposte:


44

Per disinfettare ricorsivamente un progetto, utilizzo questo oneliner:

git ls-files -z | while IFS= read -rd '' f; do tail -c1 < "$f" | read -r _ || echo >> "$f"; done

Spiegazione:

  • git ls-files -zelenca i file nel repository. Prende un modello opzionale come parametro aggiuntivo che potrebbe essere utile in alcuni casi se si desidera limitare l'operazione a determinati file / directory. In alternativa, è possibile utilizzare find -print0 ...o programmi simili per elencare i file interessati: è sufficiente assicurarsi che emetta NULvoci delimitate.

  • while IFS= read -rd '' f; do ... done scorre le voci, gestendo in modo sicuro i nomi dei file che includono spazi bianchi e / o newline.

  • tail -c1 < "$f" legge l'ultimo carattere da un file.

  • read -r _ esce con uno stato di uscita diverso da zero se manca una nuova riga finale.

  • || echo >> "$f" aggiunge una nuova riga al file se lo stato di uscita del comando precedente era diverso da zero.


Puoi anche farlo in questo modo se vuoi solo disinfettare un sottoinsieme dei tuoi file:find -name \*.java | while read f; do tail -n1 $f | read -r _ || echo >> $f; done
Per Lundberg

@ StéphaneChazelas buoni suggerimenti, cercherò di incorporare questo nella mia risposta.
Patrick Oscity,

@PerLundberg puoi anche passare un modello a git ls-filescui ti salverai ancora dalla modifica dei file che non sono monitorati nel controllo versione.
Patrick Oscity,

@ StéphaneChazelas aggiungendo IFS= per disinserire il separatore è buono per preservare lo spazio bianco circostante. Le voci nulle terminate sono rilevanti solo se hai file o directory con una nuova riga nel loro nome, che sembra un po 'inverosimile, ma è il modo più corretto di gestire il caso generico, sono d'accordo. Proprio come un piccolo avvertimento: l' -dopzione per readnon è disponibile in POSIX sh.
Patrick Oscity,

Sì, quindi i miei zsh / bash's . Vedi anche il mio uso di tail -n1 < "$f"per evitare problemi con i nomi di file che iniziano con -( tail -n1 -- "$f"non funziona per il file chiamato -). Potresti voler chiarire che la risposta è ora specifica per zsh / bash.
Stéphane Chazelas il

203

Ecco qua :

sed -i -e '$a\' file

E in alternativa per OS X sed:

sed -i '' -e '$a\' file

Ciò si aggiunge \nalla fine del file solo se non termina già con una nuova riga. Quindi, se lo esegui due volte, non aggiungerà un'altra nuova riga:

$ cd "$(mktemp -d)"
$ printf foo > test.txt
$ sed -e '$a\' test.txt > test-with-eol.txt
$ diff test*
1c1
< foo
\ No newline at end of file
---
> foo
$ echo $?
1
$ sed -e '$a\' test-with-eol.txt > test-still-with-one-eol.txt
$ diff test-with-eol.txt test-still-with-one-eol.txt
$ echo $?
0

1
@jwd: Da man sed: $ Match the last line.ma forse funziona solo per caso. La tua soluzione funziona anche.
1212

1
La tua soluzione è anche più elegante e l'ho testata e impegnata , ma come può funzionare? Se $corrisponde all'ultima riga, perché non aggiunge un'altra riga a una stringa che contiene già una riga?
1212

27
Ci sono due significati diversi di $. All'interno di una regex, come nel caso del modulo /<regex>/, ha il solito significato di "corrispondenza fine linea". Altrimenti, usato come indirizzo, sed gli dà il significato speciale di "ultima riga nel file". Il codice funziona perché sed di default aggiunge una nuova riga al suo output se non è già presente. Il codice "$ a \" dice semplicemente "corrisponde all'ultima riga del file e non aggiunge nulla ad esso". Ma implicitamente, sed aggiunge la nuova riga a ogni linea che elabora (come questa $linea) se non è già presente.
jwd

1
Per quanto riguarda la manpage: la citazione a cui ti riferisci è nella sezione "Indirizzi". Metterlo dentro gli /regex/dà un significato diverso. Le pagine di manuale di FreeBSD sono un po 'più informativo, penso: freebsd.org/cgi/man.cgi?query=sed
JWD

2
Se il file termina già con una nuova riga, ciò non lo modifica, ma lo riscrive e aggiorna il suo timestamp. Questo può o non può importare.
Keith Thompson,

39

Dare un'occhiata:

$ echo -n foo > foo 
$ cat foo
foo$
$ echo "" >> foo
$ cat foo
foo

quindi echo "" >> noeol-filedovrebbe fare il trucco. (O intendevi chiedere di identificare questi file e risolverli?)

modifica rimosso il ""da echo "" >> foo(vedi il commento di @yuyichao ) edit2 aggiunto di ""nuovo ( ma vedi il commento di @Keith Thompson)


4
il ""non è necessario (almeno per bash) e tail -1 | wc -lpuò essere utilizzato per scoprire il file senza una nuova riga alla fine
yuyichao

5
@yuyichao: ""non è necessario per bash, ma ho visto echoimplementazioni che non stampano nulla quando invocate senza argomenti (anche se nessuna di quelle che trovo ora lo fanno). echo "" >> noeol-fileè probabilmente leggermente più robusto. printf "\n" >> noeol-fileè ancora di più.
Keith Thompson,

2
@KeithThompson, csh's echoè quello noto per emettere nulla quando non superato ogni discussione. Ma se poi supporteremo shell non simili a Bourne, dovremmo farcela echo ''invece di echo ""come echo ""farebbe ""<newline>con rco esper esempio.
Stéphane Chazelas,

1
@ StéphaneChazelas: E tcsh, a differenza csh, stampa una nuova riga quando viene invocato senza argomenti, indipendentemente dall'impostazione di $echo_style.
Keith Thompson,

16

Un'altra soluzione che utilizza ed. Questa soluzione ha effetto solo sull'ultima riga e solo se \nmanca:

ed -s file <<< w

Funziona essenzialmente aprendo il file per la modifica tramite uno script, lo script è il singolo wcomando, che riscrive il file su disco. Si basa su questa frase trovata nella ed(1)pagina man:

LIMITAZIONI
       (...)

       Se un file di testo (non binario) non è terminato da un carattere di nuova riga,
       quindi ed aggiunge uno alla lettura / scrittura. Nel caso di un binario
       file, ed non aggiunge una nuova riga in lettura / scrittura.

1
Questo non aggiunge una nuova riga per me.
Olhovsky,

4
Per me va bene; stampa anche "Newline appended" (ed-1.10-1 su Arch Linux).
Stefan Majewsky,

12

Un modo semplice, portatile, conforme a POSIX per aggiungere una nuova riga finale assente a un file sarebbe:

[ -n "$(tail -c1 file)" ] && echo >> file

Questo approccio non ha bisogno di leggere l'intero file; può semplicemente cercare di EOF e lavorare da lì.

Inoltre, questo approccio non ha bisogno di creare file temporanei dietro la schiena (ad es. Sed -i), quindi i collegamenti fisici non sono interessati.

echo aggiunge una nuova riga al file solo quando il risultato della sostituzione del comando è una stringa non vuota. Si noti che ciò può accadere solo se il file non è vuoto e l'ultimo byte non è una nuova riga.

Se l'ultimo byte del file è una nuova riga, tail lo restituisce, quindi la sostituzione del comando lo spoglia; il risultato è una stringa vuota. Il test -n fallisce e l'eco non viene eseguito.

Se il file è vuoto, il risultato della sostituzione del comando è anche una stringa vuota e di nuovo l'eco non viene eseguito. Ciò è auspicabile, poiché un file vuoto non è un file di testo non valido, né è equivalente a un file di testo non vuoto con una riga vuota.


1
Si noti che non funziona yashse l'ultimo carattere nel file è un carattere multibyte (ad esempio nelle versioni locali UTF-8) o se la locale è C e l'ultimo byte nel file ha l'ottavo bit impostato. Con altre shell (tranne zsh), non aggiungerebbe una nuova riga se il file terminasse in un byte NUL (ma, di nuovo, ciò significherebbe che l'input sarebbe non testuale anche dopo l'aggiunta di una nuova riga).
Stéphane Chazelas,


1
È possibile eseguirlo per ogni file in una cartella e sottocartelle?
Qwerty,

12

Aggiungi newline indipendentemente da:

echo >> filename

Ecco un modo per verificare se esiste una nuova riga alla fine prima di aggiungerne una, usando Python:

f=filename; python -c "import sys; sys.exit(open(\"$f\").read().endswith('\n'))" && echo >> $f

1
Non userei la versione di Python in alcun tipo di loop a causa del lento tempo di avvio di Python. Ovviamente potresti fare il loop in Python se lo desideri.
Kevin Cox,

2
Il tempo di avvio per Python è di 0,03 secondi qui. Lo consideri davvero problematico?
Alexander

3
Il tempo di avvio è importante se si chiama python in un ciclo, ecco perché ho detto di considerare di fare il ciclo in python. Quindi dovrai sostenere il costo di avvio una sola volta. Per me, metà del costo dell'avvio è più della metà del tempo dell'intero snipit, considererei questo notevole sovraccarico. (Ancora una volta, irrilevante anche solo per fare un piccolo numero di file)
Kevin Cox,

2
echo ""sembra più robusto di echo -n '\n'. Oppure potresti usareprintf '\n'
Keith Thompson il

2
Questo ha funzionato bene per me
Daniel Gomez Rico, il

8

La soluzione più veloce è:

[ -n "$(tail -c1 file)" ] && printf '\n' >>file 

  1. È veramente veloce.
    Su un file di medie dimensioni seq 99999999 >fileciò richiede millisecondi.
    Altre soluzioni richiedono molto tempo:

    [ -n "$(tail -c1 file)" ] && printf '\n' >>file  0.013 sec
    vi -ecwq file                                    2.544 sec
    paste file 1<> file                             31.943 sec
    ed -s file <<< w                             1m  4.422 sec
    sed -i -e '$a\' file                         3m 20.931 sec
  2. Funziona in ash, bash, lksh, mksh, ksh93, attsh e zsh ma non in yash.

  3. Non modifica il timestamp del file se non è necessario aggiungere una nuova riga.
    Tutte le altre soluzioni presentate qui cambiano il timestamp del file.
  4. Tutte le soluzioni sopra sono POSIX valide.

Se hai bisogno di una soluzione portatile per lo yash (e tutte le altre shell sopra elencate), potrebbe diventare un po 'più complesso:

f=file
if       [ "$(tail -c1 "$f"; echo x)" != "$(printf '\nx')" ]
then     printf '\n' >>"$f"
fi

7

Il modo più rapido per verificare se l'ultimo byte di un file è una nuova riga consiste nel leggere solo l'ultimo byte. Questo potrebbe essere fatto con tail -c1 file. Tuttavia, il modo semplicistico di verificare se il valore del byte è una nuova riga, a seconda della solita rimozione della shell di una nuova riga finale all'interno di un'espansione del comando, fallisce (ad esempio) in yash, quando l'ultimo carattere nel file è un UTF- 8 valore.

Il modo corretto, conforme a POSIX, tutto (ragionevole) conchiglie per scoprire se l'ultimo byte di un file è una nuova linea è usare xxd o hexdump:

tail -c1 file | xxd -u -p
tail -c1 file | hexdump -v -e '/1 "%02X"'

Quindi, confrontando l'output di cui sopra 0Afornirà un test affidabile.
È utile evitare di aggiungere una nuova riga a un file altrimenti vuoto.
File che non fornirà un ultimo carattere di 0A, ovviamente:

f=file
a=$(tail -c1 "$f" | hexdump -v -e '/1 "%02X"')
[ -s "$f" -a "$a" != "0A" ] && echo >> "$f"

Breve e dolce Questo richiede pochissimo tempo poiché legge solo l'ultimo byte (cerca in EOF). Non importa se il file è grande. Quindi aggiungere solo un byte se necessario.

Non sono necessari né utilizzati file temporanei. Nessun hardlink è interessato.

Se questo test viene eseguito due volte, non aggiungerà un'altra riga.


1
@crw Credo che aggiunga informazioni utili.
sorontar

2
Notare che né le utility POSIX xxdné le hexdumpsono. Nel toolchest POSIX, c'è od -An -tx1per ottenere il valore esadecimale di un byte.
Stéphane Chazelas,

@ StéphaneChazelas Per favore, inseriscilo come risposta; Sono venuto qui alla ricerca di questo commento troppe volte :)
Kelvin il

@kelvin, ho aggiornato la mia risposta
Stéphane Chazelas il

Si noti che POSIX non garantisce che il valore di LF sia 0x0a. Esistono ancora sistemi POSIX dove non lo sono (quelli basati su EBCDIC) sebbene siano estremamente rari in questi giorni.
Stéphane Chazelas,

4

Faresti meglio a correggere l'editor dell'utente che ha modificato il file per ultimo. Se sei l'ultima persona ad aver modificato il file, quale editor stai usando, immagino che compagno di testo ...?


2
Vim è l'editore in questione. Ma in generale, hai ragione, non dovrei solo correggere i sintomi;)
k0pernikus,

6
per vim, devi fare di tutto e fare la danza binaria-file-on-save per ottenere vim per non aggiungere una nuova riga alla fine del file - semplicemente non fare quella danza. OPPURE, per correggere semplicemente i file esistenti, aprili in vim e salvali e vim 'aggiusterà' la nuova riga mancante per te (può essere facilmente copiata per più file)
AD7six,

3
La mia emacsnon aggiungere una nuova riga alla fine del file.
enzotib,

2
Grazie per il commento @ AD7six, continuo a ricevere rapporti fantasma da diff quando commetto cose, su come il file originale non ha una nuova riga alla fine. Non importa come modifico un file con VIM, non riesco a farlo per non inserire una nuova riga lì. Quindi è solo Vim a farlo.
Steven Lu,

1
@enzotib: ho (setq require-final-newline 'ask)nel mio.emacs
Keith Thompson il

3

Se si desidera aggiungere rapidamente una nuova riga durante l'elaborazione di una pipeline, utilizzare questo:

outputting_program | { cat ; echo ; }

è anche conforme a POSIX.

Quindi, ovviamente, puoi reindirizzarlo su un file.


2
È utile il fatto di poterlo utilizzare in una pipeline. Questo mi permette di contare il numero di righe in un file CSV, esclusa l'intestazione. E aiuta a ottenere un conteggio delle righe accurato sui file di Windows che non terminano con una nuova riga o un ritorno a capo. cat file.csv | tr "\r" "\n" | { cat; echo; } | sed "/^[[:space:]]*$/d" | tail -n +2 | wc -l
Kyle Tolle,

3

A condizione che non vi siano null nell'input:

paste - <>infile >&0

... basterebbe sempre aggiungere una nuova riga solo alla fine di un file se non ne aveva già uno. E basta leggere il file di input solo una volta per farlo bene.


Ciò non funzionerà in questo modo poiché stdin e stdout condividono la stessa descrizione di file aperto (quindi cursore all'interno del file). Avresti bisogno paste infile 1<> infileinvece.
Stéphane Chazelas,

2

Sebbene non risponda direttamente alla domanda, ecco uno script correlato che ho scritto per rilevare i file che non terminano con la nuova riga. È molto veloce.

find . -type f | # sort |        # sort file names if you like
/usr/bin/perl -lne '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

Lo script perl legge un elenco di nomi di file (ordinati facoltativamente) da stdin e per ogni file legge l'ultimo byte per determinare se il file termina in una nuova riga o meno. È molto veloce perché evita di leggere l'intero contenuto di ciascun file. Emette una riga per ogni file che legge, preceduto da "errore:" se si verifica un tipo di errore, "vuoto:" se il file è vuoto (non termina con la nuova riga!), "EOL:" ("fine di line ") se il file termina con newline e" no EOL: "se il file non termina con newline.

Nota: lo script non gestisce i nomi dei file che contengono nuove righe. Se utilizzi un sistema GNU o BSD, puoi gestire tutti i possibili nomi di file aggiungendo -print0 per trovare, -z per ordinare e -0 per perl, in questo modo:

find . -type f -print0 | sort -z |
/usr/bin/perl -ln0e '
   open FH, "<", $_ or do { print " error: $_"; next };
   $pos = sysseek FH, 0, 2;                     # seek to EOF
   if (!defined $pos)     { print " error: $_"; next }
   if ($pos == 0)         { print " empty: $_"; next }
   $pos = sysseek FH, -1, 1;                    # seek to last char
   if (!defined $pos)     { print " error: $_"; next }
   $cnt = sysread FH, $c, 1;
   if (!$cnt)             { print " error: $_"; next }
   if ($c eq "\n")        { print "   EOL: $_"; next }
   else                   { print "no EOL: $_"; next }
'

Ovviamente, dovresti comunque trovare un modo per codificare i nomi dei file con nuove righe nell'output (lasciato come esercizio per il lettore).

L'output potrebbe essere filtrato, se lo si desidera, per aggiungere una nuova riga a quei file che non ne hanno uno, molto semplicemente con

 echo >> "$filename"

La mancanza di una nuova riga finale può causare bug negli script poiché alcune versioni della shell e altre utilità non gestiranno correttamente una nuova riga finale mancante durante la lettura di tale file.

Nella mia esperienza, la mancanza di una nuova riga finale è causata dall'utilizzo di varie utilità di Windows per modificare i file. Non ho mai visto VIM causare una nuova riga finale mancante durante la modifica di un file, anche se riporterà su tali file.

Infine, ci sono script molto più brevi (ma più lenti) che possono passare in rassegna gli input dei nomi dei file per stampare quei file che non terminano con una nuova riga, come:

/usr/bin/perl -ne 'print "$ARGV\n" if /.\z/' -- FILE1 FILE2 ...

1

Gli editor vi/ vim/ exvengono aggiunti automaticamente <EOL>a EOF a meno che il file non lo abbia già.

Quindi prova uno di questi:

vi -ecwq foo.txt

che equivale a:

ex -cwq foo.txt

test:

$ printf foo > foo.txt && wc foo.txt
0 1 3 foo.txt
$ ex -scwq foo.txt && wc foo.txt
1 1 4 foo.txt

Per correggere più file, selezionare: Come correggere "Nessuna nuova riga alla fine del file" per molti file? a SO

Perché questo è così importante? Per mantenere i nostri file POSIX compatibili .


0

Per applicare la risposta accettata a tutti i file nella directory corrente (oltre alle sottodirectory):

$ find . -type f -exec sed -i -e '$a\' {} \;

Funziona su Linux (Ubuntu). Su OS X probabilmente devi usare -i ''(non testato).


4
Nota che find .elenca tutti i file, inclusi i file in .git. Per escludere:find . -type f -not -path './.git/*' -exec sed -i -e '$a\' {} \;
friederbluemle il

Vorrei aver letto questo commento / pensato prima di eseguirlo. Oh bene.
kstev,

0

Almeno nelle versioni GNU, semplicemente grep ''oawk 1 canonicalizza il suo input, aggiungendo una nuova riga finale se non già presente. Copiano il file nel processo, il che richiede tempo se grande (ma il sorgente non dovrebbe essere troppo grande da leggere comunque?) E aggiorna il modtime a meno che tu non faccia qualcosa di simile

 mv file old; grep '' <old >file; touch -r old file

(sebbene ciò possa andare bene su un file che si sta effettuando il check-in perché è stato modificato) e perde collegamenti, permessi non predefiniti e ACL ecc. a meno che non si sia ancora più attenti.


O semplicemente grep '' file 1<> file, anche se questo continuerebbe a leggere e scrivere il file completamente.
Stéphane Chazelas,

-1

Funziona in AIX ksh:

lastchar=`tail -c 1 *filename*`
if [ `echo "$lastchar" | wc -c` -gt "1" ]
then
    echo "/n" >> *filename*
fi

Nel mio caso, se nel file manca la nuova riga, il wccomando restituisce un valore di 2e scriviamo una nuova riga.


Il feedback arriverà sotto forma di voti positivi o negativi, o ti verrà chiesto nei commenti di delineare le tue risposte / domande più, inutile chiederlo nel corpo della risposta. Tienilo al punto, benvenuto su stackexchange!
k0pernikus,

-1

Aggiungendo alla risposta di Patrick Oscity , se vuoi solo applicarla a una directory specifica, puoi anche usare:

find -type f | while read f; do tail -n1 $f | read -r _ || echo >> $f; done

Esegui questo all'interno della directory a cui desideri aggiungere nuove righe.


-1

echo $'' >> <FILE_NAME> aggiungerà una riga vuota alla fine del file.

echo $'\n\n' >> <FILE_NAME> aggiungerà 3 righe vuote alla fine del file.


StackExchange ha una formattazione divertente, l'ho risolto per te :-)
peterh

-1

Se il file è terminato con terminazioni di linea di Windows\r\n e ci si trova in Linux, è possibile utilizzare questo sedcomando. Si aggiunge \r\nall'ultima riga solo se non è già presente:

sed -i -e '$s/\([^\r]\)$/\1\r\n/'

Spiegazione:

-i    replace in place
-e    script to run
$     matches last line of a file
s     substitute
\([^\r]\)$    search the last character in the line which is not a \r
\1\r\n    replace it with itself and add \r\n

Se l'ultima riga contiene già a, \r\nla ricerca regexp non corrisponderà, quindi non accadrà nulla.


-1

Puoi scrivere una fix-non-delimited-linesceneggiatura come:

#! /bin/zsh -
zmodload zsh/system || exit
ret=0
for file do
  if sysopen -rwu0 -- "$file"; then
    if sysseek -w end -1; then
      read -r x || print -u0
    else
      syserror -p "Can't seek in $file before the last byte: "
      ret=1
    fi
  else
    ret=1
  fi
done
exit $ret

Contrariamente ad alcune delle soluzioni fornite qui, esso

  • dovrebbe essere efficiente in quanto non esegue il fork di alcun processo, legge solo un byte per ogni file e non riscrive il file (basta aggiungere una nuova riga)
  • non interromperà i collegamenti simbolici / hardlink o influenzerà i metadati (inoltre, i ctime / mtime vengono aggiornati solo quando viene aggiunta una nuova riga)
  • dovrebbe funzionare correttamente anche se l'ultimo byte è un NUL o fa parte di un carattere multi-byte.
  • dovrebbe funzionare correttamente indipendentemente da quali caratteri o non caratteri possono contenere i nomi dei file
  • Dovrebbe gestire correttamente file illeggibili o non scrivibili o non ricercabili (e riportare gli errori di conseguenza)
  • Non dovrebbe aggiungere una nuova riga a file vuoti (ma segnala un errore su una ricerca non valida in quel caso)

Puoi usarlo ad esempio come:

that-script *.txt

o:

git ls-files -z | xargs -0 that-script

POSIX, potresti fare qualcosa di funzionalmente equivalente

export LC_ALL=C
ret=0
for file do
  [ -s "$file" ] || continue
  {
    c=$(tail -c 1 | od -An -vtc)
    case $c in
      (*'\n'*) ;;
      (*[![:space:]]*) printf '\n' >&0 || ret=$?;;
      (*) ret=1;; # tail likely failed
    esac
  } 0<> "$file" || ret=$? # record failure to open
done
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.