¿Cómo obtener la parte de un archivo después de la primera línea que coincide con una expresión regular?


169

Tengo un archivo con aproximadamente 1000 líneas. Quiero la parte de mi archivo después de la línea que coincide con mi declaración grep.

Es decir:

$ cat file | grep 'TERMINATE'     # It is found on line 534

Entonces, quiero el archivo de la línea 535 a la línea 1000 para su posterior procesamiento.

¿Cómo puedo hacer eso?


34
UUOC (uso inútil del gato):grep 'TERMINATE' file
Jacob

30
Lo sé, es como si lo usara de esa manera. Volvamos a la pregunta.
Yugal Jindle

3
Esta es una pregunta de programación perfectamente buena, y muy adecuada para stackoverflow.
aioobe

13
@Jacob No es un uso inútil del gato en absoluto. Su uso es imprimir un archivo a la salida estándar, lo que significa que podemos usar grepla interfaz de entrada estándar para leer datos, en lugar de tener que aprender a qué interruptor aplicar grep, y sed, y awk, y pandoc, y ffmpegetc. cuando queremos leer de un archivo Ahorra tiempo porque no tenemos que aprender un nuevo interruptor cada vez que queremos hacer lo mismo: leer de un archivo.
runeks

@runeks Estoy de acuerdo con su sentimiento - pero se puede lograr que sin gato: grep 'TERMINATE' < file. Tal vez haga que la lectura sea un poco más difícil, pero esto es un script de shell, por lo que siempre será un problema :)
LOAS

Respuestas:


307

Lo siguiente imprimirá la coincidencia de línea TERMINATEhasta el final del archivo:

sed -n -e '/TERMINATE/,$p'

Explicado: -n deshabilita el comportamiento predeterminado sedde imprimir cada línea después de ejecutar su secuencia de comandos, -eindicando una secuencia de comandos para sed, /TERMINATE/,$es una selección de rango de dirección (línea) que significa la primera línea que coincide con la TERMINATEexpresión regular (como grep) al final del archivo ( $) , y pes el comando de impresión que imprime la línea actual.

Esto se imprimirá desde la línea que sigue a la línea coincidente TERMINATEhasta el final del archivo:
(DESPUÉS de la línea coincidente a EOF, SIN incluir la línea coincidente)

sed -e '1,/TERMINATE/d'

Explicado: 1,/TERMINATE/ es una selección de rango de dirección (línea) que significa la primera línea para la entrada a la primera línea que coincide con la TERMINATEexpresión regular, y des el comando de eliminación que elimina la línea actual y salta a la siguiente línea. Como sedel comportamiento predeterminado es imprimir las líneas, imprimirá las líneas desde TERMINATE el final de la entrada.

Editar:

Si quieres las líneas antes TERMINATE:

sed -e '/TERMINATE/,$d'

Y si desea ambas líneas antes y después TERMINATEen 2 archivos diferentes en una sola pasada:

sed -e '1,/TERMINATE/w before
/TERMINATE/,$w after' file

Los archivos antes y después contendrán la línea con terminación, por lo que para procesar cada uno debe usar:

head -n -1 before
tail -n +2 after

Edit2:

SI no desea codificar los nombres de archivo en el script sed, puede:

before=before.txt
after=after.txt
sed -e "1,/TERMINATE/w $before
/TERMINATE/,\$w $after" file

Pero luego debe escapar del $significado de la última línea para que el shell no intente expandir la $wvariable (tenga en cuenta que ahora usamos comillas dobles alrededor del script en lugar de comillas simples).

Olvidé decir que la nueva línea es importante después de los nombres de archivo en el script para que sed sepa que los nombres de archivo terminan.


Editar: 2016-0530

Sébastien Clément preguntó: "¿Cómo reemplazarías el hardcoded TERMINATEpor una variable?"

Haría una variable para el texto coincidente y luego lo haría de la misma manera que en el ejemplo anterior:

matchtext=TERMINATE
before=before.txt
after=after.txt
sed -e "1,/$matchtext/w $before
/$matchtext/,\$w $after" file

para usar una variable para el texto coincidente con los ejemplos anteriores:

