Comment puis-je supprimer une nouvelle ligne s'il s'agit du dernier caractère d'un fichier?

162

J'ai quelques fichiers que j'aimerais supprimer le dernier saut de ligne s'il s'agit du dernier caractère d'un fichier. od -cme montre que la commande que j'exécute écrit le fichier avec une nouvelle ligne à la fin:

0013600   n   t  >  \n

J'ai essayé quelques trucs avec sed, mais le mieux auquel je puisse penser ne fait pas l'affaire:

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

Des idees pour faire cela?

Todd Partridge 'Gen2ly'
la source
4
newline n'est qu'un seul caractère pour les retours à la ligne unix. Les retours à la ligne DOS sont composés de deux caractères. Bien sûr, le littéral "\ n" est composé de deux caractères. Lequel recherchez-vous réellement?
Suspendu jusqu'à nouvel ordre.
3
Bien que la représentation puisse être \n, dans Linux, il y a un caractère
pavium
10
Pouvez-vous expliquer pourquoi vous voulez faire cela? Les fichiers texte sont censés se terminer par une fin de ligne, à moins qu'ils ne soient entièrement vides. Il me semble étrange que vous souhaitiez avoir un fichier aussi tronqué?
Thomas Padron-McCarthy
La raison habituelle de faire quelque chose comme ceci est de supprimer une virgule à la fin de la dernière ligne d'un fichier CSV. Sed fonctionne bien, mais les nouvelles lignes doivent être traitées différemment.
pavium
9
@ ThomasPadron-McCarthy "En informatique, pour toutes les bonnes raisons de faire quelque chose, il existe une bonne raison de ne pas le faire et vice versa." -Jésus - "tu ne devrais pas faire ça" est une réponse horrible quelle que soit la question. Le format correct est: [comment le faire] mais [pourquoi cela peut être une mauvaise idée]. #sacrilege
Cory Mawhorter

Réponses:

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

ou, pour éditer le fichier sur place:

perl -pi -e 'chomp if eof' filename

[Note de l'éditeur: -pi -ec'était à l'origine -pie, mais, comme l'ont noté plusieurs commentateurs et expliqué par @hvd, ce dernier ne fonctionne pas.]

Cela a été décrit comme un «blasphème perl» sur le site awk que j'ai vu.

Mais, dans un test, cela a fonctionné.

Pavium
la source
11
Vous pouvez le rendre plus sûr en utilisant chomp. Et cela vaut la peine de glisser le fichier.
Sinan Ünür
6
Aussi blasphématoire que ce soit, cela fonctionne très bien. perl -i -pe 'chomp if eof' nom de fichier. Je vous remercie.
Todd Partridge 'Gen2ly'
13
Ce qui est drôle à propos du blasphème et de l'hérésie, c'est qu'il est généralement détesté parce que c'est correct. :)
Ether
8
Petite correction: vous pouvez utiliser perl -pi -e 'chomp if eof' filename, pour éditer un fichier sur place au lieu de créer un fichier temporaire
Romuald Brunet
7
perl -pie 'chomp if eof' filename-> Impossible d'ouvrir le script perl "chomp if eof": aucun fichier ou répertoire de ce type; perl -pi -e 'chomp if eof' filename-> fonctionne
aditsu quitte car SE est EVIL
56

Vous pouvez profiter du fait que les substitutions de commandes shell suppriment les caractères de fin de ligne :

Formulaire simple qui fonctionne dans bash, ksh, zsh:

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

Alternative portable (compatible POSIX) (légèrement moins efficace):

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

