Como grep-inverso-match e excluir linhas "antes" e "depois"


26

Considere um arquivo de texto com as seguintes entradas:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Dado um padrão (por exemplo fff), gostaria de cumprimentar o arquivo acima para obter a saída:

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

Por exemplo, se B = 2e A = 1, a saída com padrão = fffdeve ser:

aaa
bbb
ccc
hhh
iii

Como posso fazer isso com grep ou outras ferramentas de linha de comando?


Observe que quando tento:

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

Eu não consigo o que quero. Em vez disso, recebo:

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

Respostas:


9

don's pode ser melhor na maioria dos casos, mas caso o arquivo seja realmente grande e você não consiga sedlidar com um arquivo de script tão grande (o que pode acontecer em mais de 5000 linhas de script) , aqui está o seguinte 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 é um exemplo do que é chamado de janela deslizante na entrada. Ele funciona criando um buffer antecipado de $Blinhas -count antes de tentar imprimir qualquer coisa.

E, na verdade, provavelmente devo esclarecer meu argumento anterior: o limitador de desempenho primário para essa solução e don's estará diretamente relacionado ao intervalo. Esta solução irá diminuir com intervalo maior tamanhos , enquanto Don irá diminuir com intervalo maior frequência . Em outras palavras, mesmo que o arquivo de entrada seja muito grande, se a ocorrência real do intervalo ainda for muito pouco frequente, sua solução provavelmente é o caminho a percorrer. No entanto, se o tamanho do intervalo for relativamente gerenciável e provavelmente ocorrer com freqüência, então essa é a solução que você deve escolher.

Então, aqui está o fluxo de trabalho:

  • Se $matchfor encontrado no espaço do padrão precedido por uma linha de \new, elimina sedrecursivamente Dcada linha de \new que o precede.
    • Eu estava limpando $matcho espaço do padrão completamente antes - mas lidar facilmente com a sobreposição, deixar um ponto de referência parece funcionar muito melhor.
    • Também tentei s/.*\n.*\($match\)/\1/fazê-lo de uma só vez e desviar do loop, mas quando $A/$Bsão grandes, o Dloop elete se mostra consideravelmente mais rápido.
  • Em seguida, puxamos a Nlinha ext de entrada precedida por um \ndelimitador de linha ew e tentamos novamente Dexcluir uma /\n.*$match/vez mais, referindo-se à nossa expressão regular usada mais recentemente com //.
  • Se o espaço do padrão corresponder $match, ele poderá fazê-lo apenas $matchno início da linha - todas as $Blinhas anteriores foram limpas.
    • Então começamos a repetir depois $A.
    • Cada execução deste ciclo vamos tentar s///ubstitute para &si o $Ath \npersonagem ewline no espaço de padrões, e, se bem sucedida, test nos ramificar - e toda a nossa $Atampão epois - fora do roteiro inteiramente para iniciar o script ao longo do topo com a próxima linha de entrada, se houver.
    • Se o test não for bem-sucedido, bvoltaremos ao :trótulo op e recuaremos para outra linha de entrada - possivelmente iniciando o loop se $matchocorrer durante a coleta $Aposterior.
  • Se passar por um $matchcircuito de função, então vamos tentar print a $última linha, se é isso, e se !não tentar s///ubstitute para &si o $Bth \npersonagem ewline no espaço padrão.
    • Também determinaremos tisso e, se for bem-sucedido, ramificaremos para o :Prótulo da rint.
    • Caso contrário, voltaremos à :toperação e obteremos outra linha de entrada anexada ao buffer.
  • Se fizermos o rint , o rint :Pserá eliminado até o primeiro ewline no espaço do padrão e reexecutar o script de cima com o que resta.PD\n

E desta vez, se estivéssemos fazendo A=2 B=2 match=5; seq 5 | sed...

O espaço do padrão para a primeira iteração no :Print seria semelhante a:

^1\n2\n3$

E é assim que sedreúne seu $Bbuffer anterior. E assim sedimprime $Bnas linhas de contagem de saída atrás da entrada que ela coletou. Isto significa que, dado nosso exemplo anterior, sedseria Print 1para a saída, e depois Delete isso e enviar de volta para o topo do script um espaço padrão que se parece com:

^2\n3$

... e na parte superior do script, a Nlinha de entrada ext é recuperada e, portanto, a próxima iteração se parece com:

^2\n3\n4$

E assim, quando encontramos a primeira ocorrência de 5in input, o espaço do padrão se parece com:

^3\n4\n5$

Em seguida, o Dloop elete entra em ação e, quando termina, parece:

^5$

E quando a Nlinha de entrada ext é puxada, sedatinge EOF e sai. Naquela época, apenas Pas linhas 1 e 2 foram criadas.

Aqui está um exemplo de execução:

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'

Isso 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

Na verdade, estou trabalhando com arquivos enormes, e a resposta de don foi notavelmente mais lenta que esta solução. Inicialmente, hesitei em alterar minha resposta aceita, mas a diferença de velocidade é bastante visível.
Amelio Vazquez-Reina

4
@Amelio - isso funcionará com um fluxo de qualquer tamanho e não precisará ler o arquivo até o trabalho. O maior fator de desempenho é o tamanho de $Ae / ou $B. Quanto maiores esses números, mais lento ele fica - mas você pode aumentá-los razoavelmente.
precisa saber é o seguinte

