Exclui o intervalo de linhas acima do padrão com sed (ou awk)

22

Eu tenho o seguinte código que removerá as linhas com o padrão banana e 2 linhas depois disso:

sed '/banana/I,+2 d' file

Até agora, tudo bem! Mas preciso remover 2 linhas antes de banana , mas não consigo usar um sinal de menos ou o que for (semelhante ao que grep -v -B2 banana file deveria fazer, mas não faz):

teresaejunior@localhost ~ > LC_ALL=C sed '-2,/banana/I d' file
sed: invalid option -- '2'
teresaejunior@localhost ~ > LC_ALL=C sed '/banana/I,-2 d' file
sed: -e expression #1, char 16: unexpected ','
teresaejunior@localhost ~ > LC_ALL=C sed '/banana/I,2- d' file
sed: -e expression #1, char 17: unknown command: '-'
    
por Teresa e Junior 24.01.2012 / 17:09

5 respostas

16

Sed não recua: uma vez processada uma linha, é feito. Então, “encontrar uma linha e imprimir as N linhas anteriores” não vai funcionar como está, ao contrário de “encontrar uma linha e imprimir as próximas N linhas”, que é fácil de enxertar.

Se o arquivo não for muito longo, já que você parece estar bem com as extensões GNU, você pode usar tac para reverter as linhas do arquivo.

tac | sed '/banana/I,+2 d' | tac

Outro ângulo de ataque é manter uma janela deslizante em uma ferramenta como o awk. Adaptação de Existe alguma alternativa para os switches -A -B-C do grep (para imprimir algumas linhas antes e depois)? (aviso: minimamente testado):

#!/bin/sh
{ "exec" "awk" "-f" "$0" "$@"; } # -*-awk-*-
# The array h contains the history of lines that are eligible for being "before" lines.
# The variable skip contains the number of lines to skip.
skip { --skip }
match($0, pattern) { skip = before + after }
NR > before && !skip { print NR h[NR-before] }
{ delete h[NR-before]; h[NR] = $0 }
END { if (!skip) {for (i=NR-before+1; i<=NR; i++) print h[i]} }

Uso: /path/to/script -v pattern='banana' -v before=2

    
por 24.01.2012 / 19:55
18

Isso é muito fácil com ex ou vim -e

    vim -e - $file <<@@@
g/banana/.-2,.d
wq
@@@

A expressão lê: para cada linha contendo banana no intervalo da linha atual -2 para a linha atual, delete.

O legal é que o intervalo também pode conter pesquisas para trás e para frente, por exemplo, isso excluirá todas as seções do arquivo começando com uma linha contendo maçã e terminando com uma linha contendo laranja e contendo uma linha com banana:

    vim -e - $file <<@@@
g/banana/?apple?,/orange/d
wq
@@@
    
por 03.06.2014 / 15:00
6

Usando a "janela deslizante" em perl :

perl -ne 'push @lines, $_;
          splice @lines, 0, 3 if /banana/;
          print shift @lines if @lines > 2
          }{ print @lines;'
    
por 24.01.2012 / 17:59
5

Você pode fazer isso de forma bastante simples com sed :

printf %s\n    1 2 3 4match 5match 6 \
                7match 8 9 10 11match |
sed -e'1N;$!N;/\n.*match/!P;D'

Não sei por que alguém diria o contrário, mas encontrar uma linha e imprimir linhas anteriores sed incorpora a primitiva P rint integrada, que grava apenas até a primeira \n caractere de ewline no espaço padrão. A primitiva complementar D elete remove o mesmo segmento de espaço de padrão antes de recursivamente reciclar o script com o que resta. E, para arredondá-lo, há uma primitiva para anexar a linha de entrada N ext ao espaço padrão seguindo um caractere \n ewline inserido.

Para que uma linha de sed seja tudo que você precisa. Você acabou de substituir match por qualquer que seja seu regexp e você é de ouro. Essa também deve ser uma solução muito rápida .

Note também que ele irá contar corretamente um match imediatamente antes de outro match como um gatilho para acalmar a saída das duas linhas anteriores e acalme sua impressão também:

1
7match
8
11match

Para que funcione para um número arbitrário de linhas, tudo o que você precisa fazer é obter uma pista.

Então:

    printf %s\n     1 2 3 4 5 6 7match     \
                     8match 9match 10match  \
                     11match 12 13 14 15 16 \
                     17 18 19 20match       |
    sed -e:b -e'$!{N;2,5bb' -e\} -e'/\n.*match/!P;D'
1
11match
12
13
14
20match

... exclui as 5 linhas que precedem qualquer correspondência.

    
por 15.12.2015 / 06:31
1

Usando man 1 ed :

str='
1
2
3
banana
4
5
6
banana
8
9
10
'

# using Bash
cat <<-'EOF' | ed -s <(echo "$str")  | sed -e '1{/^$/d;}' -e '2{/^$/d;}'
H
0i


.
,g/banana/km\
'm-2,'md
,p
q
EOF
    
por 19.04.2013 / 18:05