Comment supprimer plusieurs sauts de ligne à l'EOF?

25

J'ai des fichiers qui se terminent par un ou plusieurs sauts de ligne et doivent se terminer par un seul saut de ligne. Comment puis-je faire cela avec les outils Bash / Unix / GNU?

Exemple de mauvais fichier:

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

Exemple de fichier corrigé:

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

En d'autres termes: il doit y avoir exactement une nouvelle ligne entre l'EOF et le dernier caractère non nouvelle ligne du fichier.

Implémentation de référence

Lisez le contenu du fichier, coupez une seule nouvelle ligne jusqu'à ce qu'il n'y ait plus deux nouvelles lignes à la fin, réécrivez-la:

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

Clarification: Bien sûr, la tuyauterie est autorisée, si c'est plus élégant.

Bengt
la source

Réponses:

16
awk '/^$/ {nlstack=nlstack "\n";next;} {printf "%s",nlstack; nlstack=""; print;}' file
Hauke ​​Laging
la source
2
+1: les solutions d'awk sont (presque) toujours élégantes et lisibles!
Olivier Dulac
@OlivierDulac En effet. Quand j'ai vu la sedproposition, je pensais juste OMG ...
Hauke ​​Laging
1
cela ne fonctionne pas sur OSX Mavericks en utilisant le dernier awk disponible de Homebrew. Il contient des erreurs awk: illegal statement. brew install mawket changer la commande en mawkfonctionne cependant.
tjmcewan
@noname Je ne comprends même pas la question ...
Hauke ​​Laging
Tout awk dans lequel le script ne fonctionne pas est un awk mal cassé - arrêtez de l'utiliser et obtenez un nouvel awk parce que s'il ne peut pas faire cela, alors qui sait quel autre casse il a.
Ed Morton
21

À partir de scripts d'une ligne utiles pour sed .

# Delete all trailing blank lines at end of file (only).
sed -e :a -e '/^\n*$/{$d;N;};/\n$/ba' file
Alexey Shmalko
la source
4
Merci, j'ai utilisé ce qui suit pour le faire en place pour plusieurs fichiers: find . -type f -name '*.js' -exec sed --in-place -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
jakub.g
@ jakub.g en place et récursif est exactement ce dont j'avais besoin. Je vous remercie.
Buttle Butkus
Pour ajouter à l'excellent commentaire de @ jakub.g, vous pouvez invoquer la commande comme celle-ci sur OS X:find . -type f -name '*.js' -exec sed -i '' -e :a -e '/^\n*$/{$d;N;};/\n$/ba' {} \;
davejagoda
18

Puisque vous avez déjà des réponses avec les outils les plus adaptés sed et awk; vous pouvez profiter du fait que les $(< file)bandes vides de fin sont supprimées.

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

Ce hack bon marché ne fonctionnerait pas pour supprimer les lignes vides de fin qui peuvent contenir des espaces ou d'autres caractères non imprimables, uniquement pour supprimer les lignes vides de fin. Cela ne fonctionnera pas non plus si le fichier contient des octets nuls.

Dans les shells autres que bash et zsh, utilisez $(cat file)plutôt que $(<file).

llua
la source
+1 pour signaler à quoi ressemble un bug: $ (<fichier) ne lit pas vraiment le fichier? pourquoi rejette-t-il les sauts de ligne? (il le fait, je viens de le tester, merci de l'avoir signalé!)
Olivier Dulac
2
@OlivierDulac $()supprime les nouvelles lignes de fin. C'est une décision de conception. Je suppose que cela facilitera l'intégration dans d'autres chaînes: ce echo "On $(date ...) we will meet."serait mal avec la nouvelle ligne que presque toutes les commandes shell produisent à la fin.
Hauke ​​Laging
@HaukeLaging: bon point, c'est probablement la source de ce comportement
Olivier Dulac
J'ai ajouté un cas particulier pour éviter annexant « \ n » pour vider les fichiers: [[ $a == '' ]] || printf '%s\n' "$a" >"$file".
davidchambers
Pour supprimer plusieurs sauts de ligne au début d'un fichier, insérez tac dans le processus (j'utilise gnu coreutils sur Mac, donc gtac pour moi):a=$(gtac file.txt); printf '%s\n' "$a" | gtac > file.txt
r_alex_hall
5

Vous pouvez utiliser cette astuce avec cat& printf:

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

Par exemple

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

Le $dénote la fin d'une ligne.

Les références

slm
la source
4

Cette question est étiquetée avec , mais personne n'a proposé de edsolution.

En voici un:

ed -s file <<'ED_END'
a

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

ou équivalent,

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

ed vous placera par défaut à la dernière ligne du tampon d'édition au démarrage.

La première commande ( a) ajoute une ligne vide à la fin du tampon (la ligne vide dans le script d'édition est cette ligne, et le point ( .) est juste pour revenir en mode commande).

La deuxième commande ( ?) recherche la ligne précédente la plus proche contenant quelque chose (même des espaces blancs), puis supprime tout à la fin du tampon de la ligne suivante.

La troisième commande ( w) réécrit le fichier sur le disque.

La ligne vide ajoutée protège le reste du fichier contre la suppression dans le cas où il n'y a pas de lignes vides à la fin du fichier d'origine.

Kusalananda
la source
3

Voici une solution Perl qui ne nécessite pas de lire plus d'une ligne en mémoire à la fois:

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

ou, en une ligne:

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

Cela lit le fichier une ligne à la fois et vérifie chaque ligne pour voir si contient un caractère non-newline. Si ce n'est pas le cas, il incrémente un compteur; si c'est le cas, il imprime le nombre de nouvelles lignes indiqué par le compteur, suivi de la ligne elle-même, puis réinitialise le compteur.

Techniquement, même la mise en mémoire tampon d'une seule ligne en mémoire n'est pas nécessaire; il serait possible de résoudre ce problème en utilisant une quantité constante de mémoire en lisant le fichier en morceaux de longueur fixe et en le traitant caractère par caractère à l'aide d'une machine à états. Cependant, je soupçonne que ce serait inutilement compliqué pour le cas d'utilisation typique.

Ilmari Karonen
la source
1

Si votre fichier est suffisamment petit pour glisser en mémoire, vous pouvez utiliser ce

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

En python (je sais que ce n'est pas ce que vous voulez, mais c'est beaucoup mieux car il est optimisé, et un prélude à la version bash) sans réécrire le fichier et sans lire tout le fichier (ce qui est une bonne chose si le fichier est très grand):

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

Notez qu'il ne fonctionne pas sur les fichiers où le caractère EOL n'est pas '\ n'.

jfg956
la source
0

Une version bash, implémentant l'algorithme python, mais moins efficace car elle nécessite de nombreux processus:

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

Celui-ci est rapide à taper et, si vous connaissez sed, facile à retenir:

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

Il utilise le script sed pour supprimer les premières lignes vides des scripts d'une ligne utiles pour sed , référencés par Alexey, ci-dessus, et tac (reverse cat).

Dans un test rapide, sur un fichier de 18 Mo, 64 000 lignes, l'approche d'Alexey était plus rapide (0,036 contre 0,046 seconde).

freeB
la source