bash buscar líneas que comienzan con una cadena

10

Tengo un montón de archivos y quiero encontrar cuál contiene líneas secuenciales que comienzan con una cadena determinada.

Por ejemplo para el siguiente archivo:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Cyyyyyyyyy
Czzzzzzzzz
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd
Ceeeeee

Hay más de una línea que comienza con 'C', por lo que quiero que este archivo se encuentre por comando.
Por ejemplo para el siguiente archivo:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Siempre hay una línea que comienza con 'C', no quiero este archivo. Pensé en usar a grepo a sedpero no sé exactamente cómo hacerlo. Tal vez usando una expresión regular ^C.*$^Co algo así. Alguna idea ?

Jérémie
fuente
Hay dos líneas que comienzan Cen su segundo ejemplo.
Cuonglm
55
Esta pregunta no está clara. ¿Está buscando archivos que tengan más de una línea consecutiva comenzando C?
Graeme
Sí, esto es lo que quiero. Perdón por el malentendido.
Jérémie
2
@terdon, parece que las búsquedas de varias líneas con -P funcionaron hasta 2.5.4 y no más después de eso, aunque no puedo encontrar nada en el registro de cambios que explique por qué.
Stéphane Chazelas
1
@Graeme, es posible que desee recuperar su respuesta, vea el comentario de Stephane, aparentemente funciona para algunas grepversiones anteriores .
terdon

Respuestas:

5

Con pcregrep:

pcregrep -rMl '^C.*\nC' .

POSIXY:

find . -type f -exec awk '
  FNR==1 {last=0; printed=0; next}
  printed {next}
  /^C/ {if (last) {print FILENAME; printed=1; nextfile} else last=1; next}
  {last=0}' {} +

(aunque eso significa leer todos los archivos completamente con aquellas awkimplementaciones que no son compatibles nextfile).


Con versiones de GNU grephasta 2.5.4:

grep -rlP '^C.*\nC' .

parece funcionar, pero es por accidente y no se garantiza que funcione.

Antes de que se corrigiera en 2.6 (por este commit ), GNU grephabía pasado por alto que la función de búsqueda de pcre que estaba usando coincidiría con todo el búfer procesado actualmente grep, causando todo tipo de comportamiento sorprendente. Por ejemplo:

grep -P 'a\s*b'

coincidiría en un archivo que contiene:

bla
bla

Esto coincidiría con:

printf '1\n2\n' | grep -P '1\n2'

Pero esto:

(printf '1\n'; sleep 1; printf '2\n') | grep -P '1\n2'

O:

(yes | head -c 32766; printf '1\n2\n') > file; grep -P '1\n2' file

no lo haría (ya que 1\n2\nestá en dos buffers procesados ​​por grep).

Sin embargo, ese comportamiento terminó siendo documentado:

15- ¿Cómo puedo hacer coincidir líneas?

Grep estándar no puede hacer esto, ya que se basa fundamentalmente en líneas. Por lo tanto, el simple uso de la clase de caracteres '[: espacio:]' no coincide con las nuevas líneas de la manera que cabría esperar. Sin embargo, si su grep se compila con patrones Perl habilitados, se puede usar el modificador 's' de Perl (que hace que las líneas '.' Coincidan):

     printf 'foo\nbar\n' | grep -P '(?s)foo.*?bar'

Después de que se corrigió en 2.6, la documentación no se modificó (una vez lo informé allí ).

