bash encontrar linhas que começam com string

10

Eu tenho vários arquivos e quero descobrir qual deles contém linhas sequenciais que começam com uma certa string.

Por exemplo, para o seguinte arquivo:

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

Existe mais de uma linha começando com 'C', então eu quero que este arquivo seja encontrado pelo comando.
Por exemplo, para o seguinte arquivo:

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Existe sempre uma linha começando com 'C', não quero este arquivo. Pensei em usar um grep ou um sed , mas não sei exatamente como fazê-lo. Talvez usando um regexp ^C.*$^C ou algo parecido. Alguma idéia?

    
por Jérémie 25.03.2014 / 14:32

6 respostas

5

com pcregrep :

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

POSIXly:

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}' {} +

(embora isso signifique ler todos os arquivos totalmente com as implementações awk que não suportam nextfile ).

Com versões do GNU grep até 2.5.4:

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

aparece para funcionar, mas é por acidente e não é garantido que funcione.

Antes de ser corrigido em 2.6 (por este commit ), O GNU grep ignorou que a função de pesquisa do pcre que estava usando corresponderia a todo o buffer atualmente processado por grep , causando todo tipo de comportamento surpreendente. Por exemplo:

grep -P 'a\s*b'

corresponderia em um arquivo contendo:

bla
bla

Isso corresponderia:

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

Mas isso:

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

Ou:

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

não (como o 1\n2\n está em dois buffers processados por grep ).

Esse comportamento acabou sendo documentado:

15- How can I match across lines?

Standard grep cannot do this, as it is fundamentally line-based. Therefore, merely using the '[:space:]' character class does not match newlines in the way you might expect. However, if your grep is compiled with Perl patterns enabled, the Perl 's' modifier (which makes '.' match newlines) can be used:

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

Depois que foi corrigido em 2.6, a documentação não foi alterada (uma vez eu relatei que ).

    
por 25.03.2014 / 15:16
2

com awk :

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

Isso imprimirá o conteúdo do arquivo se houver linhas consecutivas iniciando com C . A expressão (p ~ /^C/ && $1 ~ /^C/) examinará linhas sucessivas no arquivo e será avaliada como verdadeira se o primeiro caractere em ambos corresponder C . Se for esse o caso, a linha será impressa.

Para encontrar todos os arquivos que possuem tal padrão, você pode executar o awk acima através de um comando find :

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

Nesse comando, o find + exec passará por cada um dos arquivos e executará a filtragem awk semelhante em cada arquivo e imprimirá seu nome via FILENAME se a expressão awk for avaliada como true. Para evitar imprimir FILENAME várias vezes para um único arquivo com várias correspondências, a instrução exit é usada (thanks @terdon).

    
por 25.03.2014 / 14:52
2

Ainda outra opção com o GNU sed :

Para um único arquivo:

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

(embora também relate os arquivos que não pode ler).

Para find :

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

O problema com arquivos ilegíveis sendo impressos pode ser evitado escrevendo:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
    
por 25.03.2014 / 17:37
1

Supondo que seus arquivos sejam pequenos o suficiente para serem lidos na memória:

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

Explicação:

  • - 000 : define \n\n como o separador de registro, isso ativa o modo de parágrafo, que tratará parágrafos (separados por novas linhas consecutivas) como linhas únicas.
  • -ne : aplica o script fornecido como um argumento para -e para cada linha do (s) arquivo (s) de entrada.
  • $ARGV : é o arquivo atualmente sendo processado
  • /^C[^\n]*\nC/ : corresponde a C no início de uma linha (veja a descrição dos modificadores sm abaixo para saber por que isso funciona aqui) seguido por 0 ou mais caracteres não pertencentes à nova linha, uma nova linha e depois outra Em outras palavras, encontre linhas consecutivas começando com C . * //sm : estes modificadores de correspondência são (conforme documentado [aqui]):

    • m : Treat string as multiple lines. That is, change "^" and "$" from matching the start or end of line only at the left and right ends of the string to matching them anywhere within the string.

    • s: Treat string as single line. That is, change "." to match any character whatsoever, even a newline, which normally it would not match.

Você também pode fazer algo feio como:

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

Aqui, o código perl substitui as novas linhas por %% , supondo que você não tenha %% em seu arquivo de entrada (é claro se ), o grep corresponderá consecutivamente linhas começando com C .

    
por 25.03.2014 / 15:33
1

SOLUÇÃO:

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

DEMO:

Primeiro, criaremos uma base de teste:

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

O texto acima cria 26 arquivos em /tmp chamado file1-26 . Em cada arquivo existem 27 ou 28 linhas começando com as letras a-z e seguido pelo resto do alfabeto. Cada terceiro arquivo contém duas linhas consecutivas em que o primeiro caractere é duplicado.

AMOSTRA:

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

E quando eu mudar:

set -- *files

para:

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

Eu recebo ...

OUTPUT:

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

Então, em resumo, a solução funciona assim:

sets subshell positionals to all of your files, and for each

sets a nested subshell's positionals to the first letter of each line in each file as it loops.

[ tests ] if $1 negates $2 indicating a match, and if so

echoes the filename then breaks the current loop iteration

     

else shift s para o próximo caractere único posicional para tentar novamente

    
por 26.03.2014 / 12:59
0

Este script usa grep e cut para obter números de linha de linhas correspondentes e verifica dois números consecutivos. O arquivo é considerado um nome de arquivo válido passado como o primeiro argumento para o 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
    
por 25.03.2014 / 22:03