web-dev-qa-db-fra.com

Grep de la fin d'un fichier au début

J'ai un fichier avec environ 30 000 000 lignes (Radius Accounting) et j'ai besoin de trouver la dernière correspondance d'un modèle donné.

La commande:

tac accounting.log | grep $pattern

donne ce dont j'ai besoin, mais c'est trop lent car le système d'exploitation doit d'abord lire le fichier entier, puis l'envoyer au canal.

Donc, j'ai besoin de quelque chose de rapide qui puisse lire le fichier de la dernière ligne à la première.

45
Hábner Costa

tac n'aide que si vous utilisez également grep -m 1 (en supposant GNU grep) que grep s'arrête après la première correspondance:

tac accounting.log | grep -m 1 foo

De man grep:

   -m NUM, --max-count=NUM
          Stop reading a file after NUM matching lines.  

Dans l'exemple de votre question, tac et grep doivent traiter l'intégralité du fichier, donc l'utilisation de tac est un peu inutile.

Donc, sauf si vous utilisez grep -m, n'utilisez pas du tout tac, analysez simplement la sortie de grep pour obtenir la dernière correspondance:

grep foo accounting.log | tail -n 1 

Une autre approche consisterait à utiliser Perl ou tout autre langage de script. Par exemple (où $pattern=foo):

Perl -ne '$l=$_ if /foo/; END{print $l}' file

ou

awk '/foo/{k=$0}END{print k}' file
48
terdon

La raison pour laquelle

tac file | grep foo | head -n 1

ne s'arrête pas au premier match à cause de la mise en mémoire tampon.

Normalement, head -n 1 Se ferme après avoir lu une ligne. Donc grep devrait obtenir un SIGPIPE et quitter dès qu'il écrit sa deuxième ligne.

Mais ce qui se passe, c'est que parce que sa sortie ne va pas vers un terminal, grep le met en mémoire tampon. Autrement dit, il ne l'écrit que lorsqu'il s'est suffisamment accumulé (4096 octets dans mon test avec GNU grep).

Cela signifie que grep ne se fermera pas avant d'avoir écrit 8192 octets de données, donc probablement pas mal de lignes.

Avec GNU grep, vous pouvez le faire quitter plus tôt en utilisant --line-buffered Qui lui dit d'écrire des lignes dès qu'elles sont trouvées, qu'elles soient envoyées ou non à un terminal . Donc grep sortirait alors sur la deuxième ligne qu'il trouve.

Mais avec GNU grep de toute façon, vous pouvez utiliser -m 1 À la place comme l'a montré @terdon, ce qui est mieux car il se termine lors de la première correspondance.

Si votre grep n'est pas le GNU grep, vous pouvez utiliser sed ou awk à la place. Mais tac étant une commande GNU, je doute que vous trouverez un système avec tacgrep n'est pas GNU grep.

tac file | sed "/$pattern/!d;q"                             # BRE
tac file | P=$pattern awk '$0 ~ ENVIRON["P"] {print; exit}' # ERE

Certains systèmes ont tail -r Pour faire la même chose que GNU tac.

Notez que, pour les fichiers normaux (recherchables), tac et tail -r Sont efficaces car ils lisent les fichiers en arrière, ils ne lisent pas uniquement le fichier entièrement en mémoire avant de l'imprimer en arrière (comme l'approche sed de @ slm ou tac sur les fichiers non réguliers le ferait).

Sur les systèmes où ni tac ni tail -r Ne sont disponibles, les seules options sont d'implémenter la lecture en arrière à la main avec des langages de programmation comme Perl ou d'utiliser:

grep -e "$pattern" file | tail -n1

Ou:

sed "/$pattern/h;$!d;g" file

Mais cela signifie trouver toutes les correspondances et n'imprimer que la dernière.

12
Stéphane Chazelas

Voici une solution possible qui trouvera l'emplacement de la première occurrence du motif du dernier:

tac -s "$pattern" -r accounting.log | head -n 1

Cela utilise le -s et -r commutateurs de tac qui sont les suivants:

-s, --separator=STRING
use STRING as the separator instead of newline

-r, --regex
interpret the separator as a regular expression
4
mkc

Utilisation de sed

Voici quelques méthodes alternatives à @ Terdon's fine answer using sed:

$ sed '1!G;h;$!d' file | grep -m 1 $pattern
$ sed -n '1!G;h;$p' file | grep -m 1 $pattern

Exemples

$ seq 10 > file

$ sed '1!G;h;$!d' file | grep -m 1 5
5

$ sed -n '1!G;h;$p' file | grep -m 1 5
5

Utiliser Perl

En bonus, voici une notation un peu plus simple en Perl à retenir:

$ Perl -e 'print reverse <>' file | grep -m 1 $pattern

Exemple

$ Perl -e 'print reverse <>' file | grep -m 1 5
5
2
slm