Cómo grep-inversa-coincidencia y excluir las líneas "antes" y "después"


26

Considere un archivo de texto con las siguientes entradas:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Dado un patrón (por ejemplo fff), me gustaría grep el archivo de arriba para obtener la salida:

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

Por ejemplo, si B = 2y A = 1, la salida con patrón = fffdebería ser:

aaa
bbb
ccc
hhh
iii

¿Cómo puedo hacer esto con grep u otras herramientas de línea de comandos?


Nota, cuando lo intento:

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

No consigo lo que quiero. En cambio, obtengo:

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

Respuestas:


9

don puede ser mejor en la mayoría de los casos, pero en caso de que el archivo sea realmente grande y no pueda sedmanejar un archivo de script tan grande (que puede suceder en más de 5000 líneas de script) , aquí está con simple 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'

Este es un ejemplo de lo que se llama una ventana deslizante en la entrada. Funciona mediante la construcción de un preanálisis de amortiguamiento de las $Blíneas -Contar incluso antes de intentar imprimir nada.

Y en realidad, probablemente debería aclarar mi punto anterior: el limitador de rendimiento primario tanto para esta solución como para el don estará directamente relacionado con el intervalo. Esta solución se ralentizará con intervalos de mayor tamaño , mientras que la de don disminuirá con frecuencias de intervalo más grandes . En otras palabras, incluso si el archivo de entrada es muy grande, si la ocurrencia del intervalo real es muy poco frecuente, entonces su solución es probablemente el camino a seguir. Sin embargo, si el tamaño del intervalo es relativamente manejable y es probable que ocurra con frecuencia, entonces esta es la solución que debe elegir.

Así que aquí está el flujo de trabajo:

  • Si $matchse encuentra en un espacio de patrón precedido por una línea de \nflujo, sedelegirá recursivamente Dcada línea de \nflujo que lo precede.
    • Antes estaba limpiando $matchcompletamente el espacio del patrón, pero para manejar fácilmente la superposición, dejar un hito parece funcionar mucho mejor.
    • También traté s/.*\n.*\($match\)/\1/de intentarlo de una vez y esquivar el bucle, pero cuando $A/$Bson grandes, el Dbucle elete prueba considerablemente más rápido.
  • Luego, extraemos la Nlínea de entrada de extensión precedida por un \ndelimitador de línea de línea e intentamos nuevamente Delegir una de /\n.*$match/nuevo refiriéndonos a nuestra expresión regular w / utilizada más recientemente //.
  • Si el espacio de patrón coincide, $matchentonces solo puede hacerlo $matchal principio de la línea: todas las $Blíneas anteriores se han borrado.
    • Entonces comenzamos a dar vueltas $Adespués.
    • Cada ejecución de este bucle vamos a tratar de s///ubstitute por &sí misma el $AXX \ncarácter ewline en el espacio de patrones, y, si tiene éxito, test nos BRANCH - y toda nuestra $Amemoria intermedia espués - fuera del guión completo para iniciar el script más de la parte superior con la siguiente línea de entrada si la hay.
    • Si el test no tiene éxito, bvolveremos a la :tetiqueta de operación y recurriremos a otra línea de entrada, posiblemente comenzando el ciclo nuevamente si $matchocurre mientras se recolecta $Adespués.
  • Si conseguimos más allá de un $matchbucle de función, entonces vamos a tratar de print la $última línea, si esto es así, y si !no tratar de s///ubstitute por &sí misma el $BXX \ncarácter ewline en el espacio patrón.
    • También consideraremos testo, y si tiene éxito, pasaremos a la :Petiqueta de la pista.
    • Si no, volveremos a :toperar y obtendremos otra línea de entrada añadida al búfer.
  • Si llegamos a :Print, vamos a rint, Pluego Delegiremos hasta la primera línea de \newline en el espacio del patrón y volveremos a ejecutar el script desde arriba con lo que queda.

Y esta vez, si estuviéramos haciendo A=2 B=2 match=5; seq 5 | sed...

El espacio del patrón para la primera iteración en :Print se vería así:

^1\n2\n3$

Y así es como sedreúne su $Bbuffer antes. Y así se sedimprime en las $Blíneas de conteo de salida detrás de la entrada que ha reunido. Esto significa que, dado nuestro ejemplo anterior, se seddaría una Ppista 1al resultado, y luego lo Delegiría y enviaría de nuevo a la parte superior del script un espacio de patrón que se parece a:

^2\n3$

... y en la parte superior de la secuencia de comandos Nse recupera la línea de entrada ext y, por lo tanto, la siguiente iteración se ve así:

^2\n3\n4$

Y así, cuando encontramos la primera aparición de 5entrada, el espacio del patrón en realidad se ve así:

^3\n4\n5$

Luego, el Dbucle elete se activa y cuando termina, se ve así:

^5$

Y cuando Nse tira de la línea de entrada ext sedgolpea EOF y se cierra. En ese momento, solo ha Pborrado las líneas 1 y 2.

Aquí hay un ejemplo de ejecución:

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'

Que 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

De hecho, estoy trabajando con archivos enormes, y la respuesta de don fue notablemente más lenta que esta solución. Inicialmente tenía mis dudas al cambiar mi respuesta aceptada, pero la diferencia de velocidad es bastante visible.
Amelio Vazquez-Reina