Remarque:

  • Si in.txtextrémités avec plusieurs caractères de nouvelle ligne, la substitution de commande supprime tous d'entre eux - grâce, @Sparhawk. (Il ne supprime pas les caractères d'espacement autres que les retours à la ligne de fin.)
  • Étant donné que cette approche lit le fichier d'entrée entier en mémoire , elle n'est recommandée que pour les fichiers plus petits.
  • printf %sgarantit qu'aucune nouvelle ligne n'est ajoutée à la sortie (c'est l'alternative compatible POSIX au non standard echo -n; voir http://pubs.opengroup.org/onlinepubs/009696799/utilities/echo.html et https: //unix.stackexchange. com / a / 65819 )

Un guide des autres réponses :

  • Si Perl est disponible, choisissez la réponse acceptée - elle est simple et économe en mémoire (ne lit pas tout le fichier d'entrée à la fois).

  • Sinon, considérez la réponse Awk de ghostdog74 - elle est obscure, mais aussi efficace en mémoire ; un équivalent plus lisible (compatible POSIX) est:

    • awk 'NR > 1 { print prev } { prev=$0 } END { ORS=""; print }' in.txt
    • L'impression est retardée d'une ligne afin que la dernière ligne puisse être traitée dans le ENDbloc, où elle est imprimée sans fin en \nraison de la définition du séparateur d'enregistrement de sortie ( OFS) sur une chaîne vide.
  • Si vous voulez une solution détaillée, mais rapide et robuste qui modifie réellement sur place (par opposition à la création d'un fichier temporaire qui remplace ensuite l'original), considérez le script Perl de jrockway .

mklement0
la source
3
NB s'il y a plusieurs retours à la ligne à la fin du fichier, cette commande les supprimera tous.
Sparhawk
47

Vous pouvez le faire avec headdepuis GNU coreutils, il prend en charge les arguments relatifs à la fin du fichier. Donc, pour ne pas utiliser le dernier octet:

head -c -1

Pour tester une nouvelle ligne de fin, vous pouvez utiliser tailet wc. L'exemple suivant enregistre le résultat dans un fichier temporaire et remplace par la suite l'original:

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

Vous pouvez également utiliser spongefrom moreutilspour effectuer des modifications "sur place":

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

Vous pouvez également créer une fonction réutilisable générale en insérant ceci dans votre .bashrcfichier:

# 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
}

Mettre à jour

Comme indiqué par KarlWilbur dans les commentaires et utilisé dans la réponse de Sorentar , truncate --size=-1peut remplacer head -c-1et prend en charge l'édition sur place.

Thor
la source
3
La meilleure solution à ce jour. Utilise un outil standard que toutes les distributions Linux ont, et est concis et clair, sans aucune magie sed ou perl.
Dakkaron le
2
Belle solution. Un changement est que je pense que j'utiliserais truncate --size=-1au lieu de head -c -1car il redimensionne simplement le fichier d'entrée plutôt que de lire le fichier d'entrée, de l'écrire dans un autre fichier, puis de remplacer l'original par le fichier de sortie.
Karl Wilbur
1
Notez que head -c -1cela supprimera le dernier caractère, qu'il s'agisse d'une nouvelle ligne ou non, c'est pourquoi vous devez vérifier si le dernier caractère est une nouvelle ligne avant de le supprimer.
wisbucky
Malheureusement, ne fonctionne pas sur Mac. Je soupçonne que cela ne fonctionne sur aucune variante BSD.
Edward Falk
16
head -n -1 abc > newfile
tail -n 1 abc | tr -d '\n' >> newfile

Modifier 2:

Voici une awkversion (corrigée) qui n'accumule pas un tableau potentiellement énorme:

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

Suspendu jusqu'à nouvel ordre.
la source
Bonne façon originale d'y penser. Merci Dennis.
Todd Partridge 'Gen2ly'
Vous avez raison. Je m'en remets à votre awkversion. Il faut deux décalages (et un test différent) et je n'en ai utilisé qu'un. Cependant, vous pouvez utiliser à la printfplace de ORS.
Suspendu jusqu'à nouvel ordre.
vous pouvez faire de la sortie un tube avec substitution de processus:head -n -1 abc | cat <(tail -n 1 abc | tr -d '\n') | ...
BCoates
2
Utiliser -c au lieu de -n pour la tête et la queue devrait être encore plus rapide.
rudimeier
1
Pour moi, head -n -1 abc a supprimé la dernière ligne réelle du fichier, laissant une nouvelle ligne à la fin; head -c -1 abc semblait mieux fonctionner
ChrisV
10

rester bouche bée

   awk '{q=p;p=$0}NR>1{print q}END{ORS = ""; print p}' file
ghostdog74
la source
Cela me semble encore beaucoup de personnages ... l'apprendre lentement :). Fait le travail cependant. Merci Ghostdog.
Todd Partridge 'Gen2ly'
1
awk '{ prev_line = line; line = $0; } NR > 1 { print prev_line; } END { ORS = ""; print line; }' filecela devrait être plus facile à lire.
Yevhen Pavliuk
Que diriez - vous: awk 'NR>1 {print p} {p=$0} END {printf $0}' file.
Isaac
@sorontar Le premier argument de printfest l' argument format . Ainsi, si le fichier d'entrée avait quelque chose qui pourrait être interprété comme un spécificateur de format comme %d, vous obtiendrez une erreur. Une solution serait de le changer enprintf "%s" $0
Robin A. Meade
9

Une méthode très simple pour les fichiers sur une seule ligne, nécessitant l'écho GNU de coreutils:

/bin/echo -n $(cat $file)
autre
la source
C'est une manière décente si ce n'est pas trop cher (répétitif).
Cela pose des problèmes lorsqu'il \nest présent. Au fur et à mesure qu'il est converti en une nouvelle ligne.
Chris Stryczynski
Semble également fonctionner pour les fichiers multi-lignes, il $(...)est cité
Thor
/bin/echo -n "$(cat infile)" Je dois absolument le citer ... De plus, je ne suis pas sûr de la longueur maximale echoou du shell dans les versions / distributions d'os / shell (je cherchais juste ceci et c'était un trou de lapin), alors je suis Je ne sais pas à quel point il serait portable (ou performant) pour autre chose que les petits fichiers - mais pour les petits fichiers, super.
michael
8

