web-dev-qa-db-fra.com

Comment grep-invert-match et exclure les lignes "avant" et "après"

Considérez un fichier texte avec les entrées suivantes:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Étant donné un modèle (par exemple fff), je voudrais grep le fichier ci-dessus pour obtenir la sortie:

all_lines except (pattern_matching_lines  U (B lines_before) U (A lines_after))

Par exemple, si B = 2 et A = 1, la sortie avec pattern = fff doit être:

aaa
bbb
ccc
hhh
iii

Comment puis-je le faire avec grep ou d'autres outils de ligne de commande?


Remarque, lorsque j'essaie:

grep -v 'fff'  -A1 -B2 file.txt

Je n'obtiens pas ce que je veux. Je reçois à la place:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii
27

don peut être mieux dans la plupart des cas, mais juste au cas où le fichier est vraiment gros, et vous ne pouvez pas obtenir sed pour gérer un fichier de script de cette taille (qui peut se produire à environ 5000+ lignes de script), le voici en clair sed:

sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Ceci est un exemple de ce qu'on appelle un fenêtre coulissante en entrée. Cela fonctionne en construisant un look-ahead tampon de $B - comptez les lignes avant de tenter d'imprimer quoi que ce soit.

Et en fait, je devrais probablement clarifier mon point précédent: le principal limiteur de performances pour cette solution et pour Don sera directement lié à l'intervalle. Cette solution ralentira avec un plus grand intervalle tailles, tandis que celle de Don ralentira avec un plus grand intervalle fréquences. En d'autres termes, même si le fichier d'entrée est très volumineux, si l'occurrence réelle de l'intervalle est encore très peu fréquente, sa solution est probablement la voie à suivre. Cependant, si la taille de l'intervalle est relativement gérable et est susceptible de se produire souvent, c'est la solution que vous devez choisir.

Voici donc le flux de travail:

  • Si $match Se trouve dans l'espace modèle précédé d'une ligne électronique \n, sed supprimera récursivement D chaque ligne électronique \n Qui la précède.
    • J'effaçais complètement l'espace de motif de $match Auparavant - mais pour gérer facilement les chevauchements, laisser un point de repère semble fonctionner beaucoup mieux.
    • J'ai également essayé s/.*\n.*\($match\)/\1/ pour essayer de l'obtenir en une seule fois et d'esquiver la boucle, mais lorsque $A/$B Sont grands, la boucle elete D s'avère beaucoup plus rapide.
  • Ensuite, nous tirons la ligne d'entrée N ext précédée d'un délimiteur de ligne électronique \n Et essayons à nouveau de D supprimer un /\n.*$match/ En faisant référence à notre expression régulière la plus récemment utilisée avec //.
  • Si l'espace de motif correspond à $match, Il ne peut le faire qu'avec $match En tête de ligne - toutes les lignes $B Antérieures ont été effacées.
    • Nous commençons donc à boucler sur $A Après.
    • À chaque exécution de cette boucle, nous essaierons de s/// Remplacer par & Lui-même le $A Ème \n Caractère en ligne dans l'espace modèle et, en cas de succès, t est nous ramènera - et tout notre tampon $A Fter - entièrement hors du script pour recommencer le script par le haut avec la ligne d'entrée suivante le cas échéant.
    • Si l'est t ne réussit pas, nous ranchons b vers l'étiquette d'opération :t Et reconsidérons pour une autre ligne d'entrée - en commençant éventuellement la boucle si $match Se produit lors de la collecte de $A Fter.
  • Si nous dépassons une boucle de fonction $match, Alors nous essaierons de p imprimer la dernière ligne $ Si c'est le cas, et si ! Pas essayez de s/// remplacer par & lui-même le $B ème \n caractère de ligne dans l'espace de motif.
    • Nous allons t également, et si cela réussit, nous allons nous connecter au label de rint :P.
    • Si ce n'est pas le cas, nous allons revenir à :t Op et obtenir une autre ligne d'entrée ajoutée au tampon.
  • Si nous arrivons à :P Rint, nous P rint puis D éliminons jusqu'au premier \n Ewline dans l'espace modèle et réexécutez le script à partir de la top avec ce qui reste.