## Print the line containing the matching text, till the end of the file:
## (from the matching line to EOF, including the matching line)
matchtext=TERMINATE
sed -n -e "/$matchtext/,\$p"
## Print from the line that follows the line containing the 
## matching text, till the end of the file:
## (from AFTER the matching line to EOF, NOT including the matching line)
matchtext=TERMINATE
sed -e "1,/$matchtext/d"
## Print all the lines before the line containing the matching text:
## (from line-1 to BEFORE the matching line, NOT including the matching line)
matchtext=TERMINATE
sed -e "/$matchtext/,\$d"

Los puntos importantes sobre la sustitución de texto con variables en estos casos son:

  1. Las variables ( $variablename) encerradas en single quotes[ '] no se "expandirán" pero las variables dentro de double quotes[ "] sí. Por lo tanto, debe cambiar todo single quotesa double quotessi contienen texto que desea reemplazar con una variable.
  2. Los sedrangos también contienen una $y son seguidos inmediatamente por una carta como: $p, $d, $w. También se verá como las variables que ser ampliado, por lo que tiene que escapar de esos $personajes con una barra invertida [ \] como: \$p, \$d, \$w.

¿Cómo podemos obtener las líneas antes de TERMINAR y eliminar todo lo que sigue?
Yugal Jindle

¿Cómo reemplazaría el TERMINAL codificado por una variable?
Sébastien Clément

2
Un caso de uso que falta aquí es cómo imprimir líneas después del último marcador (si puede haber varios de ellos en el archivo ... piense en archivos de registro, etc.).
mato

El ejemplo sed -e "1,/$matchtext/d"no funciona cuando $matchtextocurre en la primera línea. Tuve que cambiarlo a sed -e "0,/$matchtext/d".
Karalga

61

Como una aproximación simple, podría usar

grep -A100000 TERMINATE file

que greps TERMINATEy salidas de hasta 100000 líneas después de esa línea.

De la página man

-A NUM, --after-context=NUM

Imprima NUM líneas del contexto final después de las líneas coincidentes. Coloca una línea que contiene un separador de grupo (-) entre grupos contiguos de coincidencias. Con la opción -o o --only-matching, esto no tiene efecto y se da una advertencia.


Eso podría funcionar para esto, pero necesito codificarlo en mi script para procesar muchos archivos. Entonces, muestra alguna solución genérica.
Yugal Jindle

3
¡Creo que esta es una solución práctica!
michelgotta

2
de forma similar -B NUM, --before-context = NUM ​​Imprime NUM líneas del contexto inicial antes de hacer coincidir las líneas. Coloca una línea que contiene un separador de grupo (-) entre grupos contiguos de coincidencias. Con la opción -o o --only-matching, esto no tiene efecto y se da una advertencia.
PiyusG

esta solución funcionó para mí porque puedo usar fácilmente variables como mi cadena para verificar.
Jose Martinez

3
¡Buena idea! Si no está seguro del tamaño del contexto, puede contar las líneas en su filelugar:grep -A$(cat file | wc -l) TERMINATE file
Lemming

26

Una herramienta para usar aquí es awk:

cat file | awk 'BEGIN{ found=0} /TERMINATE/{found=1}  {if (found) print }'

Como funciona esto:

  1. Establecemos la variable 'encontrado' en cero, evaluando falso
  2. Si se encuentra una coincidencia para 'TERMINAR' con la expresión regular, la establecemos en una.
  3. Si nuestra variable 'encontrado' se evalúa como Verdadero, imprime :)

Las otras soluciones pueden consumir mucha memoria si las usa en archivos muy grandes.


Simple, elegante y muy genérico. En mi caso, estaba imprimiendo todo hasta la segunda aparición de '###':cat file | awk 'BEGIN{ found=0} /###/{found=found+1} {if (found<2) print }'
Aleksander Stelmaczonek

3
Una herramienta para no usar aquí es cat. awkes perfectamente capaz de tomar uno o más nombres de archivo como argumentos. Ver también stackoverflow.com/questions/11710552/useless-use-of-cat
tripleee

9

Si entiendo tu pregunta correctamente, quieres las líneas después TERMINATE , sin incluir la línea TERMINATE. awkpuede hacer esto de una manera simple:

awk '{if(found) print} /TERMINATE/{found=1}' your_file

Explicación:

  1. Aunque no es la mejor práctica, puede confiar en el hecho de que todos los valores predeterminados de vars son 0 o la cadena vacía si no está definida. Entonces, la primera expresión ( if(found) print) no imprimirá nada para comenzar.
  2. Una vez realizada la impresión, verificamos si esta es la línea de inicio (que no debe incluirse).