Stéphane Chazelas
fuente
¿Hay alguna razón para no usar exity en -exec \;lugar de nextfile?
terdon
@terdon, eso significaría ejecutar uno awkpor archivo. Desea hacer eso solo si awkno es compatible nextfiley tiene una gran proporción de archivos que son grandes y tienen líneas coincidentes hacia el comienzo del archivo.
Stéphane Chazelas
¿Qué tal esta técnica grep (supongo que con versiones más recientes de GNU grep) que facilita las coincidencias multilínea al hacer que todo el archivo se vea como una sola cadena al configurar el terminador de línea en NUL? ¿Sería consciente de si hay alguna limitación?
iruvar
1
@ 1_CR, eso cargaría todo el archivo en la memoria si no hay caracteres NUL allí y se supone que las líneas no contienen caracteres NUL. También tenga en cuenta que las versiones anteriores de GNU grep (que tiene el OP) no se pueden usar -zcon -P. No hay \Nsin -P, tendrías que escribirlo, $'[\01-\011\013-\0377]'que solo funcionaría en C locales (ver thread.gmane.org/gmane.comp.gnu.grep.bugs/5187 )
Stéphane Chazelas
@StephaneChazelas, detalle muy útil, gracias
iruvar
2

Con awk:

awk '{if (p ~ /^C/ && $1 ~ /^C/) print; p=$1}' afile.txt

Esto imprimirá el contenido del archivo si hay líneas consecutivas que comienzan con a C. La expresión (p ~ /^C/ && $1 ~ /^C/)buscará líneas sucesivas en el archivo y se evaluará como verdadero si el primer carácter de ambos coincide C. Si ese es el caso, se imprimirá la línea.

Para encontrar todos los archivos que tienen dicho patrón, puede ejecutar el awk anterior a través de un findcomando:

find /your/path -type f -exec awk '{if (p ~ /^C/ && $1 ~ /^C/) {print FILENAME; exit;} p=$1}' {} \;

En este comando, el find+ execpasará por cada uno de los archivos y realizará un awkfiltrado similar en cada archivo e imprimirá su nombre FILENAMEsi la expresión awk se evalúa como verdadera. Para evitar imprimir FILENAMEvarias veces para un solo archivo con múltiples coincidencias exit, se utiliza la declaración (gracias @terdon).

mkc
fuente
Mi pregunta no era lo suficientemente clara, quiero saber el nombre de los archivos con más de una línea consecutiva que comienza conC
Jérémie
@ Jérémie Actualicé mi respuesta.
mkc
¿Podría por favor agregar una explicación de cómo funciona esto? Además, no es necesario flag, solo en su exitlugar. De esa manera, no necesita seguir procesando archivos después de encontrar una coincidencia.
terdon
2

Otra opción más con GNU sed:

Para un solo archivo:

sed -n -- '/^C/{n;/^C/q 1}' "$file" || printf '%s\n' "$file"

(aunque también informará los archivos que no puede leer).

Para find:

find . -type f ! -exec sed -n '/^C/{n;/^C/q 1}' {} \; -print