44
@Amelio: esto funcionará con una secuencia de cualquier tamaño y no necesita leer el archivo para que funcione. El mayor factor de rendimiento es el tamaño de $Ay / o $B. Cuanto más grandes sean esos números, más lento se volverá, pero puede hacerlos razonablemente grandes.
mikeserv 01 de

1
@ AmelioVazquez-Reina: si está usando el anterior, creo que es mejor.
mikeserv

11

Puede usar gnu grepcon -Ay -Bpara imprimir exactamente las partes del archivo que desea excluir, pero agregue el -ninterruptor para imprimir también los números de línea y luego formatee el resultado y páselo como un script de comando sedpara eliminar esas líneas:

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

Esto también debería funcionar con archivos de patrones pasados ​​a greptravés de, por -fejemplo:

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

Creo que esto podría optimizarse ligeramente si colapsó tres o más números de línea consecutivos en rangos para tener, por ejemplo, en 2,6dlugar de 2d;3d;4d;5d;6d... aunque si la entrada tiene solo unas pocas coincidencias, no vale la pena hacerlo.


Otras formas que no conservan el orden de las líneas y probablemente sean más lentas:
con comm:

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

commrequiere una entrada ordenada, lo que significa que el orden de las líneas no se conservará en la salida final (a menos que su archivo ya esté ordenado), por lo que nlse usa para numerar las líneas antes de ordenarlas, comm -13imprime solo las líneas exclusivas de 2nd FILE y luego cutelimina la parte que se agregó nl(es decir, el primer campo y el delimitador :)
con join:

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

Gracias don! Pregunta rápida, ¿esperaría que la solución con commsea ​​más rápida que la original con sedy grep?
Amelio Vazquez-Reina

1
@ AmelioVazquez-Reina: no lo creo, ya que todavía lee el archivo de entrada dos veces (además de ordenarlo) en lugar de la solución de Mike, que solo procesa el archivo una vez.
don_crissti

9

Si no te importa usar vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesactiva el modo ex silencioso no compatible. Útil para scripting.
  • +{command}dile a vim que se ejecute {command}en el archivo.
  • g/${PAT}/- En todas las líneas coincidentes /fff/. Esto se vuelve complicado si el patrón contiene caracteres especiales de expresión regular que no pretendía tratar de esa manera.
  • .-${B} - desde 1 línea por encima de esta
  • .+${A}- a 2 líneas debajo de esta (ver :he cmdline-rangespara estas dos)
  • d - Eliminar las líneas.
  • +w !tee luego escribe en la salida estándar.
  • +q! se cierra sin guardar los cambios.

Puede omitir las variables y usar el patrón y los números directamente. Los usé solo por claridad de propósito.


3

¿Qué tal (usando GNU grepy bash):

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

Aquí estamos encontrando las líneas a ser descartadas grep -B2 -A1 'fff' file.txt, luego usándolas como un archivo de entrada para encontrar las líneas deseadas que las descartan.


Hmm, esto no genera nada en mi máquina (OS X)
Amelio Vazquez-Reina

@ AmelioVazquez-Reina perdón por eso ... no conocía tu sistema operativo antes ... de todos modos, he probado esto en Ubuntu ... ''
dijo el

2
Esto tendría el mismo problema que kosla solución (ahora eliminada) como si hubiera líneas duplicadas en el archivo de entrada y algunas caen fuera del rango y otras están dentro de ese rango, esto las eliminará a todas. Además, con múltiples ocurrencias de patrón , si hay líneas como --en el archivo de entrada (fuera de los rangos) esto las eliminará porque el delimitador --aparece en grepla salida cuando más de una línea coincide con el patrón (la última es altamente improbable pero vale la pena mencionando supongo).
don_crissti 01 de

@don_crissti Gracias ... tienes razón ... aunque estaba tomando el ejemplo de OP literalmente ... lo dejaré en caso de que alguien lo encuentre útil más tarde ...
heemayl

1

Puede alcanzar un resultado suficientemente bueno mediante el uso de archivos temporales:

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

El resultado es lo suficientemente bueno porque puede perder algo de sangría en el proceso, pero si se trata de un archivo insensible xml o de sangría, no debería ser un problema. Dado que este script usa una unidad ram, escribir y leer esos archivos temporales es tan rápido como trabajar en la memoria.


1

Además, si solo desea excluir algunas líneas delante de un marcador dado, puede usar:

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

(Glenn Jackman en /programming//a/1492538 )

Al canalizar algunos comandos, puede obtener el comportamiento antes / despué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
Brillante, úselo awken un archivo invertido para manejar las siguientes líneas cuando quiera afectar las líneas anteriores y revertir el resultado.
karmakaze

0

Una forma de lograr esto, quizás la forma más fácil sería crear una variable y hacer lo siguiente:

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

De esta manera todavía tienes tu estructura. Y puede ver fácilmente desde el revestimiento que está tratando de eliminar.

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

misma solución que heemayl, y el mismo problema descrito por don_crissti: esto tendría el mismo problema que la solución de kos (ahora eliminada) como si hubiera líneas duplicadas en el archivo de entrada y algunas de ellas caen fuera del rango y otras están dentro de ese rango esto los eliminará a todos. Además, con múltiples ocurrencias de patrón, si hay líneas como - en el archivo de entrada (fuera de los rangos) esto las eliminará porque el delimitador - aparece en la salida de grep cuando más de una línea coincide con el patrón (este último es altamente improbable pero vale la pena mencionar, supongo).
Bodo Thiesen

0

Si solo hay 1 coincidencia:

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

De lo contrario (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)
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.