Esto imprimirá todas las líneas después de la línea TERMINATE.


Generalización:

  • Tiene un archivo con inicio - y finales -lines y desea que los límites entre esas líneas excluyendo el inicio - y finales -lines.
  • Las líneas de inicio y final podrían definirse mediante una expresión regular que coincida con la línea.

Ejemplo:

$ cat ex_file.txt 
not this line
second line
START
A good line to include
And this line
Yep
END
Nope more
...
never ever
$ awk '/END/{found=0} {if(found) print} /START/{found=1}' ex_file.txt 
A good line to include
And this line
Yep
$

Explicación:

  1. Si se encuentra la línea final, no se debe imprimir. Tenga en cuenta que esta comprobación se realiza antes de la impresión real para excluir la línea final del resultado.
  2. Imprime la línea actual si foundestá configurada.
  3. Si se encuentra la línea de inicio, configúrela found=1para que se impriman las siguientes líneas. Tenga en cuenta que esta verificación se realiza después de la impresión real para excluir la línea de inicio del resultado.

Notas:

  • El código se basa en el hecho de que todos los valores predeterminados de awk-vars son 0 o la cadena vacía si no está definida. Esto es válido pero puede que no sea la mejor práctica, por lo que puede agregar un BEGIN{found=0}al comienzo de la expresión awk.
  • Si se encuentran varios bloques de inicio-fin , todos se imprimen.

1
Awesome Awesome ejemplo. Acabo de pasar 2 horas mirando csplit, sed y todo tipo de comandos awk más complicados. Esto no solo hizo lo que quería, sino que se mostró lo suficientemente simple como para inferir cómo modificarlo para hacer algunas otras cosas relacionadas que necesitaba. Me hace recordar que awk es genial y no solo en un indescifrable desastre. Gracias.
user1169420

{if(found) print}es un poco anti-patrón en awk, es más idiomático reemplazar el bloque con solo foundo found;si necesita otro filtro después.
user000001

@ user000001 por favor explique. No entiendo qué reemplazar y cómo. De todos modos, creo que la forma en que está escrito deja muy claro lo que está sucediendo.
UlfR

1
Reemplazaría awk '{if(found) print} /TERMINATE/{found=1}' your_filecon awk 'found; /TERMINATE/{found=1}' your_file, ambos deberían hacer lo mismo.
user000001

7

Utilice la expansión de parámetros bash de la siguiente manera:

content=$(cat file)
echo "${content#*TERMINATE}"

¿Puedes explicar qué estás haciendo?
Yugal Jindle

