Commande pour afficher les premières et dernières lignes d'un fichier

23

J'ai un fichier avec plusieurs lignes et chaque ligne a un horodatage au début, comme

[Thread-3] (21/09/12 06:17:38:672) logged message from code.....

Donc, je vérifie fréquemment 2 choses dans ce fichier journal.

  1. Les premières lignes, qui ont les conditions globales et l'heure de début sont également indiquées.
  2. Dernières lignes, qui ont le statut de sortie avec quelques autres informations.

Existe-t-il une seule commande rapide et pratique qui pourrait me permettre d'afficher uniquement les premières et dernières lignes d'un fichier?

mtk
la source
2
Quelles sont les conditions mondiales et ne head and tailfonctionnent pas pour vous?
daisy
C'est la partie de mon fichier journal. J'essayais d'être élaboratif. Vous pouvez ignorer cela.
mtk
Votre solution me convient parfaitement. Si vous voulez plus de commodité, faites-en une fonction shell (même un alias pourrait le faire).
vonbrand
@vonbrand Le problème est que je ne sais pasN
Bernhard
@Bernhard, je ne suis pas un sed(1)expert, mais il existe des moyens de ranger des trucs pour une utilisation ultérieure avec. Peut-être que cela vaut la peine de regarder là-dedans. OTOH, je préparerais probablement un script Perl (ou autre) pour le faire s'il est utilisé fréquemment, car je suis plus familier avec cela.
vonbrand

Réponses:

12