Et donc cette fois, si nous faisions A=2 B=2 match=5; seq 5 | sed...

L'espace de motif pour la première itération à :P Rint ressemblerait à:

^1\n2\n3$

Et c'est ainsi que sed rassemble son $B Avant tampon. Et donc sed s'imprime pour afficher $B - comptez les lignes derrière l'entrée qu'il a collectée. Cela signifie que, dans notre exemple précédent, sed serait P rint 1 pour sortir, puis D supprimer cela et renvoyer en haut du script un espace modèle qui ressemble à:

^2\n3$

... et en haut du script la ligne d'entrée ext N est récupérée et donc la prochaine itération ressemble à:

^2\n3\n4$

Et donc quand nous trouvons la première occurrence de 5 En entrée, l'espace de motif ressemble en fait à:

^3\n4\n5$

Ensuite, la boucle elete D entre en action et lorsqu'elle est terminée, elle ressemble à ceci:

^5$

Et lorsque la ligne d'entrée ext N est tirée sed frappe EOF et se ferme. À ce moment-là, il n'y a plus que P lignes imprimées 1 et 2.

Voici un exemple d'exécution:

A=8 B=7 match='[24689]0'
seq 100 |
sed -ne:t -e"/\n.*$match/D" \
    -e'$!N;//D;/'"$match/{" \
            -e"s/\n/&/$A;t" \
            -e'$q;bt' -e\}  \
    -e's/\n/&/'"$B;tP"      \
    -e'$!bt' -e:P  -e'P;D'

Cela imprime:

1
2
3
4
5
6
7
8
9
10
11
12
29
30
31
32
49
50
51
52
69
70
71
72
99
100
9
mikeserv

Vous pouvez utiliser gnu grep avec -A et -B pour imprimer exactement les parties du fichier que vous souhaitez exclure mais ajoutez le -n basculez pour imprimer également les numéros de ligne, puis formatez la sortie et passez-la en tant que script de commande à sed pour supprimer ces lignes:

grep -n -A1 -B2 PATTERN infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Cela devrait également fonctionner avec les fichiers de modèles passés à grep via -f par exemple.:

grep -n -A1 -B2 -f patterns infile | \
sed -n 's/^\([0-9]\{1,\}\).*/\1d/p' | \
sed -f - infile

Je pense que cela pourrait être légèrement optimisé s'il réduisait au moins trois numéros de ligne consécutifs en plages de manière à avoir par exemple 2,6d au lieu de 2d;3d;4d;5d;6d... cependant si l'entrée n'a que quelques correspondances, cela ne vaut pas la peine de le faire.


Autres moyens qui ne préservent pas l'ordre des lignes et sont probablement plus lents:
avec comm:

comm -13 <(grep PATTERN -A1 -B2 <(nl -ba -nrz -s: infile) | sort) \
<(nl -ba -nrz -s: infile | sort) | cut -d: -f2-

comm nécessite une entrée triée, ce qui signifie que l'ordre des lignes ne sera pas conservé dans la sortie finale (sauf si votre fichier est déjà trié) donc nl est utilisé pour numéroter les lignes avant le tri, comm -13 imprime uniquement les lignes propres à 2nd FILE puis cut supprime la partie ajoutée par nl (c'est-à-dire le premier champ et le délimiteur :)
avec join:

join -t: -j1 -v1 <(nl -ba -nrz -s:  infile | sort) \
<(grep PATTERN -A1 -B2 <(nl -ba -nrz -s:  infile) | sort) | cut -d: -f2-
11
don_crissti

