Como grep-inverse-match e exclui linhas “antes” e “depois”

20

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 receber 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 = 2 e A = 1 , a saída com padrão = fff deve ser:

aaa
bbb
ccc
hhh
iii

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

Nota: quando tento:

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

Eu não entendo o que quero. Em vez disso, obtenho:

aaa
bbb
ccc
ddd
eee
fff
--
--
fff
ggg
hhh
iii
    
por Amelio Vazquez-Reina 01.07.2015 / 21:22

7 respostas

9

os don's podem ser melhores na maioria dos casos, mas apenas no caso do arquivo ser realmente grande, e você não pode obter sed para manipular um arquivo de script tão grande acontecer em torno de 5000 + linhas de script) , aqui está com 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 de look-ahead de $B -count linhas antes de tentar imprimir qualquer coisa.

E, na verdade, provavelmente devo esclarecer meu ponto anterior: o limitador principal de desempenho para essa solução e para o don estará diretamente relacionado ao intervalo. Esta solução diminuirá com intervalos maiores , enquanto os don's diminuirão com intervalos maiores freqüências . Em outras palavras, mesmo que o arquivo de entrada seja muito grande, se a ocorrência real do intervalo ainda for muito rara, sua solução provavelmente é o caminho a ser seguido. No entanto, se o tamanho do intervalo for relativamente gerenciável e for provável que ocorra com frequência, essa é a solução que você deve escolher.

Então aqui está o fluxo de trabalho:

  • Se $match for encontrado no espaço de padrão precedido por \n ewline, sed recursivamente D excluirá cada \n ewline que o precede.
    • Eu estava limpando completamente o espaço de padrão de $match - mas para lidar facilmente com a sobreposição, deixar um ponto de referência parece funcionar muito melhor.
    • Eu também tentei s/.*\n.*\($match\)// tentar obtê-lo de uma só vez e evitar o loop, mas quando $A/$B é grande, o loop D elete é consideravelmente mais rápido.
  • Em seguida, inserimos a linha N ext da entrada precedida por um delimitador \n ewline e tentamos novamente D elete a /\n.*$match/ referindo-se à expressão regular mais recentemente usada w / // .
  • Se o espaço de padrão corresponder a $match , só poderá fazê-lo com $match na parte superior da linha - todas as $B antes de as linhas terem sido limpas.
    • Então, começamos a fazer o loop em $A fter.
    • Cada execução deste loop tentará s/// ubstitute para & o caractere $A th \n ewline no espaço padrão e, se tiver êxito, t est nos ramificará - e todo o nosso $A fter buffer - fora do script inteiramente para iniciar o script do topo com a próxima linha de entrada, se houver.
    • Se o t est não for bem-sucedido, b ranch voltará ao rótulo :t op e recorrerá a outra linha de entrada - possivelmente iniciando o loop se $match ocorrer ao coletar $A após.
  • Se passarmos de um loop de função $match , tentaremos p rint a última linha $ , e se ! não tentar s/// ubstitute por & em si o caractere de ewline $B th \n no espaço padrão.
    • Estaremos t est isto também e, se for bem-sucedido, vamos ramificar para o rótulo :P rint.
    • Se não, voltaremos para :t op e obteremos outra linha de entrada anexada ao buffer.
  • Se chegarmos a :P rint, P rint, em seguida, D será excluído até o primeiro \n ewline no espaço padrão e executaremos novamente o script a partir do topo com o que resta.

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

O espaço padrão para a primeira iteração em :P rint seria semelhante a:

^1\n2\n3$

E é assim que sed reúne seu buffer $B fore. E assim, sed imprime para a saída $B -count linhas por trás da entrada que reuniu. Isso significa que, dado o nosso exemplo anterior, sed iria P rint 1 para a saída, e então D elete isso e mandaria de volta para o topo do roteiro um espaço padrão que parece:

^2\n3$

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

^2\n3\n4$

E assim, quando encontramos a primeira ocorrência de 5 na entrada, o espaço padrão realmente se parece com:

^3\n4\n5$

Em seguida, o loop D elete entra em ação e, quando passa por ele, parece:

^5$

E quando a linha de entrada N ext é extraída, sed atinge o EOF e sai. Por esse tempo, ele tem apenas P rinted linhas 1 e 2.

Veja 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
    
por 01.07.2015 / 22:47
10

Você pode usar gnu grep com -A e -B para imprimir exatamente as partes do arquivo que deseja excluir, mas adicione a opção -n para também imprimir os números de linha e formatar a saída e passá-la como um script de comando para sed para excluir essas linhas:

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

Isso também deve funcionar com arquivos de padrões transmitidos para grep via -f , por exemplo:

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

Acho que isso poderia ser um pouco otimizado se colapsasse três ou mais números de linha consecutivos em intervalos, para ter, por exemplo, 2,6d em vez de 2d;3d;4d;5d;6d ... mas se a entrada tiver apenas alguns resultados, não vale a pena fazê-lo.

Outras maneiras que não preservam a ordem da linha e provavelmente são 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-

comm requer entrada classificada, o que significa que a ordem de linha não será preservada na saída final (a menos que seu arquivo já esteja classificado), portanto nl é usado para numerar as linhas antes da classificação, comm -13 imprime somente linhas exclusivas para 2nd FILE e, em seguida, cut remove a parte que foi 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-
    
por 01.07.2015 / 21:45
7

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
  • -Nes ativa o modo ex silencioso não compatível. Útil para scripts.
  • +{command} diga ao vim para executar {command} no arquivo.
  • g/${PAT}/ - em todas as linhas que correspondem a /fff/ . Isso fica complicado se o padrão contiver caracteres especiais de expressões regulares que você não pretendia tratar dessa maneira.
  • .-${B} - de uma linha acima desta
  • .+${A} - para duas linhas abaixo desta (consulte :he cmdline-ranges para esses dois)
  • d - exclua as linhas.
  • +w !tee , em seguida, grava na saída padrão.
  • +q! sai 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 clareza de propósito.

    
por 01.07.2015 / 21:44
2

Que tal (usando o GNU grep e bash ):

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

Aqui estamos encontrando as linhas a serem descartadas por grep -B2 -A1 'fff' file.txt , usando isso como um arquivo de entrada para encontrar as linhas desejadas descartando-as.

    
por 01.07.2015 / 22:00
1

Você pode alcançar 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 porque você pode perder algum recuo no processo, mas se for um arquivo xml ou indentação insensível, isso não deve ser um problema. Como esse script usa um RAM, escrever e ler esses arquivos temporários é tão rápido quanto trabalhar na memória.

    
por 28.04.2016 / 00:13
0

Além disso, se você quiser excluir apenas algumas linhas antes de um determinado marcador, use:

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

(glenn jackman, link )

Por piping alguns comandos você pode obter o antes / depois behaivour:

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
    
por 28.04.2016 / 00:33
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

Desta forma, você ainda tem sua estrutura. E você pode ver facilmente a partir de um forro o que você está tentando remover.

$ grep -v "$(grep "fff" -A1 -B2 file.txt)" file.txt
aaa
bbb
ccc
hhh
iii
    
por 06.11.2017 / 14:46