Vous pouvez utiliser sedou awkpour le faire avec une seule commande. Cependant, vous perdrez de la vitesse, de la cause sedet awkdevrez de toute façon parcourir tout le fichier. Du point de vue de la vitesse, il est préférable de créer une fonction ou à chaque fois de combiner tail+ head. Cela a l'inconvénient de ne pas fonctionner si l'entrée est un canal, mais vous pouvez utiliser la substitution de processus, au cas où votre shell la prend en charge (regardez l'exemple ci-dessous).

first_last () {
    head -n 10 -- "$1"
    tail -n 10 -- "$1"
}

et lancez-le comme

first_last "/path/to/file_to_process"

pour procéder à la substitution de processus (bash, zsh, ksh comme les shells uniquement):

first_last <( command )

ps. vous pouvez même ajouter un greppour vérifier si vos "conditions globales" existent.

se ruer
la source
-n 10est la valeur par défaut, non?
l0b0
@ l0b0 oui, c'est par défaut. -n 10n'est pas nécessaire ici.
rush
20

@rush a raison d'utiliser plus efficacement head + tail pour les gros fichiers, mais pour les petits fichiers (<20 lignes), certaines lignes peuvent être sorties deux fois.

{ head; tail;} < /path/to/file

serait tout aussi efficace, mais n'aurait pas le problème ci-dessus.

Stéphane Chazelas
la source
Contrairement à la solution Rushs, cela ne fonctionne pas dans un shell POSIX.
Marco
2
@Marco Huh? Seules les constructions POSIX sont utilisées ici. Que voyez-vous qui ne va pas?
Gilles 'SO- arrête d'être méchant'
2
@Gilles J'ai raté l'espace: {head; tail;} < filefonctionne en zsh mais échoue en sh. { head; tail;} < filefonctionne toujours. Désolé pour le bruit.
Marco
@Marco, s'il y avait des problèmes avec ça, ce serait avec head, pas avec le shell. POSIX nécessite headde laisser le curseur dans le fichier juste après ces 10 lignes pour les fichiers normaux. Un problème pourrait survenir pour les headimplémentations non-POSIX (les très anciennes versions de GNU head étaient autrefois non conformes dans ce cas, mais nous parlons de décennies) ou si le fichier n'est pas recherchable (comme un tube ou socket nommé, mais une autre solution aurait le même problème).
Stéphane Chazelas
1
@FCTW,sudo sh -c '{ head; tail;} < /path/to/file'
Stéphane Chazelas
9

La { head; tail; }solution ne fonctionnerait pas sur les canaux (ou sockets ou tout autre fichier non recherchable) car headpourrait consommer trop de données lors de la lecture par blocs et ne peut pas rechercher sur un canal laissant potentiellement le curseur à l'intérieur du fichier au-delà de ce qui tailest voulu pour sélectionner.

Ainsi, vous pouvez utiliser un outil qui lit un caractère à la fois comme celui du shell read(ici en utilisant une fonction qui prend le nombre de lignes de tête et de queue comme arguments).

head_tail() {
  n=0
  while [ "$n" -lt "$1" ]; do
    IFS= read -r line || { printf %s "$line"; break; }
    printf '%s\n' "$line"
    n=$(($n + 1))
  done
  tail -n "${2-$1}"
}
seq 100 | head_tail 5 10
seq 20 | head_tail 5

ou implémenter taildans awk par exemple comme:

head_tail() {
  awk -v h="$1" -v t="${2-$1}" '
    {l[NR%t]=$0}
    NR<=h
    END{
      n=NR-t+1
      if(n <= h) n = h+1
      for (;n<=NR;n++) print l[n%t]
    }'
}

Avec sed:

head_tail() {
  sed -e "1,${1}b" -e :1 -e "$(($1+${2-$1})),\$!{N;b1" -e '}' -e 'N;D'
}

(mais sachez que certaines sedimplémentations ont une faible limitation sur la taille de leur espace de motif, donc échoueraient pour de grandes valeurs du nombre de lignes de queue).

Stéphane Chazelas
la source
4

En utilisant la bashsubstitution de processus, vous pouvez effectuer les opérations suivantes:

make_some_output | tee >(tail -n 2) >(head -n 2; cat >/dev/null) >/dev/null

Notez que les lignes ne sont pas garanties d'être en ordre, bien que pour les fichiers plus longs qu'environ 8 Ko, elles le seront très probablement. Cette coupure de 8 Ko est la taille typique du tampon de lecture et est liée au fait que | {head; tail;}cela ne fonctionne pas pour les petits fichiers.

Il cat >/dev/nullest nécessaire de maintenir le headpipeline en vie. Sinon tee, vous quitterez tôt, et même si vous obtiendrez une sortie de tail, ce sera quelque part au milieu de l'entrée, plutôt qu'à la fin.

Enfin, pourquoi au >/dev/nulllieu de, disons, passer tailà un autre |? Dans le cas suivant:

make_some_output | tee >(head -n 2; cat >/dev/null) | tail -n 2  # doesn't work

headstdout est injecté dans le tuyau tailplutôt que dans la console, ce qui n'est pas du tout ce que nous voulons.

Jander
la source
Lorsque la tête ou la queue ont fini d'écrire la sortie souhaitée, ils ferment leur stdin et quittent. C'est de là que vient le SIGPIPE. Normalement, c'est une bonne chose, ils rejettent le reste de la sortie, il n'y a donc aucune raison pour que l'autre côté du tuyau continue à passer du temps à le générer.
derobert
Qu'est-ce qui rend l'ordonnance susceptible d'être maintenue? Ce sera probablement pour un gros fichier, car il taildoit travailler plus longtemps, mais je m'attends (et le vois) à un échec environ la moitié du temps pour les entrées courtes.
Gilles 'SO- arrête d'être méchant'
Vous obtiendrez le SIGPIPE tee >(head) >(tail)pour les mêmes raisons ( >(...)qui est d'ailleurs une fonctionnalité ksh désormais prise en charge par zsh et bash) utilise également des tuyaux. Vous pouvez le faire, ... | (trap '' PIPE; tee >(head) >(tail) > /dev/null)mais vous verrez toujours des messages d'erreur de tuyaux casséstee .
Stéphane Chazelas
Sur mon système (bash 4.2.37, coreutils 8.13), tailest celui qui est tué par SIGPIPE, non tee, et n'écrit tailpas dans un pipe. Donc ça doit venir d'un kill(), non? Et cela ne se produit que lorsque j'utilise la |syntaxe. stracedit que ça teen'appelle pas kill()... alors peut bash- être ?
Jander
1
@Jander, essayez de nourrir plus de 8k commeseq 100000 | tee >(head -n1) >(tail -n1) > /dev/null
Stéphane Chazelas
3

Utilisation ed(qui lira cependant le fichier entier dans la RAM):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' 'H' '1,10p' '$-10,$p' 'q' | ed -s file
curx
la source
Plus court:ed -s file <<< $'11,$-10d\n,p\nq\n'
don_crissti
2

Première solution de Stéphane dans une fonction pour que vous puissiez utiliser des arguments (fonctionne dans n'importe quel shell Bourne-like ou POSIX):

head_tail() {
    head "$@";
    tail "$@";
}

Vous pouvez maintenant le faire:

head_tail -n 5 < /path/to/file

Cela suppose bien sûr que vous ne regardez qu'un seul fichier et que la solution de Stéphane ne fonctionne (de manière fiable) que sur des fichiers standard (recherchables).

l0b0
la source
2

Avec l' option -u( --unbuffered) de GNU sed, vous pouvez utiliser sed -u 2qcomme alternative non tamponnée à head -n2:

$ seq 100|(sed -u 2q;tail -n2)
1
2
99
100

(head -n2;tail -n2)échoue lorsque les dernières lignes font partie du bloc de l'entrée consommé par head:

$ seq 1000|(head -n2;tail -n2)
1
2
999
1000
$ seq 100|(head -n2;tail -n2)
1
2
nisetama
la source
cela devrait être la meilleure réponse! fonctionne comme un charme!
Ben Usman
1

Je suis tombé sur quelque chose comme ça aujourd'hui où je n'avais besoin que de la dernière ligne et de quelques lignes de l'avant d'un flux et j'ai trouvé ce qui suit.

sed -n -e '1{h}' -e '2,3{H}' -e '${H;x;p}'

Je lis ceci comme: initialiser l'espace d'attente avec le contenu de la première ligne, ajouter les lignes 2-3 dans l'espace d'attente, à EOF ajouter la dernière ligne à l'espace d'attente, échanger l'espace d'attente et de motif et imprimer le motif espace.

Peut-être quelqu'un avec plus sed-FU que moi peut comprendre comment généraliser cette option pour imprimer les dernières quelques lignes du flux indiqués dans cette question , mais je n'ai pas besoin et ne pouvait pas trouver un moyen facile de faire des mathématiques en fonction de l' $adresse dans sedou peut-être en gérant l'espace d'attente de sorte que seules les dernières lignes soient dedans quand il EOFest atteint.

deaks
la source
1

Vous pouvez essayer Perl, si vous l'avez installé:

perl -e '@_ = <>; @_=@_[0, -3..-1]; print @_'

Cela fonctionnera pour la plupart des fichiers, mais lit tout le fichier en mémoire avant de le traiter. Si vous n'êtes pas familier avec les tranches Perl, "0" entre crochets signifie "prenez la première ligne" et "-3 ...- 1" signifie "prenez les trois dernières lignes". Vous pouvez les adapter tous les deux à vos besoins. Si vous avez besoin de traiter des fichiers vraiment volumineux (ce qui est «gros» peut dépendre de votre RAM et peut-être changer de taille), vous pouvez opter pour:

perl -e 'while($_=<>){@_=(@_,$_)[0,-3..-1]}; print @_'

il peut être un peu plus lent, car il fait une tranche à chaque itération, mais il est indépendant de la taille du fichier.

Les deux commandes devraient fonctionner à la fois dans les canaux et avec les fichiers normaux.

Jasio
la source