Comment obtenir la partie d'un fichier après la première ligne qui correspond à une expression régulière?


169

J'ai un fichier avec environ 1000 lignes. Je veux la partie de mon fichier après la ligne qui correspond à mon instruction grep.

C'est:

$ cat file | grep 'TERMINATE'     # It is found on line 534

Donc, je veux le fichier de la ligne 535 à la ligne 1000 pour un traitement ultérieur.

Comment puis je faire ça?


34
UUOC (Useless Use of cat):grep 'TERMINATE' file
Jacob

30
Je le sais, c'est comme si je l'utilisais de cette façon. Revenons à la question.
Yugal Jindle

3
C'est une question de programmation parfaitement fine, et bien adaptée au stackoverflow.
aioobe

13
@Jacob Ce n'est pas du tout une utilisation inutile du chat. Son utilisation est d'imprimer un fichier sur la sortie standard, ce qui signifie que nous pouvons utiliser l' grepinterface d'entrée standard pour lire les données, plutôt que d'avoir à apprendre à quel commutateur appliquer grep, et sed, et awk, et pandoc, ffmpegetc. lorsque nous voulons lire à partir d'un fichier. Cela fait gagner du temps car nous n'avons pas à apprendre un nouveau commutateur à chaque fois que nous voulons faire la même chose: lire à partir d'un fichier.
runeks

@runeks Je suis d' accord avec votre sentiment - mais vous pouvez y arriver sans chat: grep 'TERMINATE' < file. Peut-être que cela rend la lecture un peu plus difficile - mais c'est du script shell, donc ça va toujours être un problème :)
LOAS

Réponses:


307

Ce qui suit imprimera la ligne correspondant TERMINATEjusqu'à la fin du fichier:

sed -n -e '/TERMINATE/,$p'

Expliqué: -n désactive le comportement par défaut sedde l'impression de chaque ligne après avoir exécuté son script dessus, a -eindiqué un script à sed, /TERMINATE/,$est une sélection de plage d'adresses (ligne) signifiant la première ligne correspondant à l' TERMINATEexpression régulière (comme grep) à la fin du fichier ( $) , et pest la commande d'impression qui imprime la ligne courante.

