Supprimer la plage de lignes au-dessus du motif avec sed (ou awk)

28

J'ai le code suivant qui supprimera les lignes avec le motif bananaet 2 lignes après:

sed '/banana/I,+2 d' file

Jusqu'ici tout va bien! Mais j'en ai besoin pour supprimer 2 lignes avant banana , mais je ne peux pas l'obtenir avec un «signe moins» ou autre (similaire à ce qui grep -v -B2 banana filedevrait faire mais ne le fait pas):

teresaejunior@localhost ~ > LC_ALL=C sed '-2,/banana/I d' file
sed: invalid option -- '2'
teresaejunior@localhost ~ > LC_ALL=C sed '/banana/I,-2 d' file
sed: -e expression #1, char 16: unexpected `,'
teresaejunior@localhost ~ > LC_ALL=C sed '/banana/I,2- d' file
sed: -e expression #1, char 17: unknown command: `-'
Teresa e Junior
la source
1
Le plus simple est de charger toutes les données dans un tableau, sauter les lignes non désirées puis sortie ce qui reste: awk '{l[m=NR]=$0}/banana/{for(i=NR-2;i<=NR;i++)delete l[i]}END{for(i=1;i<=m;i++)if(i in l)print l[i]}'. Ce n'est pas efficace, donc ce n'est qu'un indice, pas une solution.
manatwork
6
Faites-le tac file | sed ... | tac. : P
angus
@angus je n'y ai pas pensé;)
Teresa e Junior
1
vous auriez pu faire sed '/banana/,+2d' file cela fonctionnera également
Akaks
1
Si vous êtes ouvert à l'utilisation de awk, c'est assez simple: awk 'tolower($0)~/bandana/{print prev[!idx];print prev[idx]} {idx=!idx;prev[idx]=$0}' filein comme il s'agit d'un commentaire et non d'une réponse (il existe déjà d'autres réponses), je n'entrerai pas dans trop de détails, mais le point crucial est que vous avez toujours le deux précédents enregistrements prev [0] et prev [1], la « plus fraîche » en fonction de l'itération , mais toujours prev[idx], donc quand vous imprimez, vous imprimez en !idxpuis idxcommande. Quoi qu'il en soit, alternez idxet mettez le record en cours prev[idx].
Luv2code

Réponses:

22

Sed ne revient pas en arrière: une fois la ligne traitée, c'est fait. Donc, «trouver une ligne et imprimer les N lignes précédentes» ne fonctionnera pas tel quel, contrairement à «trouver une ligne et imprimer les N lignes suivantes» qui est facile à greffer.

Si le fichier n'est pas trop long, puisque vous semblez être d'accord avec les extensions GNU, vous pouvez utiliser tacpour inverser les lignes du fichier.

tac | sed '/banana/I,+2 d' | tac

Un autre angle d'attaque consiste à maintenir une fenêtre coulissante dans un outil comme awk. Adaptation à partir de Y a-t-il une alternative aux commutateurs -A -B -C de grep (pour imprimer quelques lignes avant et après)? (avertissement: peu testé):

#!/bin/sh
{ "exec" "awk" "-f" "$0" "$@"; } # -*-awk-*-
# The array h contains the history of lines that are eligible for being "before" lines.
# The variable skip contains the number of lines to skip.
skip { --skip }
match($0, pattern) { skip = before + after }
NR > before && !skip { print NR h[NR-before] }
{ delete h[NR-before]; h[NR] = $0 }
END { if (!skip) {for (i=NR-before+1; i<=NR; i++) print h[i]} }

Usage: /path/to/script -v pattern='banana' -v before=2

Gilles 'SO- arrête d'être méchant'
la source
2
sedpeut également faire des fenêtres coulissantes, mais le script résultant est généralement si illisible qu'il est plus facile à utiliser awk.
jw013
@Gilles .. Le awkscript n'est pas tout à fait correct; en l'état, il imprime les lignes vides et manque les dernières lignes. Cela semble le corriger, mais ce n'est peut-être pas l'idéal ou le droit lui-même: if (NR-before in h) { print...; delete...; }... et dans la ENDsection: for (i in h) print h[i]... De plus, le script awk imprime la ligne correspondante, mais pas la tac/secversion; mais la question est un peu ambiguë à ce sujet .. Le script awk "original", auquel vous avez fourni un lien, fonctionne bien .. Je l'aime bien ... Je ne sais pas comment le "mod" ci-dessus affecte l' impression après lignes ...
Peter.O
@ Peter.O Merci, le script awk devrait être meilleur maintenant. Et cela m'a pris moins de 6 à 8 ans!
Gilles 'SO- arrête d'être méchant'
19

C'est assez facile avec ex ou vim -e

    vim -e - $file <<@@@
g/banana/.-2,.d
wq
@@@

L'expression se lit comme suit: pour chaque ligne contenant de la banane dans la plage allant de la ligne actuelle -2 à la ligne actuelle, supprimez.

Ce qui est cool, c'est que la plage peut également contenir des recherches en arrière et en avant, par exemple, cela supprimera toutes les sections du fichier commençant par une ligne contenant apple et se terminant par une ligne contenant orange et contenant une ligne avec banane:

    vim -e - $file <<@@@
g/banana/?apple?,/orange/d
wq
@@@
Justin Rowe
la source
7

Utilisation de la "fenêtre coulissante" dans perl:

perl -ne 'push @lines, $_;
          splice @lines, 0, 3 if /banana/;
          print shift @lines if @lines > 2
          }{ print @lines;'
choroba
la source
6

Vous pouvez le faire assez simplement avec sed:

printf %s\\n    1 2 3 4match 5match 6 \
                7match 8 9 10 11match |
sed -e'1N;$!N;/\n.*match/!P;D'

Je ne sais pas pourquoi quelqu'un dirait le contraire, mais pour trouver une ligne et imprimer les lignes précédentes sed incorpore la Pprimitive \nrint intégrée qui n'écrit que jusqu'au premier caractère ewline dans l'espace modèle. La Dprimitive elete complémentaire supprime ce même segment d'espace de motif avant de recycler récursivement le script avec ce qui reste. Et pour l'arrondir, il existe une primitive pour ajouter la Nligne d'entrée ext à l'espace de motif après un \ncaractère ewline inséré .

Pour qu'une seule ligne sedsoit tout ce dont vous avez besoin. Vous remplacez simplement matchpar votre expression rationnelle et vous êtes en or. Cela devrait également être une solution très rapide .

Notez également qu'il comptera correctement un matchprécédent immédiatement matchcomme déclencheur de sortie silencieuse pour les deux lignes précédentes et calmera également son impression:


1
7match
8
11match

Pour que cela fonctionne pour un nombre arbitraire de lignes, tout ce que vous avez à faire est d'obtenir une piste.

Alors:

    printf %s\\n     1 2 3 4 5 6 7match     \
                     8match 9match 10match  \
                     11match 12 13 14 15 16 \
                     17 18 19 20match       |
    sed -e:b -e'$!{N;2,5bb' -e\} -e'/\n.*match/!P;D'

1
11match
12
13
14
20match

... supprime les 5 lignes précédant toute correspondance.

mikeserv
la source
1

En utilisant man 1 ed:

str='
1
2
3
banana
4
5
6
banana
8
9
10
'

# using Bash
cat <<-'EOF' | ed -s <(echo "$str")  | sed -e '1{/^$/d;}' -e '2{/^$/d;}'
H
0i


.
,g/banana/km\
'm-2,'md
,p
q
EOF
larz
la source