Bagaimana cara grep-inverse-match dan mengecualikan garis "sebelum" dan "setelah"


26

Pertimbangkan file teks dengan entri berikut:

aaa
bbb
ccc
ddd
eee
fff
ggg
hhh
iii

Diberi pola (misalnya fff), saya ingin membuka file di atas untuk mendapatkan keluaran:

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

Misalnya, jika B = 2dan A = 1, output dengan pola = fffseharusnya:

aaa
bbb
ccc
hhh
iii

Bagaimana saya bisa melakukan ini dengan grep atau alat baris perintah lainnya?


Catatan, ketika saya mencoba:

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

Saya tidak mendapatkan apa yang saya inginkan. Saya malah mendapatkan:

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

Jawaban:


9

don mungkin lebih baik dalam banyak kasus, tetapi untuk berjaga-jaga jika file tersebut sangat besar, dan Anda tidak seddapat menangani file skrip sebesar itu (yang dapat terjadi pada sekitar 5000 baris naskah) , ini dia dengan polos 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'

Ini adalah contoh dari apa yang disebut jendela geser pada input. Ia bekerja dengan membangun tampak-depan penyangga dari $Bgaris -count sebelum pernah mencoba untuk mencetak apa pun.

Dan sebenarnya, mungkin saya harus mengklarifikasi poin saya sebelumnya: limiter kinerja utama untuk solusi ini dan don's akan langsung berhubungan dengan interval. Solusi ini akan melambat dengan ukuran interval yang lebih besar , sedangkan don's akan melambat dengan frekuensi interval yang lebih besar . Dengan kata lain, bahkan jika file input sangat besar, jika kejadian interval yang sebenarnya masih sangat jarang maka solusinya mungkin adalah cara untuk pergi. Namun, jika ukuran interval relatif dapat dikelola, dan cenderung sering terjadi, maka ini adalah solusi yang harus Anda pilih.

Jadi, inilah alur kerjanya:

  • Jika $matchditemukan di ruang pola yang didahului oleh \newline, sedsecara rekursif akan menghapus Dsetiap \newline yang mendahuluinya.
    • Saya telah membersihkan $matchruang pola sepenuhnya sebelumnya - tetapi untuk dengan mudah menangani tumpang tindih, meninggalkan tengara tampaknya bekerja jauh lebih baik.
    • Saya juga mencoba s/.*\n.*\($match\)/\1/mencoba mendapatkannya dalam sekali jalan dan menghindari loop, tetapi ketika $A/$Bbesar, Dloop elete terbukti jauh lebih cepat.
  • Kemudian kami menarik Ninput ext baris yang didahului oleh \npembatas ewline dan mencoba sekali lagi untuk Dmenghapus /\n.*$match/sekali lagi dengan merujuk ke ekspresi reguler kami yang terakhir digunakan w / //.
  • Jika ruang pola cocok $matchmaka hanya dapat melakukannya dengan $matchdi kepala garis - semua $Bgaris sebelumnya telah dihapus.
    • Jadi kita mulai mengulang-ulang $A.
    • Setiap menjalankan loop ini kami akan mencoba untuk s///ubstitute untuk &dirinya $Ath \nkarakter ewline di ruang pola, dan, jika berhasil, test akan cabang kami - dan kami seluruh $Aetelah penyangga - dari script sepenuhnya untuk memulai script lebih dari atas dengan jalur input berikutnya jika ada.
    • Jika test tidak berhasil, kami akan bkembali ke :tlabel op dan melakukan recurse untuk jalur input lain - mungkin memulai perulangan jika $matchterjadi saat mengumpulkan setelah itu $A.
  • Jika kita bisa melewati $matchlingkaran fungsi, maka kami akan mencoba untuk petak yang $baris terakhir jika ini itu, dan jika !tidak mencoba untuk s///ubstitute untuk &dirinya $Bth \nkarakter ewline di ruang pola.
    • Kami juga akan tmemperkirakan ini, dan jika berhasil kami akan mencabangkan ke :Plabel rintisan.
    • Jika tidak kita akan bercabang kembali ke :top dan mendapatkan baris input lain ditambahkan ke buffer.
  • Jika kita membuatnya untuk :Pmemecah kita akan Pmemecah kemudian Dmenghapus hingga baris pertama \ndi ruang pola dan menjalankan kembali skrip dari atas dengan apa yang tersisa.