Cela imprimera à partir de la ligne qui suit la ligne correspondant TERMINATEjusqu'à la fin du fichier:
(d'Après la ligne correspondante à EOF, sans inclure la ligne correspondante)

sed -e '1,/TERMINATE/d'

Expliqué: 1,/TERMINATE/ est une sélection de plage d'adresses (ligne) signifiant la première ligne pour l'entrée de la 1ère ligne correspondant à l' TERMINATEexpression régulière, et dest la commande de suppression qui supprime la ligne actuelle et passe à la ligne suivante. Le sedcomportement par défaut étant d'imprimer les lignes, il imprimera les lignes après TERMINATE la fin de l'entrée.

Éditer:

Si vous voulez les lignes avant TERMINATE:

sed -e '/TERMINATE/,$d'

Et si vous voulez les deux lignes avant et après TERMINATEdans 2 fichiers différents en un seul passage:

sed -e '1,/TERMINATE/w before
/TERMINATE/,$w after' file

Les fichiers avant et après contiendront la ligne avec terminate, donc pour traiter chacun d'entre eux, vous devez utiliser:

head -n -1 before
tail -n +2 after

Edit2:

SI vous ne souhaitez pas coder en dur les noms de fichiers dans le script sed, vous pouvez:

before=before.txt
after=after.txt
sed -e "1,/TERMINATE/w $before
/TERMINATE/,\$w $after" file

Mais alors vous devez échapper à la $signification de la dernière ligne pour que le shell n'essaie pas de développer la $wvariable (notez que nous utilisons maintenant des guillemets doubles autour du script au lieu de guillemets simples).

J'ai oublié de dire que la nouvelle ligne est importante après les noms de fichiers dans le script afin que sed sache que les noms de fichiers se terminent.


Edit: 2016-0530

Sébastien Clément a demandé: "Comment remplaceriez-vous le codé TERMINATEen dur par une variable?"

Vous créeriez une variable pour le texte correspondant, puis vous le feriez de la même manière que dans l'exemple précédent:

matchtext=TERMINATE
before=before.txt
after=after.txt
sed -e "1,/$matchtext/w $before
/$matchtext/,\$w $after" file

pour utiliser une variable pour le texte correspondant aux exemples précédents:

## Print the line containing the matching text, till the end of the file:
## (from the matching line to EOF, including the matching line)
matchtext=TERMINATE
sed -n -e "/$matchtext/,\$p"
## Print from the line that follows the line containing the 
## matching text, till the end of the file:
## (from AFTER the matching line to EOF, NOT including the matching line)
matchtext=TERMINATE
sed -e "1,/$matchtext/d"
## Print all the lines before the line containing the matching text:
## (from line-1 to BEFORE the matching line, NOT including the matching line)
matchtext=TERMINATE
sed -e "/$matchtext/,\$d"

Les points importants sur le remplacement du texte par des variables dans ces cas sont:

  1. Les variables ( $variablename) entre single quotes[ '] ne "se développeront" pas mais les variables à l'intérieur de double quotes[ "] le seront. Donc, vous devez changer tous les single quotesen double quotess'ils contiennent du texte que vous souhaitez remplacer par une variable.
  2. Les sedgammes contiennent également $et sont immédiatement suivies d'une lettre comme: $p, $d, $w. Ils examineront également comme des variables à être étendu, donc vous devez échapper à ces $personnages avec une barre oblique inverse [ \] comme: \$p, \$d, \$w.

Comment pouvons-nous obtenir les lignes avant TERMINATE et supprimer tout ce qui suit?
Yugal Jindle

Comment remplaceriez-vous le TERMINAL codé en dur par une variable?
Sébastien Clément

2
Un cas d'utilisation qui manque ici est de savoir comment imprimer les lignes après le dernier marqueur (s'il peut y en avoir plusieurs dans le fichier .. pensez aux fichiers journaux, etc.).
mato

L'exemple sed -e "1,/$matchtext/d"ne fonctionne pas lorsqu'il $matchtextse produit dans la première ligne. J'ai dû le changer en sed -e "0,/$matchtext/d".
Karalga

61

Comme simple approximation, vous pouvez utiliser

grep -A100000 TERMINATE file

qui greps pour TERMINATEet produit jusqu'à 100 000 lignes suivant cette ligne.

Depuis la page de manuel

-A NUM, --after-context=NUM

Imprimer NUM lignes de contexte de fin après les lignes correspondantes. Place une ligne contenant un séparateur de groupe (-) entre des groupes de correspondances contigus. Avec l'option -o ou --only-matching, cela n'a aucun effet et un avertissement est donné.


Cela pourrait fonctionner pour cela, mais je dois le coder dans mon script pour traiter de nombreux fichiers. Alors, montrez une solution générique.
Yugal Jindle

3
Je pense que c'est une solution pratique!
michelgotta

2
de même -B NUM, --before-context = NUM ​​Affiche NUM lignes de contexte principal avant les lignes correspondantes. Place une ligne contenant un séparateur de groupe (-) entre des groupes de correspondances contigus. Avec l'option -o ou --only-matching, cela n'a aucun effet et un avertissement est donné.
PiyusG

cette solution a fonctionné pour moi car je peux facilement utiliser des variables comme chaîne à vérifier.
Jose Martinez

3
Bonne idée! Si vous n'êtes pas sûr de la taille du contexte, vous pouvez compter les lignes de file:grep -A$(cat file | wc -l) TERMINATE file
Lemming

26

Un outil à utiliser ici est awk:

cat file | awk 'BEGIN{ found=0} /TERMINATE/{found=1}  {if (found) print }'

Comment cela marche-t-il:

  1. Nous définissons la variable 'found' à zéro, évaluant false
  2. si une correspondance pour 'TERMINATE' est trouvée avec l'expression régulière, nous la définissons sur un.
  3. Si notre variable `` trouvé '' donne la valeur True, imprimez :)

Les autres solutions peuvent consommer beaucoup de mémoire si vous les utilisez sur des fichiers très volumineux.


Simple, élégant et très générique. Dans mon cas, il imprimait tout jusqu'à la deuxième occurrence de '###':cat file | awk 'BEGIN{ found=0} /###/{found=found+1} {if (found<2) print }'
Aleksander Stelmaczonek

3
Un outil à ne pas utiliser ici est cat. awkest parfaitement capable de prendre un ou plusieurs noms de fichiers comme arguments. Voir aussi stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

9

Si je comprends bien votre question, vous voulez les lignes après TERMINATE , sans compter la TERMINATEligne. awkpeut le faire d'une manière simple:

awk '{if(found) print} /TERMINATE/{found=1}' your_file

Explication:

  1. Bien que ce ne soit pas la meilleure pratique, vous pouvez vous fier au fait que toutes les variables par défaut sont 0 ou la chaîne vide si elle n'est pas définie. Ainsi, la première expression ( if(found) print) n'imprimera rien pour commencer.
  2. Une fois l'impression terminée, nous vérifions s'il s'agit de la ligne de départ (qui ne doit pas être incluse).

Cela imprimera toutes les lignes après la TERMINATEligne.


Généralisation:

  • Vous avez un fichier avec début - et fin trapézoidale et vous voulez que les lignes entre les lignes à l' exception du début - et fin trapézoidale.
  • Les lignes de début et de fin peuvent être définies par une expression régulière correspondant à la ligne.

Exemple:

$ cat ex_file.txt 
not this line
second line
START
A good line to include
And this line
Yep
END
Nope more
...
never ever
$ awk '/END/{found=0} {if(found) print} /START/{found=1}' ex_file.txt 
A good line to include
And this line
Yep
$

Explication:

  1. Si la ligne de fin est trouvée, aucune impression ne doit être effectuée. Notez que cette vérification est effectuée avant l'impression réelle pour exclure la ligne de fin du résultat.
  2. Imprimez la ligne actuelle si foundest défini.
  3. Si la ligne de départ est trouvée, définissez-la de found=1sorte que les lignes suivantes soient imprimées. Notez que cette vérification est effectuée après l'impression réelle d'exclure le début -ligne du résultat.

Remarques:

  • Le code repose sur le fait que tous les awk-vars prennent par défaut la valeur 0 ou la chaîne vide si elle n'est pas définie. Ceci est valide mais peut ne pas être la meilleure pratique afin que vous puissiez ajouter un BEGIN{found=0}au début de l'expression awk.
  • Si plusieurs blocs début-fin sont trouvés, ils sont tous imprimés.

1
Exemple génial génial. Je viens de passer 2 heures à regarder csplit, sed et toutes sortes de commandes awk trop compliquées. Non seulement cela a fait ce que je voulais, mais cela a été assez simple pour déduire comment le modifier pour faire quelques autres choses connexes dont j'avais besoin. Cela me rappelle que awk est génial et pas seulement dans un désordre indéchiffrable de merde. Merci.
user1169420

{if(found) print}c'est un peu un anti-pattern dans awk, il est plus idiomatique de remplacer le bloc par juste foundou found;si vous avez besoin d'un autre filtre par la suite.
user000001

@ user000001 veuillez expliquer. Je ne comprends pas quoi remplacer et comment. Quoi qu'il en soit, je pense que la façon dont il est écrit montre très clairement ce qui se passe.
UlfR

1
Vous remplaceriez awk '{if(found) print} /TERMINATE/{found=1}' your_filepar awk 'found; /TERMINATE/{found=1}' your_file, ils devraient tous les deux faire la même chose.
user000001

7

Utilisez l'expansion des paramètres bash comme suit:

content=$(cat file)
echo "${content#*TERMINATE}"

Pouvez-vous expliquer ce que vous faites?
Yugal Jindle

J'ai copié le contenu de "file" dans la variable $ content. Ensuite, j'ai supprimé tous les caractères jusqu'à ce que "TERMINATE" soit vu. Il n'a pas utilisé de correspondance gourmande, mais vous pouvez utiliser une correspondance gourmande par $ {content ## * TERMINATE}.
Mu Qiao

voici le lien du manuel bash: gnu.org/software/bash/manual/…
Mu Qiao

6
que se passera-t-il si le fichier a une taille de 100 Go?
Znik

1
Downvote: C'est horrible (lire le fichier dans une variable) et faux (utiliser la variable sans la citer; et vous devez utiliser correctement printfou vous assurer de savoir exactement à quoi vous passez echo.).
tripleee

6

grep -A 10000000 fichier 'TERMINATE'

  • est beaucoup, beaucoup plus rapide que sed, en particulier sur des fichiers très volumineux. Il fonctionne jusqu'à 10 millions de lignes (ou tout ce que vous mettez), donc pas de mal à le rendre assez grand pour gérer tout ce que vous frappez.

4

Il existe de nombreuses façons de le faire avec sedou awk:

sed -n '/TERMINATE/,$p' file

Cela recherche TERMINATEdans votre fichier et imprime à partir de cette ligne jusqu'à la fin du fichier.

awk '/TERMINATE/,0' file

C'est exactement le même comportement que sed.

Si vous connaissez le numéro de la ligne à partir de laquelle vous souhaitez démarrer l'impression, vous pouvez le spécifier avec NR(numéro d'enregistrement, qui indique éventuellement le numéro de la ligne):

awk 'NR>=535' file

Exemple

$ seq 10 > a        #generate a file with one number per line, from 1 to 10
$ sed -n '/7/,$p' a
7
8
9
10
$ awk '/7/,0' a
7
8
9
10
$ awk 'NR>=7' a
7
8
9
10

Pour le numéro que vous pouvez également utilisermore +7 file
123

Cela inclut la ligne correspondante, ce qui n'est pas ce que l'on souhaite dans cette question.
mivk le

@mivk eh bien, c'est aussi le cas de la réponse acceptée et de la deuxième plus votée, donc le problème peut être avec un titre trompeur.
fedorqui 'SO arrêtez de nuire' le

3

Si pour une raison quelconque, vous souhaitez éviter d'utiliser sed, ce qui suit affichera la ligne correspondant TERMINATEjusqu'à la fin du fichier:

tail -n "+$(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)" file

et ce qui suit sera imprimé à partir de la ligne suivante correspondant TERMINATEjusqu'à la fin du fichier:

tail -n "+$(($(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)+1))" file

Il faut 2 processus pour faire ce que sed peut faire en un seul processus, et si le fichier change entre l'exécution de grep et tail, le résultat peut être incohérent, je recommande donc d'utiliser sed. De plus, si le fichier ne contient pas TERMINATE, la 1ère commande échoue.


le fichier est analysé deux fois. et si c'est une taille de 100 Go?
Znik du

1
Évalué parce que c'est une solution merdique, mais ensuite voté parce que 90% de la réponse est des mises en garde.
Mad Physicist


0

Cela pourrait être une façon de procéder. Si vous savez quelle ligne du fichier vous avez votre mot grep et combien de lignes vous avez dans votre fichier:

grep -A466 fichier 'TERMINATE'


1
Si le numéro de ligne est connu, il grepn'est même pas nécessaire; vous pouvez simplement utiliser tail -n $NUM, donc ce n'est pas vraiment une réponse.
Samveen

-1

sed est un bien meilleur outil pour le travail: fichier sed -n '/ re /, $ p'

où re est l'expression rationnelle.

Une autre option est l'indicateur --after-context de grep. Vous devez passer un nombre pour terminer à, l'utilisation de wc sur le fichier devrait donner la bonne valeur pour s'arrêter. Combinez cela avec -n et votre expression de correspondance.


--after-context est bien mais pas dans tous les cas.
Yugal Jindle

Pouvez-vous suggérer autre chose .. ??
Yugal Jindle

-2

Ceux-ci imprimeront toutes les lignes de la dernière ligne trouvée "TERMINATE" jusqu'à la fin du fichier:

LINE_NUMBER=`grep -o -n TERMINATE $OSCAM_LOG|tail -n 1|sed "s/:/ \\'/g"|awk -F" " '{print $1}'`
tail -n +$LINE_NUMBER $YOUR_FILE_NAME

Extraire un numéro de ligne avec greppour pouvoir le nourrir tailest un anti-modèle inutile. La recherche de la correspondance et l'impression jusqu'à la fin du fichier (ou, au contraire, l'impression et l'arrêt à la première correspondance) se font éminemment avec les outils de regex normaux et essentiels eux-mêmes. Le massif grep | tail | sed | awkest également en soi une utilisation inutile etgrep massive d' amis .
tripleee le

Je pense qu'il essayait de nous donner quelque chose qui trouverait la / dernière instance / de 'TERMINATE' et donnerait les lignes à partir de cette instance. D'autres implémentations vous donnent la première instance. Le LINE_NUMBER devrait probablement ressembler à ceci, à la place: LINE_NUMBER = $ (grep -o -n 'TERMINATE' $ OSCAM_LOG | tail -n 1 | awk -F: '{print $ 1}') Peut-être pas la manière la plus élégante, mais elle semble faire le travail. ^. ^
fbicknel

... ou tout sur une ligne, mais moche: tail -n + $ (grep -o -n 'TERMINATE' $ YOUR_FILE_NAME | tail -n 1 | awk -F: '{print $ 1}') $ YOUR_FILE_NAME
fbicknel

.... et j'allais revenir en arrière et modifier $ OSCAM_LOG au lieu de $ YOUR_FILE_NAME ... mais je ne peux pas pour une raison quelconque. Aucune idée d'où vient $ OSCAM_LOG; Je l'ai simplement reproduit sans réfléchir. oO
fbicknel

Faire cela dans Awk seul est une tâche courante dans Awk 101. Si vous utilisez déjà un outil plus performant juste pour obtenir le numéro de ligne, lâchez prise tailet faites la tâche dans l'outil le plus performant. Quoi qu'il en soit, le titre dit clairement "premier match".
tripleee le
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.