Si vous voulez bien faire les choses, vous avez besoin de quelque chose comme ceci:

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;
}

Nous ouvrons le fichier pour lecture et ajout; l'ouverture pour l'ajout signifie que nous sommes déjà seekà la fin du fichier. On obtient alors la position numérique de la fin du fichier avec tell. Nous utilisons ce nombre pour rechercher un caractère, puis nous lisons ce caractère. S'il s'agit d'une nouvelle ligne, nous tronquons le fichier au caractère avant cette nouvelle ligne, sinon nous ne faisons rien.

Cela fonctionne en temps constant et en espace constant pour n'importe quelle entrée, et ne nécessite pas non plus d'espace disque.

jrockway
la source
2
mais cela a l'inconvénient de ne pas réinitialiser la propriété / les autorisations pour le fichier ... euh, attendez ...
ysth
1
Verbose, mais à la fois rapide et robuste - semble être la seule vraie réponse d'édition de fichiers sur place ici (et comme cela peut ne pas être évident pour tout le monde: il s'agit d'un script Perl ).
mklement0
6

Voici une solution Python sympa et bien rangée. Je n'ai pas essayé d'être laconique ici.

Cela modifie le fichier sur place, plutôt que de faire une copie du fichier et de supprimer la nouvelle ligne de la dernière ligne de la copie. Si le fichier est volumineux, ce sera beaucoup plus rapide que la solution Perl qui a été choisie comme meilleure réponse.

Il tronque un fichier de deux octets si les deux derniers octets sont CR / LF, ou d'un octet si le dernier octet est LF. Il n'essaye pas de modifier le fichier si le ou les derniers octets ne sont pas (CR) LF. Il gère les erreurs. Testé en Python 2.6.

Mettez ceci dans un fichier appelé "striplast" et 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 Dans l'esprit du "Perl golf", voici ma solution Python la plus courte. Il extrait le fichier entier de l'entrée standard dans la mémoire, supprime toutes les nouvelles lignes à la fin et écrit le résultat sur la sortie standard. Pas aussi laconique que le Perl; vous ne pouvez pas battre Perl pour de petits trucs rapides et délicats comme celui-ci.

Supprimez le "\ n" de l'appel à .rstrip()et il supprimera tous les espaces blancs à la fin du fichier, y compris plusieurs lignes vides.

Mettez ceci dans "slurp_and_chomp.py" puis exécutez python slurp_and_chomp.py < inputfile > outputfile.

import sys

sys.stdout.write(sys.stdin.read().rstrip("\n"))
Steveha
la source
os.path.isfile () vous informera de la présence de fichiers. Utiliser try / except pourrait attraper beaucoup d'erreurs différentes :)
Denis Barmenkov
5

Une solution rapide utilise l'utilitaire gnu truncate:

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

Le test sera vrai si le fichier a une nouvelle ligne à la fin.

La suppression est très rapide, vraiment en place, aucun nouveau fichier n'est nécessaire et la recherche lit également à la fin un seul octet ( tail -c1).