Dan kali ini, jika kita lakukan A=2 B=2 match=5; seq 5 | sed...

Ruang pola untuk iterasi pertama di :Print akan terlihat seperti:

^1\n2\n3$

Dan begitulah cara sedmengumpulkan $Bbuffer sebelumnya. Dan jadi sedmencetak ke $B-count baris di belakang input yang telah dikumpulkan. Ini berarti bahwa, dengan contoh kita sebelumnya, sedakan Pmematikan 1untuk keluaran, dan kemudian Dmenghapusnya dan mengirim kembali ke atas skrip ruang pola yang terlihat seperti:

^2\n3$

... dan di bagian atas skrip, Nbaris input ekst diambil dan sehingga iterasi berikutnya terlihat seperti:

^2\n3\n4$

Jadi ketika kita menemukan kemunculan 5input pertama, ruang pola sebenarnya terlihat seperti:

^3\n4\n5$

Kemudian Dloop elete menendang dan ketika melalui itu terlihat seperti:

^5$

Dan ketika jalur Ninput ekst ditarik, sedhits EOF dan berhenti. Pada saat itu hanya ada Pjalur 1 dan 2.

Berikut ini contoh yang dijalankan:

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'

Itu mencetak:

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

Saya sebenarnya bekerja dengan file besar, dan jawaban don terasa lebih lambat daripada solusi ini. Awalnya saya ragu untuk mengubah jawaban yang saya terima, tetapi perbedaan kecepatannya cukup terlihat.
Amelio Vazquez-Reina

4
@ Amelio - ini akan bekerja dengan aliran ukuran berapa pun, dan itu tidak perlu membaca file sampai berfungsi. Faktor kinerja terbesar adalah ukuran $Adan / atau $B. Semakin besar Anda membuat angka-angka itu, semakin lambat akan diperoleh - tetapi Anda bisa membuatnya cukup besar.
mikeserv

1
@ AmelioVazquez-Reina - jika Anda menggunakan yang lebih tua, ini lebih baik, saya pikir.
mikeserv

11

Anda dapat menggunakan gnu grepdengan -Adan -Buntuk mencetak dengan tepat bagian-bagian dari file yang ingin Anda kecualikan tetapi tambahkan -nsakelar untuk juga mencetak nomor baris dan kemudian format output dan berikan sebagai skrip perintah seduntuk menghapus baris-baris itu:

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

Ini juga harus bekerja dengan file pola dilewatkan ke grepvia -fmisalnya:

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

Saya pikir ini bisa sedikit dioptimalkan jika diciutkan tiga atau lebih nomor baris berturut-turut ke dalam rentang sehingga memiliki misalnya 2,6dbukannya 2d;3d;4d;5d;6d... meskipun jika input hanya memiliki beberapa pertandingan, itu tidak layak dilakukan.


Cara lain yang tidak mempertahankan urutan baris dan kemungkinan besar lebih lambat:
dengan comm:

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

commmembutuhkan input yang diurutkan yang berarti urutan baris tidak akan dipertahankan dalam output akhir (kecuali file Anda sudah diurutkan) sehingga nldigunakan untuk memberi nomor pada baris sebelum disortir , comm -13hanya mencetak baris yang unik ke FILE ke- 2 dan kemudian cutmenghapus bagian yang ditambahkan oleh nl(yaitu, bidang pertama dan pembatas :)
dengan join:

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

Don terima kasih! Pertanyaan singkat, apakah Anda mengharapkan solusi dengan commmenjadi lebih cepat daripada yang asli dengan seddan grep?
Amelio Vazquez-Reina

1
@ AmelioVazquez-Reina - Saya tidak berpikir demikian karena masih membaca file input dua kali (plus itu melakukan penyortiran) sebagai lawan dari solusi Mike yang hanya memproses file sekali.
don_crissti

9

Jika Anda tidak keberatan menggunakan vim:

