bash encontrar linhas começando com string

10

Eu tenho vários arquivos e quero descobrir qual deles contém linhas seqüenciais começando com uma determinada string.

Por exemplo, para o seguinte arquivo:

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

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

Aaaaaaaaaaaa
Baaaaaaaaaaa
Cxxxxxxxxx
Abbbbbbbbbbb
Bbbbbbbbbbbb
Caaaaaa
Accccccccccc
Bccccccccccc
Cdddddd

Sempre há uma linha começando com 'C', não quero esse arquivo. Pensei em usar um grepou um, sedmas não sei exatamente como fazê-lo. Talvez usando um regexp ^C.*$^Cou algo parecido. Qualquer ideia ?

Jérémie
fonte
Há duas linhas começando Cno seu segundo exemplo.
cuonglm
5
Esta questão não é clara. Você está procurando arquivos com mais de uma linha consecutiva começando C?
Graeme
Sim, é isso que eu quero. Desculpe pelo mal entendido.
Jérémie 25/03
2
@terdon, parece que pesquisas em várias linhas com -P funcionaram até 2.5.4 e não mais depois disso, embora eu não consiga encontrar nada no changelog que explique o porquê.
Stéphane Chazelas 25/03
1
@ Graeme, você pode querer recuperar sua resposta, consulte o comentário de Stephane, aparentemente funciona para algumas grepversões mais antigas.
terdon

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 completamente com as awkimplementações que não suportam nextfile).


Com versões do GNU grepaté 2.5.4:

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

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

Antes de ser corrigido no 2.6 (por esse commit ), o GNU grephavia ignorado que a função de pesquisa de pcre que estava usando corresponderia a todo o buffer atualmente processado grep, causando todo tipo de comportamento surpreendente. Por exemplo:

grep -P 'a\s*b'

corresponderia a um arquivo contendo:

bla
bla

Isso corresponderia a:

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 1\n2\né através de dois buffers processados ​​por grep).

Esse comportamento acabou sendo documentado:

15- Como posso combinar entre linhas?

O grep padrão não pode fazer isso, pois é fundamentalmente baseado em linhas. Portanto, o simples uso da classe de caracteres '[: space:]' não corresponde às novas linhas da maneira que você espera. No entanto, se o seu grep for compilado com os padrões Perl ativados, o modificador do Perl (que faz '.' Corresponder às novas linhas) poderá ser usado:

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

Depois de corrigida na versão 2.6, a documentação não foi alterada (uma vez relatei ).

Stéphane Chazelas
fonte
Existe alguma razão para não usar exite em -exec \;vez de nextfile?
terdon
@terdon, isso significaria executar um awkpor arquivo. Você gostaria de fazer isso apenas se awknão suportar nextfilee tiver uma grande proporção de arquivos grandes e com linhas correspondentes no início do arquivo.
Stéphane Chazelas 25/03
Que tal essa técnica grep (eu acho que com versões mais recentes do GNU grep) que facilita as correspondências multilinhas, fazendo com que o arquivo inteiro pareça uma única string ao definir o terminador de linha como NUL - você saberia se há alguma limitação?
iruvar 27/03
1
@ 1_CR, Isso carregaria o arquivo inteiro na memória se não houvesse um caractere NUL e que assume que as linhas não contêm caracteres NUL. Observe também que as versões mais antigas do GNU grep (que o OP tem) não pode usar -zcom -P. Há nenhum \N, sem -P, você precisa escrever $'[\01-\011\013-\0377]'o que só o trabalho em locais C (ver thread.gmane.org/gmane.comp.gnu.grep.bugs/5187 )
Stéphane Chazelas
@StephaneChazelas, detalhe muito útil, graças
Iruvar
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 começando com a C. A expressão (p ~ /^C/ && $1 ~ /^C/)procurará linhas sucessivas no arquivo e será avaliada como verdadeira se o primeiro caractere em ambas corresponder C. Se for esse o caso, a linha será impressa.

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

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

