Usuń zakres linii powyżej wzoru za pomocą sed (lub awk)

28

Mam następujący kod, który usunie linie ze wzorem bananai 2 linie po nim:

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

Jak na razie dobrze! Ale muszę go usunąć 2 linie przed banana , ale nie mogę go z „minus” lub cokolwiek (podobny do tego, co grep -v -B2 banana filenależy zrobić, ale nie robi):

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: `-'
Teresa e Junior
źródło
1
Najprostszym jest, aby załadować wszystkie dane do tablicy, pomiń niepożądanych linie następnie wyjścia, co pozostało: awk '{l[m=NR]=$0}/banana/{for(i=NR-2;i<=NR;i++)delete l[i]}END{for(i=1;i<=m;i++)if(i in l)print l[i]}'. To nie jest wydajne, więc jest to tylko wskazówka, a nie rozwiązanie.
manatwork
6
Po prostu zrób tac file | sed ... | tac. : P
angus
@angus Nie myślałem o tym;)
Teresa e Junior
1
mogłeś to zrobić sed '/banana/,+2d' file , to też zadziała
Akaks
1
Jeśli jesteś otwarty na używanie awk, jest to dość proste: awk 'tolower($0)~/bandana/{print prev[!idx];print prev[idx]} {idx=!idx;prev[idx]=$0}' filein ponieważ jest to komentarz, a nie odpowiedź (istnieją już inne odpowiedzi), nie będę wchodził w zbyt szczegółowe szczegóły, ale sedno tego jest takie, że zawsze masz poprzednie dwa rekordy w poprz [0] poprzednie [1] „najświeższych” w zależności od tego, która iteracji, ale zawsze prev[idx], więc podczas drukowania, drukowanie w !idxówczesnego idxporządku. Niezależnie od tego naprzemiennie idxi wprowadź bieżący rekord prev[idx].
Luv2code

Odpowiedzi:

22

Sed nie cofa się: po przetworzeniu linii jest wykonywany. Zatem „znajdź linię i wydrukuj poprzednie N linii” nie zadziała tak, jak jest, w przeciwieństwie do „znajdź linię i wydrukuj następne N linii”, którą łatwo jest przeszczepić.

Jeśli plik nie jest zbyt długi, ponieważ wydajesz się być w porządku z rozszerzeniami GNU, możesz użyć tacdo odwrócenia linii pliku.

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

Innym kątem ataku jest utrzymanie przesuwanego okna w narzędziu takim jak awk. Adaptacja z Czy istnieje alternatywa dla przełączników grep -A -B -C (aby wydrukować kilka linii przed i po)? (ostrzeżenie: minimalnie przetestowane):

#!/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]} }

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

Gilles „SO- przestań być zły”
źródło
2
sedpotrafi także przesuwać okna, ale wynikowy skrypt jest zazwyczaj tak nieczytelny, że łatwiej go po prostu użyć awk.
jw013
@Gilles .. awkSkrypt nie jest całkiem poprawny ; tak jak jest, drukuje puste linie i pomija ostatnie linie. Wydaje się, że to naprawia, ale może nie być idealne lub samo w sobie: if (NR-before in h) { print...; delete...; }... oraz w ENDsekcji: for (i in h) print h[i]... Ponadto skrypt awk drukuje pasującą linię, ale tac/secwersja nie; ale pytanie jest trochę dwuznaczne w tej kwestii. „Oryginalny” skrypt awk, do którego podałeś link, działa dobrze. Podoba mi się… Nie jestem pewien, jak powyższy „mod” wpływa na wydruk po linie ...
Peter.O
@ Peter.O Dzięki, skrypt awk powinien być teraz lepszy. I zajęło mi to mniej niż 6–8 lat!
Gilles „SO- przestań być zły”
19

Jest to dość łatwe w ex lub vim -e

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

Wyrażenie brzmi: dla każdej linii zawierającej banana w zakresie od bieżącej linii -2 do bieżącej linii usuń.

Fajne jest to, że zakres może również zawierać wyszukiwania do tyłu i do przodu, na przykład spowoduje to usunięcie wszystkich sekcji pliku, zaczynając od linii zawierającej jabłko i kończąc na linii zawierającej pomarańczowy i zawierający linię z bananem:

    vim -e - $file <<@@@
g/banana/?apple?,/orange/d
wq
@@@
Justin Rowe
źródło
7

Korzystanie z „przesuwanego okna” w perl:

perl -ne 'push @lines, $_;
          splice @lines, 0, 3 if /banana/;
          print shift @lines if @lines > 2
          }{ print @lines;'
choroba
źródło
6

Możesz to zrobić po prostu za pomocą sed:

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

Nie wiem, dlaczego ktokolwiek powiedziałby inaczej, ale aby znaleźć linię i wydrukować poprzednie linie, sed zawiera wbudowany Pprymityw rint, który zapisuje tylko do pierwszego \nznaku ewline w przestrzeni wzorów. Uzupełniający Delement podstawowy elete usuwa ten sam segment przestrzeni wzorów przed rekurencyjnym recyklingiem skryptu z tym, co pozostało. Aby go zaokrąglić, istnieje prymitywne dodanie Nlinii wejściowej ext do przestrzeni wzorów po wstawionym \nznaku ewline.

Tak więc jedna linia sedpowinna być wszystkim, czego potrzebujesz. Po prostu zamieniasz na matchdowolne wyrażenie regularne i jesteś złoty. To też powinno być bardzo szybkie rozwiązanie.

Zauważ również, że poprawnie policzy matchbezpośrednio poprzedzający inny matchjako wyzwalacz wyciszenia wyjścia dla dwóch poprzednich wierszy i wyciszenia również jego wydruku:


1
7match
8
11match

Aby działał na dowolnej liczbie wierszy, wystarczy zdobyć trop.

Więc:

    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

... usuwa 5 wierszy poprzedzających dowolne dopasowanie.

mikeserv
źródło
1

Używanie 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
larz
źródło