El problema con los archivos ilegibles que se imprimen se puede evitar escribiéndolo:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
prisa
fuente
¿Puedes por favor detallar el sed -n '$q1;/^C/{n;/^C/q}'?
Jérémie
¿Alguien que me explique?
Jérémie
@ Jérémie $q1: obliga a sed a salir con un error si no se encuentra el patrón. También terminará con un error si algo está mal con el archivo (es ilegible o está roto). Por lo tanto, se cerrará con el estado de salida 0 solo en caso de que se encuentre un patrón y se pase a imprimir. Parte con /^C/{n;/^C/qes bastante simple. Si encuentra una cadena que comienza con C, leerá la siguiente línea y si también comienza con C, se cerrará con el estado de salida cero.
prisa
1

Asumiendo que sus archivos son lo suficientemente pequeños como para ser leídos en la memoria:

perl -000ne 'print "$ARGV\n" if /^C[^\n]*\nC/sm' *

Explicación:

  • - 000: establecido \n\ncomo separador de registros, esto activa el modo de párrafo que tratará los párrafos (separados por nuevas líneas consecutivas) como líneas individuales.
  • -ne: aplica el script dado como argumento -ea cada línea de los archivos de entrada.
  • $ARGV : es el archivo que se está procesando actualmente
  • /^C[^\n]*\nC/: coincide Cal principio de una línea (consulte la descripción de los smmodificadores a continuación para saber por qué esto funciona aquí) seguido de 0 o más caracteres que no sean de nueva línea, una nueva línea y luego otra C. En otras palabras, encuentre líneas consecutivas que comiencen por C. * //sm: estos modificadores de coincidencia son (como se documenta [aquí]):

    • m : trata la cadena como varias líneas. Es decir, cambie "^" y "$" para que coincidan con el inicio o el final de la línea solo en los extremos izquierdo y derecho de la cadena para que coincidan en cualquier lugar dentro de la cadena.

    • s : Tratar la cadena como una sola línea. Es decir, cambiar "." para que coincida con cualquier personaje, incluso una nueva línea, que normalmente no coincidiría.

También podrías hacer algo feo como:

for f in *; do perl -pe 's/\n/%%/' "$f" | grep -q 'C[^%]*%%C' && echo "$f"; done

En este caso, el perlcódigo reemplaza los saltos de línea, con %%lo que, suponiendo que no tiene %%en su archivo de entrada (grande si , por supuesto), la grepcoincidirá con líneas consecutivas comenzando con C.

terdon
fuente
1

SOLUCIÓN:

( set -- *files ; for f ; do (
set -- $(printf %c\  `cat <$f`)
while [ $# -ge 1 ] ;do [ -z "${1#"$2"}" ] && {
    echo "$f"; break ; } || shift
done ) ; done )

MANIFESTACIÓN:

Primero, crearemos una base de prueba:

abc="a b c d e f g h i j k l m n o p q r s t u v w x y z" 
for l in $abc ; do { i=$((i+1)) h= c= ;
    [ $((i%3)) -eq 0 ] && c="$l" h="${abc%"$l"*}"
    line="$(printf '%s ' $h $c ${abc#"$h"})"
    printf "%s$(printf %s $line)\n" $line >|/tmp/file${i}
} ; done

Lo anterior crea 26 archivos en /tmpnombre file1-26. En cada archivo hay 27 o 28 líneas que comienzan con las letras a-zy seguidas del resto del alfabeto. Cada tercer archivo contiene dos líneas consecutivas en las que se duplica el primer carácter.

MUESTRA:

cat /tmp/file12
...
aabcdefghijkllmnopqrstuvwxyz
babcdefghijkllmnopqrstuvwxyz
cabcdefghijkllmnopqrstuvwxyz
...
kabcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
labcdefghijkllmnopqrstuvwxyz
mabcdefghijkllmnopqrstuvwxyz
...

Y cuando cambio:

set -- *files

a:

set -- /tmp/file[0-9]*

Yo obtengo...

SALIDA:

/tmp/file12
/tmp/file15
/tmp/file18
/tmp/file21
/tmp/file24
/tmp/file3
/tmp/file6
/tmp/file9

Entonces, en resumen, la solución funciona así:

sets posicionales de subshell para todos sus archivos, y para cada

sets las posiciones de una subshell anidada a la primera letra de cada línea en cada archivo a medida que se repite.

[ tests ]si $1niega $2indicar una coincidencia, y si es así

echoesel nombre de archivo a continuación, breakes la iteración actual del bucle

más shifts al siguiente carácter posicional para intentar de nuevo

mikeserv
fuente
0

Este script usa grepy cutpara obtener números de línea de líneas coincidentes, y verifica dos números consecutivos. Se supone que el archivo tiene un nombre de archivo válido pasado como primer argumento para el script:

#!/bin/bash

checkfile () {
 echo checking $1
 grep -n -E "^C.*$" $1 | cut -d: -f1 | while read linenum
     do
        : $[ ++PRV ] 
        if [ $linenum == $PRV ]; then return 1; fi
        PRV=$linenum
     done
     return 0
}

PRV="-1"
checkfile $1
if [ $? == 0 ]; then
   echo Consecutive matching lines found in file $1
fi
Michael Martinez
fuente