Isaac
la source
1
truncate: opérande de fichier manquant
Brian Hannay
2
il manque juste le nom de fichier de fin dans l'exemple, c'est-à-dire [ -z $(tail -c1 filename) ] && truncate -s -1 filename(aussi, en réponse à l'autre commentaire, la truncatecommande ne fonctionne pas avec stdin, un nom de fichier est requis)
michael
4

Encore un autre WTDI perl:

perl -i -p0777we's/\n\z//' filename
ysth
la source
3
$ perl -e 'local $ /; $ _ = <>; s / \ n $ //; print 'a-text-file.txt

Voir aussi Faire correspondre n'importe quel caractère (y compris les sauts de ligne) dans sed .

Sinan Ünür
la source
1
Cela supprime toutes les nouvelles lignes. Équivalent àtr -d '\n'
Suspendu jusqu'à nouvel ordre.
Cela fonctionne bien aussi, probablement moins blasphématoire que celui des paviums.
Todd Partridge 'Gen2ly'
Sinan, bien que Linux et Unix puissent définir des fichiers texte pour se terminer par une nouvelle ligne, Windows ne pose pas une telle exigence. Le Bloc-notes, par exemple, n'écrira que les caractères que vous tapez sans rien ajouter de plus à la fin. Les compilateurs C peuvent exiger qu'un fichier source se termine par un saut de ligne, mais les fichiers source C ne sont pas "simplement" des fichiers texte, ils peuvent donc avoir des exigences supplémentaires.
Rob Kennedy
dans cette veine, la plupart des minificateurs javascript / css supprimeront les retours à la ligne de fin, tout en produisant des fichiers texte.
ysth
@Rob Kennedy et @ysth: Il y a là un argument intéressant pour expliquer pourquoi ces fichiers ne sont pas réellement des fichiers texte et autres.
Sinan Ünür
2

Utilisation de 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
la source
2
perl -pi -e 's/\n$// if(eof)' your_file
Vijay
la source
Effectivement la même que la réponse acceptée, mais sans doute plus claire dans son concept pour les utilisateurs non-Perl. Notez qu'il n'y a pas besoin de les gou les parenthèses autour de eof: perl -pi -e 's/\n$// if eof' your_file.
mklement0
2

En supposant le type de fichier Unix et que vous ne voulez que la dernière nouvelle ligne, cela fonctionne.

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

Cela ne fonctionnera pas sur plusieurs nouvelles lignes ...

* Fonctionne uniquement si la dernière ligne est une ligne vierge.

LoranceStinson
la source
Voici une sedsolution qui fonctionne même pour une dernière ligne non vierge: stackoverflow.com/a/52047796
wisbucky
1

Encore une autre réponse FTR (et ma préférée!): Echo / cat la chose que vous voulez dépouiller et capturer la sortie par des backticks. La dernière nouvelle ligne sera supprimée. Par exemple:

# 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
Nicolas Wilson
la source
1
J'ai trouvé le combo cat-printf par accident (j'essayais d'obtenir le comportement opposé). Notez que cela supprimera TOUTES les nouvelles lignes de fin, pas seulement la dernière.
technosaurus du
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
la source
Je pense que cela ne le supprimera que si la dernière ligne est vide. Il ne supprimera pas la nouvelle ligne de fin si la dernière ligne n'est pas vide. Par exemple, echo -en 'a\nb\n' | sed '${/^$/d}'ne supprimera rien. echo -en 'a\nb\n\n' | sed '${/^$/d}'supprimera puisque toute la dernière ligne est vide.
wisbucky
1

C'est une bonne solution si vous en avez besoin pour travailler avec des tubes / redirection au lieu de lire / sortir depuis ou vers un fichier. Cela fonctionne avec une ou plusieurs lignes. Cela fonctionne qu'il y ait une nouvelle ligne de fin ou non.

# 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

Détails:

  • head -c -1tronque le dernier caractère de la chaîne, quel que soit le caractère. Donc, si la chaîne ne se termine pas par une nouvelle ligne, vous perdrez un caractère.
  • Donc , pour remédier à ce problème, nous ajoutons une autre commande qui ajoutera une nouvelle ligne de fuite s'il n'y a pas un: sed '$s/$//'. Le premier $signifie n'appliquer la commande qu'à la dernière ligne. s/$//signifie remplacer la «fin de la ligne» par «rien», ce qui ne fait essentiellement rien. Mais cela a pour effet secondaire d'ajouter une nouvelle ligne de fin s'il n'y en a pas.

Remarque: la valeur par défaut de Mac headne prend pas en charge l' -coption. Vous pouvez faire brew install coreutilset utiliser à la gheadplace.

wisbucky
la source
0

La seule fois où j'ai voulu faire cela, c'est pour le code golf, puis j'ai simplement copié mon code hors du fichier et l'ai collé dans une echo -n 'content'>fileinstruction.

dlamblin
la source
À mi-chemin; approche complète ici .
mklement0
0
sed ':a;/^\n*$/{$d;N;};/\n$/ba' file
ghostdog74
la source
Fonctionne, mais supprime toutes les nouvelles lignes de fin.
mklement0
0

J'ai eu un problème similaire, mais je travaillais avec un fichier Windows et j'ai besoin de conserver ces CRLF - ma solution sous Linux:

sed 's/\r//g' orig | awk '{if (NR>1) printf("\r\n"); printf("%s",$0)}' > tweaked
cadrien
la source
0
sed -n "1 x;1 !H
$ {x;s/\n*$//p;}
" YourFile

Devrait supprimer toute dernière occurrence de \ n dans le fichier. Ne fonctionne pas sur un fichier énorme (en raison de la limitation du tampon sed)

NeronLeVelu
la source
0

rubis:

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

ou:

ruby -ane 'q=p;p=$_;puts q if $.>1;END{print p.strip!}'
de pointe
la source