$ export PAT=fff A=1 B=2
$ vim -Nes "+g/${PAT}/.-${B},.+${A}d" '+w !tee' '+q!' foo
aaa
bbb
ccc
hhh
iii
  • -Nesmengaktifkan mode ex yang tidak kompatibel, diam. Berguna untuk skrip.
  • +{command}beri tahu vim untuk dijalankan {command}pada file.
  • g/${PAT}/- pada semua baris yang cocok /fff/. Ini menjadi rumit jika polanya berisi ekspresi reguler karakter khusus yang tidak ingin Anda perlakukan seperti itu.
  • .-${B} - dari 1 baris di atas yang ini
  • .+${A}- ke 2 baris di bawah ini (lihat :he cmdline-rangesuntuk dua ini)
  • d - hapus garis.
  • +w !tee kemudian menulis ke output standar.
  • +q! berhenti tanpa menyimpan perubahan.

Anda dapat melewati variabel dan menggunakan pola dan angka secara langsung. Saya menggunakannya hanya untuk kejelasan tujuan.


3

Bagaimana dengan (menggunakan GNU grepdan bash):

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

Di sini kita menemukan baris yang harus dibuang grep -B2 -A1 'fff' file.txt, kemudian menggunakan ini sebagai file input untuk menemukan baris yang diinginkan membuang ini.


Hmm, ini tidak menampilkan apa pun di komputer saya (OS X)
Amelio Vazquez-Reina

@ AmelioVazquez-Reina maaf tentang hal itu..saya tidak tahu OS Anda sebelumnya..tetapi saya telah menguji ini di Ubuntu ..
heemayl

2
Ini akan memiliki masalah yang sama dengan solusi kos(sekarang dihapus) seolah-olah ada garis duplikat dalam file input dan beberapa dari mereka berada di luar jangkauan dan yang lain di dalam kisaran itu akan menghapus semuanya. Juga, dengan beberapa kemunculan pola , jika ada baris seperti --pada file input (di luar rentang) ini akan menghapusnya karena pembatas --muncul dalam grepoutput ketika lebih dari satu baris adalah pola yang cocok (yang terakhir sangat tidak mungkin tetapi bernilai menyebutkan saya kira).
don_crissti

@don_crissti Terima kasih..kau benar..tapi aku mengambil contoh OP secara harfiah..saya akan meninggalkannya kalau-kalau ada yang merasa
terbantu

1

Anda dapat mencapai hasil yang cukup baik dengan menggunakan file sementara:

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

Hasilnya cukup baik karena Anda dapat kehilangan beberapa lekukan dalam proses, tetapi jika itu adalah file sensitif xml atau indentasi itu seharusnya tidak menjadi masalah. Karena skrip ini menggunakan ram drive, menulis dan membaca file temp tersebut secepat bekerja di memori.


1

Selain itu, jika Anda hanya ingin mengecualikan beberapa baris di depan penanda yang diberikan, Anda dapat menggunakan:

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

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

Dengan memipipkan beberapa perintah, Anda bisa mendapatkan sebelum / sesudah 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

1
Brilliant, gunakan awkpada file terbalik untuk menangani baris berikut ketika Anda bermaksud untuk mempengaruhi baris sebelum dan membalikkan hasilnya.
karmakaze

0

Salah satu cara untuk mencapai ini, mungkin cara termudah adalah dengan membuat variabel dan melakukan hal berikut:

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

Dengan cara ini Anda masih memiliki struktur Anda. Dan Anda dapat dengan mudah melihat dari liner apa yang ingin Anda hapus.

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

solusi yang sama dengan heemayl, dan masalah yang sama seperti yang dijelaskan oleh don_crissti: Ini akan memiliki masalah yang sama dengan solusi kos (sekarang dihapus) seolah-olah ada garis duplikat dalam file input dan beberapa dari mereka berada di luar jangkauan dan yang lain berada di dalam kisaran itu ini akan menghapus semuanya. Juga, dengan beberapa kemunculan pola, jika ada garis seperti - dalam file input (di luar rentang) ini akan menghapusnya karena pembatas - muncul dalam keluaran grep ketika lebih dari satu baris mencocokkan pola (yang terakhir sangat tidak mungkin tetapi layak disebutkan kurasa).
Bodo Thiesen

0

Jika hanya ada 1 kecocokan:

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

Jika tidak (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)
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.