Récupération du texte du dernier marqueur vers EOF dans POSIX.2


8

J'ai un texte avec des lignes de marqueur comme:

aaa
---
bbb
---
ccc

J'ai besoin d'obtenir un texte du dernier marqueur (non inclus) à EOF. Dans ce cas, il sera

ccc

Existe-t-il un moyen élégant dans POSIX.2? À l'heure actuelle, j'utilise deux exécutions: d'abord avec nlet greppour la dernière occurrence avec le numéro de ligne respectif. Ensuite, j'extrais le numéro de ligne et j'utilise sedpour extraire le morceau en question.

Les segments de texte peuvent être assez grands, donc j'ai peur d'utiliser une méthode d'ajout de texte comme nous ajoutons le texte à un tampon, si nous rencontrons le marqueur, nous vidons le tampon, de sorte qu'à EOF, nous avons notre dernier morceau dans le tampon.

Réponses:


6

À moins que vos segments ne soient vraiment énormes (comme dans: vous ne pouvez vraiment pas épargner autant de RAM, probablement parce que c'est un petit système embarqué contrôlant un grand système de fichiers), une seule passe est vraiment la meilleure approche. Non seulement parce que ce sera plus rapide, mais surtout parce qu'il permet à la source d'être un flux, à partir duquel toutes les données lues et non enregistrées sont perdues. C'est vraiment un travail pour awk, bien que sed puisse le faire aussi.

sed -n -e 's/^---$//' -e 't a' \
       -e 'H' -e '$g' -e '$s/^\n//' -e '$p' -e 'b' \
       -e ':a' -e 'h'              # you are not expected to understand this
awk '{if (/^---$/) {chunk=""}      # separator ==> start new chunk
      else {chunk=chunk $0 RS}}    # append line to chunk
     END {printf "%s", chunk}'     # print last chunk (without adding a newline)

Si vous devez utiliser une approche en deux passes, déterminez le décalage de ligne du dernier séparateur et imprimez à partir de cela. Ou déterminez le décalage en octets et imprimez à partir de cela.

</input/file tail -n +$((1 + $(</input/file         # print the last N lines, where N=…
                               grep -n -e '---' |   # list separator line numbers
                               tail -n 1 |          # take the last one
                               cut -d ':' -f 1) ))  # retain only line number