1
@ AmelioVazquez-Reina - se você estiver usando o mais antigo, é melhor, eu acho.
mikeserv

11

Você pode usar gnu grepcom -Ae -Bimprimir exatamente as partes do arquivo que deseja excluir, mas adicionar a -nopção para também imprimir os números de linha, formatar a saída e passá-la como um script de comando sedpara excluir essas linhas:

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

Isso também deve funcionar com arquivos de padrões passados ​​para grepvia, -fpor exemplo:

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

Eu acho que isso poderia ser um pouco otimizado se ele colapsasse três ou mais números de linhas consecutivos em intervalos, de modo a ter, por exemplo, em 2,6dvez de 2d;3d;4d;5d;6d... embora se a entrada tiver apenas algumas correspondências, não valha a pena.


Outras maneiras que não preservam a ordem das linhas e são provavelmente mais lentas:
com comm:

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

commrequer entrada classificada, o que significa que a ordem das linhas não seria preservada na saída final (a menos que seu arquivo já esteja classificado); portanto, nlé usada para numerar as linhas antes da classificação, comm -13imprime apenas linhas exclusivas para o 2º ARQUIVO e cutremove a parte adicionada por nl(ou seja, o primeiro campo e o delimitador :)
com join:

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

Graças Don! Pergunta rápida, você esperaria que a solução com commfosse mais rápida que a original com sede grep?
Amelio Vazquez-Reina

1
@ AmelioVazquez-Reina - Acho que não, pois ele ainda lê o arquivo de entrada duas vezes (além de fazer alguma classificação), em oposição à solução de Mike, que processa o arquivo apenas uma vez.
don_crissti

9

Se você não se importa em 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
  • -Nesativa o modo ex silencioso e não compatível. Útil para scripts.
  • +{command}diga ao vim para executar {command}no arquivo.
  • g/${PAT}/- em todas as linhas correspondentes /fff/. Isso fica complicado se o padrão contiver caracteres especiais de expressão regular que você não pretendia tratar dessa maneira.
  • .-${B} - a partir de 1 linha acima desta
  • .+${A}- a 2 linhas abaixo desta (veja :he cmdline-rangespara estas duas)
  • d - exclua as linhas.
  • +w !tee depois escreve na saída padrão.
  • +q! fecha sem salvar as alterações.

Você pode pular as variáveis ​​e usar o padrão e os números diretamente. Eu os usei apenas para fins de clareza.


3

Que tal (usando GNU grepe bash):

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

Aqui, encontramos as linhas a serem descartadas e grep -B2 -A1 'fff' file.txt, em seguida, usamos isso como um arquivo de entrada para encontrar as linhas desejadas descartando-as.


Hmm, este não exibe nenhum output na minha máquina (OS X)
Amelio Vazquez-Reina

@ AmelioVazquez-Reina muito sobre that..i não sabia o seu OS before..anyway Eu testei isso no Ubuntu ..
heemayl

2
Isso teria o mesmo problema que kosa solução (agora excluída), como se houvesse linhas duplicadas no arquivo de entrada e algumas delas estivessem fora do intervalo e outras dentro desse intervalo, isso excluirá todas elas. Além disso, com várias ocorrências de padrão , se houver linhas como --no arquivo de entrada (fora dos intervalos), elas serão excluídas porque o delimitador --aparecerá na grepsaída da saída quando mais de uma linha corresponder ao padrão (a última é altamente improvável, mas vale a pena). mencionando eu acho).
don_crissti

@don_crissti Thanks..you são right..although eu estava tomando o exemplo do OP literally..i am vai deixá-lo no caso de alguém encontrá-lo útil mais tarde ..
heemayl

1

Você pode obter um resultado suficientemente bom usando arquivos temporários:

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

O resultado é bom o suficiente, pois você pode perder alguma indentação no processo, mas se for um arquivo insensível a xml ou indentação, não deverá ser um problema. Como esse script usa uma unidade ram, escrever e ler esses arquivos temporários é tão rápido quanto trabalhar na memória.


1

Além disso, se você quiser excluir algumas linhas antes de um determinado marcador, poderá usar:

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

(glenn jackman em /programming//a/1492538 )

Ao canalizar alguns comandos, você pode obter o comportamento antes / depois:

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
Brilhante, use awkem um arquivo invertido para lidar com as seguintes linhas quando você quiser afetar as linhas anteriores e reverter o resultado.
karmakaze 17/09

0

Uma maneira de conseguir isso, talvez a maneira mais fácil seja criar uma variável e fazer o seguinte:

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

Dessa forma, você ainda tem sua estrutura. E você pode ver facilmente do liner o que você está tentando remover.

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

mesma solução que heemayl e o mesmo problema descrito por don_crissti: Isso teria o mesmo problema da solução de kos (agora excluída) como se houvesse linhas duplicadas no arquivo de entrada e algumas delas ficassem fora do intervalo e outras dentro desse intervalo isso excluirá todos eles. Além disso, com várias ocorrências de padrão, se houver linhas como - no arquivo de entrada (fora dos intervalos), elas serão excluídas porque o delimitador - aparecerá na saída do grep quando mais de uma linha corresponder ao padrão (a última é altamente improvável, mas vale a pena mencionar, eu acho).
Bodo Thiesen 24/09

0

Se houver apenas 1 correspondência:

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

Caso contrário (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)
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.