Eliminar el rango de líneas sobre el patrón con sed (o awk)

28

Tengo el siguiente código que eliminará líneas con el patrón bananay 2 líneas después:

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

¡Hasta aquí todo bien! Pero lo necesito para eliminar 2 líneas antes banana , pero no puedo obtenerlo con un "signo menos" o lo que sea (similar a lo que grep -v -B2 banana filedebería hacer, pero no lo hace):

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
fuente
1
La más fácil es para cargar todos los datos en una matriz, saltar las líneas no deseadas entonces la salida lo que queda: 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]}'. Esto no es eficiente, así que esto es solo una pista, no una solución.
manatwork
66
Solo hazlo tac file | sed ... | tac. : P
angus
@angus no lo pensé;)
Teresa e Junior
1
podrías haber hecho sed '/banana/,+2d' file eso también funcionará
Akaks
1
Si está abierto a usar awk, es bastante simple: awk 'tolower($0)~/bandana/{print prev[!idx];print prev[idx]} {idx=!idx;prev[idx]=$0}' filein dado que este es un comentario y no una respuesta (ya hay otras respuestas), no entraré en demasiados detalles, pero el quid de la cuestión es que siempre tiene la dos registros anteriores en Anterior [0] y anterior [1], el "más fresco", según la iteración pero siempre en prev[idx], por lo que cuando se imprime, se imprimen en !idxentonces idxorden. En cualquier caso, alterne idxy coloque el registro actual prev[idx].
Luv2code

Respuestas:

22

Sed no retrocede: una vez que se procesa una línea, se hace. Por lo tanto, "buscar una línea e imprimir las N líneas anteriores" no funcionará como está, a diferencia de "encontrar una línea e imprimir las siguientes N líneas", que es fácil de injertar.

Si el archivo no es demasiado largo, ya que parece estar bien con las extensiones GNU, puede usarlo tacpara invertir las líneas del archivo.

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

Otro ángulo de ataque es mantener una ventana deslizante en una herramienta como awk. Adaptación de ¿Hay alguna alternativa a los interruptores grep's -A -B -C (para imprimir algunas líneas antes y después)? (advertencia: mínimamente probado):

#!/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]} }

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

Gilles 'SO- deja de ser malvado'
fuente
2
sedtambién puede hacer ventanas deslizantes, pero el script resultante es típicamente tan ilegible que es más fácil de usar awk.
jw013
@Gilles .. El awkguión no es del todo correcto; as-is imprime líneas en blanco y pierde las últimas líneas. Esto parece solucionarlo, pero puede no ser ideal o correcto en sí: if (NR-before in h) { print...; delete...; }... y en la ENDsección: for (i in h) print h[i]... Además, el script awk imprime la línea correspondiente, pero la tac/secversión no; pero la pregunta es un poco ambigua en esto ... El script awk "original", al que proporcionó un enlace, funciona bien ... Me gusta ... No estoy seguro de cómo el 'mod' anterior afecta la impresión después de líneas ...
Peter.O
@ Peter.O Gracias, el script awk debería ser mejor ahora. ¡Y me llevó menos de 6 a 8 años!
Gilles 'SO- deja de ser malvado'
19

Esto es bastante fácil con ex o vim -e

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

La expresión dice: por cada línea que contenga banana en el rango de la línea actual -2 a la línea actual, elimine.

Lo bueno es que el rango también puede contener búsquedas hacia adelante y hacia atrás, por ejemplo, esto eliminará todas las secciones del archivo que comienzan con una línea que contiene manzana y termina con una línea que contiene naranja y una línea con plátano:

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

Usando la "ventana deslizante" en perl:

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

Puede hacer esto de manera bastante simple con sed:

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

No sé por qué alguien diría lo contrario, pero para encontrar una línea e imprimir líneas anteriores se sed incorpora la Pprimitiva \nrint incorporada que escribe solo hasta el primer carácter de línea ewline en el espacio del patrón. La Dprimitiva elete complementaria elimina ese mismo segmento de espacio de patrón antes de reciclar recursivamente el script con lo que queda. Y para redondearlo, hay una primitiva para agregar la Nlínea de entrada ext al espacio de patrón después de un \ncarácter de línea e insertado .

Para que esa línea de sedsea ​​todo lo que necesita. Simplemente lo reemplazas matchcon lo que sea tu expresión regular y eres dorado. Esa también debería ser una solución muy rápida .

Tenga en cuenta también que contará correctamente un matchprecedente inmediato matchcomo un disparador para silenciar la salida de las dos líneas anteriores y también silenciará su impresión:


1
7match
8
11match

Para que funcione para un número arbitrario de líneas, todo lo que necesita hacer es obtener una pista.

Asi que:

    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

... elimina las 5 líneas que preceden a cualquier coincidencia.

mikeserv
fuente
1

Utilizando 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
fuente