web-dev-qa-db-fra.com

Awk commande pour imprimer toutes les lignes sauf les trois dernières lignes

Je veux imprimer toutes les lignes sauf les trois dernières lignes de l'entrée via awk uniquement. Veuillez noter que mon fichier contient n nombre de lignes.

Par exemple,

file.txt contient,

foo
bar
foobar
barfoo
last
line

Je veux que la sortie soit,

foo
bar
foobar

Je sais que cela pourrait être possible grâce à la combinaison de tac et sed ou tac et awk

$ tac file | sed '1,3d' | tac
foo
bar
foobar

$ tac file | awk 'NR==1{next}NR==2{next}NR==3{next}1' | tac
foo
bar
foobar

Mais je veux que la sortie par awk seulement.

5
Avinash Raj

C'est toujours aussi maladroit, mais vous pouvez ajouter chaque ligne à un tableau et à la fin - quand vous connaissez la longueur - afficher tout, sauf les 3 dernières lignes.

... | awk '{l[NR] = $0} END {for (i=1; i<=NR-3; i++) print l[i]}'

Une autre approche (plus efficace ici ) consiste à empiler manuellement trois variables:

... | awk '{if (a) print a; a=b; b=c; c=$0}'

a n’imprime qu’après qu’une ligne a été déplacée de c à b, puis à a, ce qui la limite à trois lignes. Les avantages immédiats sont qu'il ne stocke pas tout le contenu en mémoire et qu'il ne devrait pas causer de problèmes de mise en mémoire tampon (fflush() après l'impression si c'est le cas), mais l'inconvénient est que ce n'est pas simple. Si vous voulez ignorer les 100 dernières lignes, vous avez besoin de 100 variables et de 100 variables.

Si awk avait les opérateurs Push et pop pour les tableaux, ce serait plus simple.

Ou nous pourrions pré-calculer le nombre de lignes et jusqu'où nous voulons aller avec $(($(wc -l < file) - 3)). Ceci est relativement inutile pour le contenu en streaming mais sur un fichier, fonctionne plutôt bien:

awk -v n=$(($(wc -l < file) - 3)) 'NR<n' file

En règle générale, vous utiliseriez simplement head si:

$ seq 6 | head -n-3
1
2
3

En utilisant repère de terdon , nous pouvons réellement voir comment ils se comparent. Je pensais cependant offrir une comparaison complète:

  • head: 0.018s (moi)
  • awk + wc: 0.169s (moi)
  • awk 3 variables: 0,178s (moi)
  • awk double-fichier: 0.322s (terdon)
  • awk buffer circulaire: 0.355s (vérificateur)
  • awk for-loop: 0.693s (me)

La solution la plus rapide utilise un utilitaire C-optimisé comme head ou wc pour gérer les tâches lourdes, mais en pure awk, la pile en rotation manuelle est roi pour le moment .

16
Oli

Pour une utilisation minimale de la mémoire, vous pouvez utiliser un tampon circulaire:

awk 'NR>n{print A[NR%n]} {A[NR%n]=$0}' n=3 file

En utilisant l'opérateur mod sur les numéros de ligne, nous avons au plus n entrées de tableau.

Prenons l'exemple de n = 3:

Sur la ligne 1 NR%n est égal à 1, la ligne 2 en produit 2, la ligne 3 en indiquant 0 et la ligne 4 de nouveau à 1.

Line 1 -> A[1]
Line 2 -> A[2]
Line 3 -> A[0]
Line 4 -> A[1]
Line 5 -> A[2]
...

Quand nous arrivons à la ligne 4, A[NR%n] contient le contenu de la ligne 1. Donc, cela s’imprime et A[NR%n] obtient le contenu de la ligne 4. La ligne suivante (ligne 5) affiche le contenu original de la ligne 2, et ainsi de suite, jusqu’à ce que nous obtenions jusqu'à la fin. Ce qui reste non imprimé, c'est le contenu du tampon, qui contient les 3 dernières lignes ...

5
Scrutinizer

Vous pouvez également traiter le fichier deux fois pour éviter de conserver rien en mémoire:

awk '{if(NR==FNR){c++}else if(FNR<=c-3){print}}' file file

L'astuce ici est le test NR==FNR. NR est le numéro de ligne actuel et FNR est le numéro de ligne actuel du fichier en cours. Si plusieurs fichiers sont passés en entrée, FNR sera égal à NR uniquement pendant le traitement du premier fichier. De cette façon, nous obtenons rapidement le nombre de lignes dans le premier fichier et l'enregistrons sous le nom c. Puisque les "deux" fichiers sont en réalité le même, nous connaissons maintenant le nombre de lignes que nous voulons et nous n'imprimons que s'il s'agit de l'une d'entre elles.

Vous pensez peut-être que cela sera plus lent que les autres approches, mais en réalité, il est plus rapide car il n’ya pratiquement aucun traitement en cours. Tout est fait en utilisant les outils internes awk (NR et FNR) en dehors d'une simple comparaison arithmétique. J'ai testé sur un fichier de 50 Mo avec un million de lignes créées avec cette commande:

for i in {500000..1000000}; do 
    echo "The quick brown fox jumped over the lazy dog $i" >> file; 
done

Comme vous pouvez le constater, les temps sont presque identiques, mais l'approche que j'ai proposée ici est légèrement plus rapide que la première suggestion d'Oli (mais plus lente que les autres):

$ for i in {1..10}; do ( 
    time awk '{if(NR==FNR){c++}else if(FNR<=c-3){print}}' file file > /dev/null ) 2>&1 | 
       grep -oP 'real.*?m\K[\d\.]+'; 
  done | awk '{k+=$1}END{print k/10" seconds"}'; 
0.4757 seconds

$  for i in {1..10}; do ( 
    time awk '{l[NR] = $0} END {for (i=1; i<=NR-3; i++) print l[i]}' file > /dev/null ) 2>&1 | 
        grep -oP 'real.*?m\K[\d\.]+'; 
   done | awk '{k+=$1}END{print k/10" seconds"}'; 
0.5347 seconds
2
terdon

Je sais que la question portait spécifiquement sur awk, mais par souci de brièveté, on pourrait toujours utiliser:

head -n -3
0
sjas