Comment grep-inverse-match et exclure les lignes «avant» et «après»

26

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 = 2et A = 1, la sortie avec pattern = fffdevrait ê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
Amelio Vazquez-Reina
la source

Réponses:

9

Don peut être mieux dans la plupart des cas, mais juste au cas où le fichier est vraiment gros et que vous ne pouvez pas sedgérer un fichier de script aussi volumineux (ce qui peut arriver à environ 5000+ lignes de script) , le voici avec plain 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 une fenêtre coulissante en entrée. Il fonctionne en construisant une anticipation tampon$B lignes -count avant même d' essayer d'imprimer quoi que ce soit.

Et en fait, je devrais probablement clarifier mon point précédent: le limiteur de performance principal pour cette solution et pour Don sera directement lié à l'intervalle. Cette solution va ralentir avec un intervalle de plus grandes tailles , alors que don de ralentiront avec un intervalle de plus grandes 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 workflow:

  • Si $matchse trouve dans l'espace de motif précédé d'une ligne \nélectronique, supprimera sedrécursivement Dchaque ligne \nélectronique qui la précède.
    • J'éclaircissais $match train de vider complètement l'espace des motifs avant - 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/d'essayer de l'obtenir en une seule fois et d'esquiver la boucle, mais lorsqu'elle $A/$Best grande, la Dboucle elete se révèle considérablement plus rapide.
  • Ensuite, nous tirons sur la Nligne d'entrée ext précédée d'un \ndélimiteur ewline et essayons une fois de plus d' Déliminer un /\n.*$match/en se référant à notre expression régulière w / la plus récemment utilisée //.
  • Si l'espace de motif correspond, $matchil ne peut le faire qu'avec $matchen tête de ligne - toutes les $Blignes précédentes ont été effacées.
    • Nous commençons donc en boucle sur $After.
    • Chaque terme de cette boucle , nous allons tenter de s///ubstitute pour &lui - même le $Ae \ncaractère ewline dans l' espace de modèle, et, en cas de succès, l' tEst nous branche - et tout notre $Atampon de près avoir - sur le script entièrement pour lancer le script au- dessus de la partie supérieure avec la ligne d'entrée suivante, le cas échéant.
    • Si l' test ne réussit pas, nous allons branch vers l' :tétiquette d'opération et recurse pour une autre ligne d'entrée - éventuellement recommencer la boucle si $matchse produit lors de la collecte $After.
  • Si nous obtenons passé une $matchboucle de fonction, nous allons essayer de pRint la $dernière ligne si cela est, et si !ne pas essayer de s///ubstitute pour &lui - même le $Be \ncaractère ewline dans l' espace modèle.
    • Nous l' testimerons également, et si cela réussit, nous passerons à l' :Pétiquette rint.
    • Si ce n'est pas le cas, nous allons revenir à :top et obtenir une autre ligne d'entrée ajoutée au tampon.
  • Si nous faisons à :PRint nous PRint alors Délete jusqu'à la première \newline dans l' espace de configuration et exécutez à nouveau le script du haut 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 à :Print ressemblerait à:

^1\n2\n3$

Et c'est ainsi que sedrassemble son $Btampon efore. Et donc sedimprime sur les $Blignes de sortie -comptes derrière l'entrée qu'il a recueillies. Cela signifie que, compte tenu de notre exemple précédent, sedse PRint 1à la sortie, puis Delete et que renvoyer au début du script un espace de motif qui ressemble à :

^2\n3$

... et en haut du script, la Nligne d'entrée ext est récupérée et donc l'itération suivante ressemble à:

^2\n3\n4$

Et donc quand nous trouvons la première occurrence de 5in input, l'espace de motif ressemble en fait à:

^3\n4\n5$

Ensuite, la Dboucle elete entre en jeu et quand elle est terminée, elle ressemble à:

^5$

Et lorsque la Nligne d'entrée ext est tirée, sedfrappe EOF et quitte. À ce moment-là, il n'a encore Pimprimé que les lignes 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
mikeserv
la source
En fait, je travaille avec d'énormes fichiers, et la réponse de don a été sensiblement plus lente que cette solution. Au départ, j'hésitais à changer ma réponse acceptée, mais la différence de vitesse est assez visible.
Amelio Vazquez-Reina
4
@Amelio - cela fonctionnera avec un flux de n'importe quelle taille, et il n'a pas besoin de lire le fichier pour fonctionner. Le plus grand facteur de performance est la taille de $Aet / ou $B. Plus vous augmentez ces chiffres, plus ils seront lents - mais vous pouvez les rendre raisonnablement grands.
mikeserv
1
@ AmelioVazquez-Reina - si vous utilisez l'ancien, c'est mieux, je pense.
mikeserv
11