Si cela ne vous dérange pas d'utiliser vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nes active le mode ex silencieux non compatible. Utile pour l'écriture de scripts.
  • +{command} dire à vim d'exécuter {command} sur le fichier.
  • g/${PAT}/ - sur toutes les lignes correspondant à /fff/. Cela devient difficile si le modèle contient des caractères spéciaux d'expression régulière que vous n'aviez pas l'intention de traiter de cette façon.
  • .-${B} - à partir d'une ligne au-dessus de celle-ci
  • .+${A} - à 2 lignes en dessous de celle-ci (voir :he cmdline-ranges pour ces deux)
  • d - supprime les lignes.
  • +w !tee écrit ensuite sur la sortie standard.
  • +q! se ferme sans enregistrer les modifications.

Vous pouvez ignorer les variables et utiliser directement le modèle et les nombres. Je les ai utilisés juste pour des raisons de clarté.

9
muru

Que diriez-vous (en utilisant GNU grep et bash):

$ grep -vFf - file.txt < <(grep -B2 -A1 'fff' file.txt)
aaa
bbb
ccc
hhh
iii

Nous trouvons ici les lignes à supprimer par grep -B2 -A1 'fff' file.txt, puis en l'utilisant comme fichier d'entrée pour trouver les lignes souhaitées en les rejetant.

4
heemayl

Vous pouvez obtenir un résultat satisfaisant en utilisant des fichiers temporaires:

my_file=file.txt #or =$1 if in a script

#create a file with all the lines to discard, numbered
grep -n -B1 -A5 TBD "$my_file" |cut -d\  -f1|tr -d ':-'|sort > /tmp/___"$my_file"_unpair

#number all the lines
nl -nln "$my_file"|cut -d\  -f1|tr -d ':-'|sort >  /tmp/___"$my_file"_all

#join the two, creating a file with the numbers of all the lines to keep
#i.e. of those _not_ found in the "unpair" file
join -v2  /tmp/___"$my_file"_unpair /tmp/___"$my_file"_all|sort -n > /tmp/___"$my_file"_lines_to_keep

#eventually use these line numbers to extract lines from the original file
nl -nln $my_file|join - /tmp/___"$my_file"_lines_to_keep |cut -d\  -f2- > "$my_file"_clean

Le résultat est assez bien parce que vous pouvez perdre une indentation dans le processus, mais s'il s'agit d'un fichier xml ou insensible à l'indentation, cela ne devrait pas poser de problème. Étant donné que ce script utilise un lecteur RAM, l'écriture et la lecture de ces fichiers temporaires sont aussi rapides que de travailler en mémoire.

1
RafDouglas

De plus, si vous souhaitez simplement exclure certaines lignes avant un marqueur donné, vous pouvez utiliser:

awk -v nlines=2 '/Exception/ {for (i=0; i<nlines; i++) {getline}; next} 1'

(Glenn Jackman sur https://stackoverflow.com/a/1492538 )

En canalisant certaines commandes, vous pouvez obtenir le comportement avant/après:

awk -v nlines_after=5 '/EXCEPTION/ {for (i=0; i<nlines_after; i++) {getline};print "EXCEPTION" ;next} 1' filename.txt|\
tac|\
awk -v nlines_before=1 '/EXCEPTION/ {for (i=0; i<nlines_before; i++) {getline}; next} 1'|\
tac
1
RafDouglas

S'il n'y a qu'un seul match:

A=1; B=2; n=$(grep -n 'fff' file.txt | cut -d: -f1)
head -n $((n-B-1)) file.txt ; tail -n +$((n+A+1)) file.txt

Sinon (awk):

# -vA=a -vB=b -vpattern=pat must be provided
BEGIN{

    # add file again. assume single file
    ARGV[ARGC]=ARGV[ARGC-1]
    ++ARGC
}

# the same as grep -An -Bn pattern
FNR==NR && $0 ~ pattern{
    for (i = 0; i <= B; ++i)
        a[NR-i]++
    for (i = 1; i <= A; ++i)
        a[NR+i]++
}

FNR!=NR && !(FNR in a)
0
dedowsdi

Une façon d'accomplir cela, peut-être la manière la plus simple serait de créer une variable et de faire ce qui suit:

grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt

De cette façon, vous avez toujours votre structure. Et vous pouvez facilement voir de la doublure que vous essayez de supprimer.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii
0
lordpavel