Copié el contenido del "archivo" en la variable $ content. Luego eliminé todos los caracteres hasta que se vio "TERMINAR". No usó la coincidencia codiciosa, pero puede usar la coincidencia codiciosa por $ {content ## * TERMINATE}.
Mu Qiao

aquí está el enlace del manual de bash: gnu.org/software/bash/manual/…
Mu Qiao

66
¿Qué pasará si el archivo tiene un tamaño de 100 GB?
Znik

1
Voto a favor: Esto es horrible (leer el archivo en una variable) e incorrecto (usar la variable sin citarlo; y debe usar adecuadamente printfo asegurarse de saber exactamente a qué está pasando echo).
tripleee

6

grep -A 10000000 'TERMINAR' archivo

  • es mucho, mucho más rápido que sed, especialmente trabajando en archivos realmente grandes. Funciona hasta 10 millones de líneas (o lo que sea que pones), por lo que no hay daño en hacer esto lo suficientemente grande como para manejar cualquier cosa que golpees.

4

Hay muchas formas de hacerlo con sedo awk:

sed -n '/TERMINATE/,$p' file

Esto busca TERMINATEen su archivo e imprime desde esa línea hasta el final del archivo.

awk '/TERMINATE/,0' file

Este es exactamente el mismo comportamiento que sed.

En caso de que sepa el número de la línea desde la que desea comenzar a imprimir, puede especificarlo junto con NR(número de registro, que finalmente indica el número de la línea):

awk 'NR>=535' file

Ejemplo

$ seq 10 > a        #generate a file with one number per line, from 1 to 10
$ sed -n '/7/,$p' a
7
8
9
10
$ awk '/7/,0' a
7
8
9
10
$ awk 'NR>=7' a
7
8
9
10

Para el número que también puede usarmore +7 file
123

Esto incluye la línea correspondiente, que no es lo que se desea en esta pregunta.
mivk

@mivk bueno, este también es el caso de la respuesta aceptada y la segunda más votada, por lo que el problema puede estar en un título engañoso.
fedorqui 'DEJA de dañar'

3

Si por alguna razón desea evitar el uso de sed, lo siguiente imprimirá la coincidencia de línea TERMINATEhasta el final del archivo:

tail -n "+$(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)" file

y lo siguiente se imprimirá desde la siguiente coincidencia de línea TERMINATEhasta el final del archivo:

tail -n "+$(($(grep -n 'TERMINATE' file | head -n 1 | cut -d ":" -f 1)+1))" file

Se necesitan 2 procesos para hacer lo que sed puede hacer en un proceso, y si el archivo cambia entre la ejecución de grep y tail, el resultado puede ser incoherente, por lo que recomiendo usar sed. Además, si el archivo no contiene TERMINATE, el primer comando falla.


el archivo se escanea dos veces. ¿Qué pasa si tiene un tamaño de 100 GB?
Znik

1
Votaron negativamente porque esta es una solución horrible, pero luego votaron porque el 90% de la respuesta son advertencias.
Físico loco


0

Esta podría ser una forma de hacerlo. Si sabe en qué línea del archivo tiene su palabra grep y cuántas líneas tiene en su archivo:

grep -A466 'TERMINAR' archivo


1
Si se conoce el número de línea, grepni siquiera se requiere; puedes usar tail -n $NUM, así que esto no es realmente una respuesta.
Samveen

-1

sed es una herramienta mucho mejor para el trabajo: archivo sed -n '/ re /, $ p'

donde re es regexp.

Otra opción es la bandera de grep --after-context. Debe pasar un número para finalizar, el uso de wc en el archivo debería proporcionar el valor correcto para detenerse. Combina esto con -n y tu expresión de coincidencia.


- after-context está bien pero no en todos los casos.
Yugal Jindle

¿Puedes sugerir algo más?
Yugal Jindle

-2

Estos imprimirán todas las líneas desde la última línea encontrada "TERMINAR" hasta el final del archivo:

LINE_NUMBER=`grep -o -n TERMINATE $OSCAM_LOG|tail -n 1|sed "s/:/ \\'/g"|awk -F" " '{print $1}'`
tail -n +$LINE_NUMBER $YOUR_FILE_NAME

Extraer un número de línea con el grepque pueda alimentarlo tailes un antipatrón derrochador. Encontrar la coincidencia e imprimir hasta el final del archivo (o, por el contrario, imprimir y detener en la primera coincidencia) se realiza eminentemente con las herramientas de expresión regular normales y esenciales. Lo masivo grep | tail | sed | awktambién es en sí mismo un uso inútil ygrep masivo de amigos .
tripleee

Creo que estaba tratando de darnos algo que encontraría la / última instancia / de 'TERMINAR' y daría las líneas de esa instancia en adelante. Otras implementaciones le brindan la primera instancia en adelante. El LINE_NUMBER probablemente debería verse así, en su lugar: LINE_NUMBER = $ (grep -o -n 'TERMINATE' $ OSCAM_LOG | tail -n 1 | awk -F: '{print $ 1}') Quizás no sea la forma más elegante, pero Parece hacer el trabajo. ^. ^
fbicknel 01 de

... o todo en una línea, pero feo: tail -n + $ (grep -o -n 'TERMINATE' $ YOUR_FILE_NAME | tail -n 1 | awk -F: '{print $ 1}') $ YOUR_FILE_NAME
fbicknel

.... e iba a volver y editar $ OSCAM_LOG en lugar de $ YOUR_FILE_NAME ... pero no puedo por alguna razón. No tengo idea de dónde vino $ OSCAM_LOG; Simplemente lo loro sin pensar. oO
fbicknel 01 de

Hacer esto solo en Awk es una tarea común en Awk 101. Si ya está utilizando una herramienta más capaz solo para obtener el número de línea, suelte taily realice la tarea en la herramienta más capaz. De todos modos, el título dice claramente "primer partido".
tripleee
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.