Vous pouvez utiliser gnu grepavec -Aet -Bpour imprimer exactement les parties du fichier que vous souhaitez exclure, mais ajoutez le -ncommutateur pour imprimer également les numéros de ligne, puis formatez la sortie et passez-la en tant que script de commande sedpour 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 des fichiers de modèles transmis grepvia -fpar 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 effondrait trois numéros de ligne consécutifs ou plus dans des plages de manière à avoir, par exemple, 2,6dau lieu de 2d;3d;4d;5d;6d... bien que 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-

commnécessite l' intervention Sorted ce qui signifie que l'ordre de ligne ne serait pas conservée dans la sortie finale ( à moins que votre fichier est déjà triée) donc nlest utilisé pour numéroter les lignes avant de tri, comm -13imprime des lignes uniques au 2ème FICHIER puis cutsupprime la partie qui a été 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-
don_crissti
la source
Merci Don! Question rapide, vous attendriez-vous à ce que la solution avec commsoit plus rapide que l'original avec sedet grep?
Amelio Vazquez-Reina
1
@ AmelioVazquez-Reina - Je ne pense pas, car il lit toujours le fichier d'entrée deux fois (en plus il fait un tri) contrairement à la solution de Mike qui ne traite le fichier qu'une seule fois.
don_crissti
9

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
  • -Nesactive le mode ex silencieux non compatible. Utile pour l'écriture de scripts.
  • +{command}dites à vim de s'exécuter {command}sur le fichier.
  • g/${PAT}/- sur toutes les lignes correspondantes /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-rangespour ces deux)
  • d - supprimez 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é.

muru
la source
3

Que diriez-vous (en utilisant GNU grepet bash):

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

Ici, nous trouvons les lignes devant être supprimées grep -B2 -A1 'fff' file.txt, puis en l'utilisant comme fichier d'entrée pour trouver les lignes souhaitées les rejetant.

heemayl
la source
Hmm, cela ne produit rien sur ma machine (OS X)
Amelio Vazquez-Reina
@ AmelioVazquez-Reina désolé pour ça .. je ne connaissais pas votre système d'exploitation avant ... de toute façon j'ai testé cela sur Ubuntu ..
heemayl
2
Cela aurait le même problème que kosla solution (maintenant supprimée) comme s'il y avait des lignes en double dans le fichier d'entrée et que certaines d'entre elles se trouvaient en dehors de la plage et que d'autres se trouvaient dans cette plage, cela les supprimerait toutes. En outre, avec plusieurs occurrences de modèle , s'il y a des lignes comme --dans le fichier d'entrée (en dehors des plages), cela les supprimera car le délimiteur --apparaît dans grepla sortie de 'lorsque plus d'une ligne correspond au modèle (ce dernier est hautement improbable mais vaut la peine). mentionnant je suppose).
don_crissti
@don_crissti Merci..vous avez raison..même si je prenais l'exemple d'OP littéralement..je vais le laisser au cas où quelqu'un le trouverait utile plus tard ..
heemayl
1

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 bon car 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.

RafDouglas
la source
1

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 /programming//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
RafDouglas
la source
1
Brillant, utilisez awksur un fichier inversé pour gérer les lignes suivantes lorsque vous voulez affecter les lignes avant et inverser le résultat.
karmakaze
0

Une façon d'y parvenir, 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
lordpavel
la source
même solution que heemayl, et même problème que celui décrit par don_crissti: cela aurait le même problème que la solution de kos (maintenant supprimée) comme s'il y avait des lignes en double dans le fichier d'entrée et que certaines d'entre elles sortaient de la plage et d'autres se trouvaient dans cette plage cela les supprimera tous. En outre, avec plusieurs occurrences de modèle, s'il y a des lignes comme - dans le fichier d'entrée (en dehors des plages), cela les supprimera car le délimiteur - apparaît dans la sortie de grep lorsque plus d'une ligne correspond au modèle (ce dernier est fortement peu probable mais mérite d'être mentionné, je suppose).
Bodo Thiesen, le
0

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)
dedowsdi
la source