Nesse comando, o find+ execpercorrerá cada um dos arquivos e executará uma awkfiltragem semelhante em cada arquivo e imprimirá seu nome via FILENAMEse a expressão awk for avaliada como verdadeira. Para evitar a impressão FILENAMEvárias vezes para um único arquivo com várias correspondências, a exitinstrução é usada (obrigado @terdon).

mkc
fonte
Minha pergunta não foi clara o suficiente, quero saber o nome dos arquivos com mais de uma linha consecutiva iniciada com:C
Jérémie
@ Jérémie Atualizei minha resposta.
Mkc
Você poderia adicionar uma explicação de como isso funciona? Além disso, não há necessidade flag, apenas pelo exitcontrário. Dessa forma, você não precisa continuar processando arquivos depois que uma correspondência foi encontrada.
terdon
2

Mais uma 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 podem ser lidos).

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-o:

find . -type f -size +2c -exec sed -n '$q1;/^C/{n;/^C/q}' {} \; -print
pressa
fonte
Você pode por favor detalhar o sed -n '$q1;/^C/{n;/^C/q}'?
31514 Jérémie
Alguém para me explicar?
Jérémie
@ Jérémie $q1- força o sed a sair com um erro se o padrão não for encontrado. Ele também terminará com erro se algo estiver errado com o arquivo (é ilegível ou quebrado). Portanto, ele sairá com o status de saída 0 apenas se o padrão for encontrado e será passado para impressão. Parte com /^C/{n;/^C/qé bastante simples. Se encontrar a sequência que começa com C, lerá a próxima linha e, se também iniciar com C, será encerrada com status de saída zero.
apressar
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: definido \n\ncomo separador de registros, ativa o modo de parágrafo, que tratará parágrafos (separados por novas linhas consecutivas) como linhas únicas.
  • -ne: aplique o script fornecido como argumento -ea cada linha do (s) arquivo (s) de entrada.
  • $ARGV : é o arquivo que está sendo processado atualmente
  • /^C[^\n]*\nC/: corresponda Cno início de uma linha (veja a descrição dos smmodificadores abaixo para saber por que isso funciona aqui) seguido por 0 ou mais caracteres que não são de nova linha, uma nova linha e depois outro C. Em outras palavras, encontre linhas consecutivas começando com C. * //sm: esses modificadores de correspondência são (conforme documentado [aqui]):

    • m : Trate a string como várias linhas. Ou seja, altere "^" e "$" de coincidir com o início ou o fim da linha apenas nas extremidades esquerda e direita da string para correspondê-los em qualquer lugar da string.

    • s : Trate a corda como uma única linha. Ou seja, mude "." para corresponder a qualquer caractere, mesmo uma nova linha, que normalmente não corresponderia.

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 perlcódigo substitui novas linhas com %%isso, supondo que você não tem %%no seu arquivo de entrada (grande , se é claro), o grepirá corresponder linhas consecutivas começando com C.

terdon
fonte
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 acima cria 26 arquivos /tmpnomeados file1-26. Em cada arquivo, há 27 ou 28 linhas começando com as letras a-ze seguidas pelo restante do alfabeto. Cada terceiro arquivo contém duas linhas consecutivas nas quais o primeiro caractere é duplicado.

AMOSTRA:

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

E quando eu mudo:

set -- *files

para:

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

Eu recebo...

RESULTADO:

/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 posicionais subnível para todos os seus arquivos, e para cada

sets posicionais de um subshell aninhado à primeira letra de cada linha em cada arquivo, conforme ele faz um loop.

[ tests ]se $1negar, $2indicando uma correspondência e, em caso afirmativo

echoeso nome do arquivo breaké a iteração do loop atual

mais shifts para o próximo caractere único posicional para tentar novamente

mikeserv
fonte
0

Este script usa grepe cutpara obter números de linhas correspondentes e verifica se há dois números consecutivos. O arquivo é assumido como 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
Michael Martinez
fonte