</input/file tail -n +$(</input/file awk '/^---$/ {n=NR+1} END {print n}')
</input/file tail -c +$(</input/file LC_CTYPE=C awk '
    {pos+=length($0 RS)}        # pos contains the current byte offset in the file
    /^---$/ {last=pos}          # last contains the byte offset after the last separator
    END {print last+1}          # print characters from last (+1 because tail counts from 1)
')

Addendum: Si vous avez plus de POSIX, voici une version simple en un seul passage qui s'appuie sur une extension commune à awk qui permet au séparateur d'enregistrement RSd'être une expression régulière (POSIX n'autorise qu'un seul caractère). Ce n'est pas tout à fait correct: si le fichier se termine par un séparateur d'enregistrements, il imprime le bloc avant le dernier séparateur d'enregistrements au lieu d'un enregistrement vide. La deuxième version utilisant RTévite ce défaut, mais RTest spécifique à GNU awk.

awk -vRS='(^|\n)---+($|\n)' 'END{printf $0}'
gawk -vRS='(^|\n)---+($|\n)' 'END{if (RT == "") printf $0}'

@ Gilles: sedfonctionne bien, mais je n'arrive pas à faire fonctionner l' awkexemple; il se bloque ... et j'obtiens une erreur dans le 3ème exemple: cut -f ':' -t 1 ... cut: option invalide - 't'
Peter.O

@ fred.bear: Je n'ai aucune idée de comment cela s'est produit - j'ai testé tous mes extraits de code, mais en quelque sorte gâché la modification post-copier-coller de l' cutexemple. Je ne vois rien de mal avec l' awkexemple, quelle version d'awk utilisez-vous et quelle est votre entrée de test?
Gilles 'SO- arrête d'être méchant'

... en fait, la awkversion fonctionne .. cela prend très longtemps sur un gros fichier .. la sedversion a traité le même fichier en 0.470s ... Mes données de test sont très pondérées ... seulement deux morceaux avec un seul '---' trois lignes à partir de la fin du million de lignes ...
Peter.O

@ Gilles .. (Je pense que je devrais arrêter de tester à 3 heures du matin. J'ai en quelque sorte testé les trois awks "à deux passes" comme une seule unité :( ... J'ai maintenant testé chacun individuellement et le second est très rapide à 0,204 secondes ... Howerver, le premier awk "deux passes" ne produit que: " (entrée standard) " (le -l semble être le coupable) ... comme pour le troisième awk "deux passes", je ne le fais pas ne produit rien ... mais le deuxième "deux passes" est la plus rapide de toutes les méthodes présentées (POSIX ou autre
:)

@ fred.bear: fixe et fixe. Mon QA n'est pas très bon pour ces courts extraits - je copie-colle généralement à partir d'une ligne de commande, formate, puis remarque un bogue et essaie de corriger en ligne plutôt que de reformater. Je suis curieux de voir si le comptage des caractères est plus efficace que le comptage des lignes (2e et 3e méthodes en deux passes).
Gilles 'SO- arrête d'être méchant'

3

Une stratégie en deux passes semble être la bonne chose. Au lieu de sed j'utiliserais awk(1). Les deux passes pourraient ressembler à ceci:

$ LINE=`awk '/^---$/{n=NR}END{print n}' file`

pour obtenir le numéro de ligne. Et puis faites écho à tout le texte à partir de ce numéro de ligne avec:

$ awk "NR>$LINE" file

Cela ne devrait pas nécessiter une mise en mémoire tampon excessive.


et ensuite ils peuvent être combinés:awk -v line=$(awk '/^---$/{n=NR}END{print n}' file) 'NR>line' file
glenn jackman

Voyant que j'ai testé les autres soumissions, j'ai également testé l'extrait ci-dessus "glen jackman's". Cela prend 0,352 seconde (avec le même fichier de données mentionné dans ma réponse) ... Je commence à recevoir le message que awk peut être plus rapide que je ne le pensais initialement possible (je pensais que sed était à peu près aussi bon qu'il l'a été, mais il semble être un cas de "chevaux pour les cours") ...
Peter.O

Très intéressant de voir tous ces scripts comparés. Beau travail Fred.
Mackie Messer

Les solutions les plus rapides utilisent tac et tail qui lisent réellement le fichier d'entrée à l'envers. Maintenant, si seulement awk pouvait lire le fichier d'entrée à l'envers ...
Mackie Messer

3
lnum=$(($(sed -n '/^---$/=' file | sed '$!d') +1)); sed -n "${lnum},$ p" file 

Les premiers sednuméros de ligne des sorties des lignes "---" ...
Le second sedextrait le dernier numéro de la sortie du premier sed ...
Ajoutez 1 à ce numéro pour obtenir le début de votre bloc "ccc" ...
Le troisième sorties 'sed' du début du bloc "ccc" à EOF

Mise à jour (avec des informations modifiées sur les méthodes de Gilles)

Eh bien, je me demandais comment Glenn Jackman tac se comporterait, alors j'ai testé les trois réponses (au moment de la rédaction) ... Le ou les fichiers de test contenaient chacun 1 million de lignes (de leurs propres numéros de ligne).
Toutes les réponses ont fait ce qui était attendu ...

Voici les temps ..


Gilles sed (passage unique)

# real    0m0.470s
# user    0m0.448s
# sys     0m0.020s

Gilles awk (passage unique)

# very slow, but my data had a very large data block which awk needed to cache.

Gilles en deux passes (première méthode)

# real    0m0.048s
# user    0m0.052s
# sys     0m0.008s

Gilles en deux passes (deuxième méthode) ... très rapide

# real    0m0.204s
# user    0m0.196s
# sys     0m0.008s

Gilles en deux passes (troisième méthode)

# real    0m0.774s
# user    0m0.688s
# sys     0m0.012s

Gilles 'gawk' (méthode RT) ... très rapide , mais ce n'est pas POSIX.

# real    0m0.221s
# user    0m0.200s
# sys     0m0.020s

glenn jackman ... très rapide , mais ce n'est pas POSIX.

# real    0m0.022s
# user    0m0.000s
# sys     0m0.036s

fred.bear

# real    0m0.464s
# user    0m0.432s
# sys     0m0.052s

Mackie Messer

# real    0m0.856s
# user    0m0.832s
# sys     0m0.028s

Par curiosité, laquelle de mes versions à deux passes avez-vous testée et quelle version d'awk avez-vous utilisée?
Gilles 'SO- arrête d'être méchant'

@ Gilles: J'ai utilisé GNU Awk 3.1.6 (dans Ubuntu 10.04 avec 4 Go de RAM). Tous les tests ont 1 million de lignes dans le premier "bloc", puis un "marqueur" suivi de 2 lignes "données" ... Il a fallu 15,540 secondes pour traiter un fichier plus petit de 100 000 lignes, mais pour les 1 000 000 lignes, je suis maintenant, et cela fait plus de 25 minutes jusqu'à présent. Il utilise un cœur à 100% ... le tue maintenant ... Voici quelques tests supplémentaires: lignes = 100000 (0m16.026s) - lignes = 200000 (2m29.990s) - lignes = 300000 (5m23. 393s) - lignes = 400000 (11m9.938s)
Peter.O

Oups .. Dans mon commentaire ci-dessus, j'ai raté votre référence awk "deux passes". Le détail ci-dessus est pour le awk "single-pass" ... La version awk est correcte ... J'ai fait d'autres commentaires sur les différentes versions "two-pass" sous votre réponse (une modification des résultats de temps ci-dessus)
Peter.O


0

Vous pouvez simplement utiliser ed

ed -s infile <<\IN
.t.
1,?===?d
$d
,p
q
IN

Comment ça marche: tduplique la ligne courante ( .) - qui est toujours la dernière ligne au eddémarrage (juste au cas où le délimiteur est présent sur la dernière ligne), 1,?===?dsupprime toutes les lignes jusqu'à et incluant la correspondance précédente ( edest toujours sur la dernière ligne ) $dsupprime ensuite la dernière ligne (en double), ,pimprime le tampon de texte (remplacez par wpour éditer le fichier en place) et qquitte finalement ed.


Si vous savez qu'il y a au moins un délimiteur dans l'entrée (et ne vous souciez pas s'il est également imprimé), alors

sed 'H;/===/h;$!d;x' infile

serait le plus court.
Fonctionnement: il ajoute toutes les lignes à l' Hancien tampon, il écrase l' hancien tampon lorsqu'il rencontre une correspondance, il dsupprime toutes les lignes sauf la dernière $lorsqu'il xchange de tampon (et s'imprime automatiquement).

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.