Bisakah grep output hanya pengelompokan tertentu yang cocok?

293

Katakanlah saya punya file:

# file: 'test.txt'
foobar bash 1
bash
foobar happy
foobar

Saya hanya ingin tahu kata-kata apa yang muncul setelah "foobar", jadi saya bisa menggunakan regex ini:

"foobar \(\w\+\)"

Tanda kurung menunjukkan bahwa saya memiliki minat khusus pada kata setelah foobar. Tetapi ketika saya melakukan grep "foobar \(\w\+\)" test.txt, saya mendapatkan seluruh baris yang cocok dengan seluruh regex, daripada hanya "kata setelah foobar":

foobar bash 1
foobar happy

Saya lebih suka output dari perintah itu terlihat seperti ini:

bash
happy

Apakah ada cara untuk memberi tahu grep agar hanya menampilkan item yang cocok dengan pengelompokan (atau pengelompokan tertentu) dalam ekspresi reguler?

Cory Klein
sumber
4
bagi mereka yang tidak perlu grep:perl -lne 'print $1 if /foobar (\w+)/' < test.txt
lemari besi

Jawaban:

327

GNU grep memiliki -Popsi untuk reg-style perl, dan -oopsi untuk hanya mencetak apa yang cocok dengan polanya. Ini dapat digabungkan dengan menggunakan pernyataan melihat-lihat (dijelaskan dalam Pola Diperluas dalam halaman perlre ) untuk menghapus bagian dari pola grep dari apa yang ditentukan telah cocok untuk keperluan -o.

$ grep -oP 'foobar \K\w+' test.txt
bash
happy
$

Ini \Kadalah bentuk pendek (dan bentuk yang lebih efisien) (?<=pattern)yang Anda gunakan sebagai pernyataan melihat ke belakang lebar nol sebelum teks yang ingin Anda hasilkan. (?=pattern)dapat digunakan sebagai pernyataan melihat ke depan dengan lebar nol setelah teks yang ingin Anda hasilkan.

Misalnya, jika Anda ingin mencocokkan kata antara foodan bar, Anda dapat menggunakan:

$ grep -oP 'foo \K\w+(?= bar)' test.txt

atau (untuk simetri)

$ grep -oP '(?<=foo )\w+(?= bar)' test.txt
camh
sumber
3
Bagaimana Anda melakukannya jika regex Anda memiliki lebih dari satu grup? (seperti judulnya tersirat?)
barracel
4
@ Barracel: Saya tidak percaya Anda bisa. Saatnyased(1)
camh
1
@camh Saya baru saja menguji bahwa grep -oP 'foobar \K\w+' test.txttidak menghasilkan apa-apa dengan OP test.txt. Versi grep adalah 2.5.1. Apa yang salah? O_O
SOUser
@XichenLi: Saya tidak bisa mengatakannya. Saya baru saja membangun v2.5.1 dari grep (sudah cukup tua - sejak 2006) dan itu berhasil untuk saya.
camh
@ SOUser: Saya mengalami hal yang sama - tidak menampilkan apa pun ke file. Saya mengirimkan permintaan edit untuk menyertakan '>' sebelum nama file untuk mengirim output karena ini berhasil bagi saya.
rjchicago
39

Grep standar tidak dapat melakukan ini, tetapi versi terbaru dari GNU grep bisa . Anda dapat beralih ke sed, awk atau perl. Berikut adalah beberapa contoh yang melakukan apa yang Anda inginkan pada input sampel Anda; mereka berperilaku sedikit berbeda dalam kasus sudut.

Ganti foobar word other stuffdengan word, cetak hanya jika penggantian dilakukan.

sed -n -e 's/^foobar \([[:alnum:]]\+\).*/\1/p'

Jika kata pertama adalah foobar, cetak kata kedua.

awk '$1 == "foobar" {print $2}'

Lepas foobarjika itu kata pertama, dan lewati saja; kemudian strip semua setelah spasi putih dan cetak.

perl -lne 's/^foobar\s+// or next; s/\s.*//; print'
Gilles
sumber
Luar biasa! Saya pikir saya mungkin bisa melakukan ini dengan sed, tapi saya belum pernah menggunakannya sebelumnya dan berharap saya bisa menggunakan familiarku grep. Tapi sintaks untuk perintah-perintah ini sebenarnya terlihat sangat akrab sekarang karena saya sudah terbiasa dengan pencarian gaya vim & ganti + regex. Terima kasih banyak.
Cory Klein
1
Tidak benar, Gilles. Lihat jawaban saya untuk solusi grep GNU.
camh
1
@camh: Ah, saya tidak tahu GNU grep sekarang memiliki dukungan PCRE penuh. Saya sudah memperbaiki jawaban saya, terima kasih.
Gilles
1
Jawaban ini sangat berguna untuk Linux tertanam karena Busybox greptidak memiliki dukungan PCRE.
Craig McQueen
Jelas ada beberapa cara untuk menyelesaikan tugas yang sama, namun, jika OP meminta penggunaan grep, mengapa Anda menjawab sesuatu yang lain? Juga, paragraf pertama Anda salah: ya grep bisa melakukannya.
fcm
32
    sed -n "s/^.*foobar\s*\(\S*\).*$/\1/p"

-n     suppress printing
s      substitute
^.*    anything before foobar
foobar initial search match
\s*    any white space character (space)
\(     start capture group
\S*    capture any non-white space character (word)
\)     end capture group
.*$    anything after the capture group
\1     substitute everything with the 1st capture group
p      print it
jgshawkey
sumber
1
+1 untuk contoh sed, sepertinya alat yang lebih baik untuk pekerjaan daripada grep. Satu komentar, ^dan $karena itu .*adalah pertandingan serakah. Namun, termasuk mereka dapat membantu memperjelas maksud regex.
Tony
18

Nah, jika Anda tahu bahwa foobar selalu merupakan kata atau baris pertama, maka Anda dapat menggunakan cut. Seperti itu:

grep "foobar" test.file | cut -d" " -f2
Dave
sumber
The -oswitch on grep secara luas diterapkan (lebih dari ekstensi grep Gnu), demikian grep -o "foobar" test.file | cut -d" " -f2akan meningkatkan efektivitas dari solusi ini, yang lebih portabel daripada menggunakan pernyataan lookbehind.
dubiousjim
Saya percaya bahwa Anda akan membutuhkan grep -o "foobar .*"atau grep -o "foobar \w+".
G-Man
9

Jika PCRE tidak didukung, Anda dapat mencapai hasil yang sama dengan dua pemanggilan grep. Misalnya untuk mengambil kata setelah foobar lakukan ini:

<test.txt grep -o 'foobar  *[^ ]*' | grep -o '[^ ]*$'

Ini dapat diperluas ke kata arbitrer setelah foobar seperti ini (dengan ERE agar mudah dibaca):

i=1
<test.txt egrep -o 'foobar +([^ ]+ +){'$i'}[^ ]+' | grep -o '[^ ]*$'

Keluaran:

1

Perhatikan indeks iberbasis nol.

Thor
sumber
6

pcregrepmemiliki -oopsi yang lebih cerdas yang memungkinkan Anda memilih grup menangkap mana yang Anda inginkan. Jadi, menggunakan file contoh Anda,

$ pcregrep -o1 "foobar (\w+)" test.txt
bash
happy
G-Man
sumber
4

Menggunakan greptidak kompatibel lintas platform, karena -P/ --perl-regexphanya tersedia di GNUgrep , bukan BSDgrep .

Inilah solusinya menggunakan ripgrep:

$ rg -o "foobar (\w+)" -r '$1' <test.txt
bash
happy

Sesuai man rg:

-r/ --replace REPLACEMENT_TEXTGanti setiap kecocokan dengan teks yang diberikan.

Indeks grup pengambilan (misalnya, $5) dan nama (misalnya, $foo) didukung dalam string pengganti.

Terkait: GH-462 .

kenorb
sumber
2

Saya menemukan jawaban @jgshawkey sangat membantu. grepbukan alat yang baik untuk ini, tetapi sed adalah, meskipun di sini kita memiliki contoh yang menggunakan grep untuk mengambil garis yang relevan.

Sintaks regex dari sed adalah istimewa jika Anda tidak terbiasa.

Berikut adalah contoh lain: yang ini mem-parsing output xinput untuk mendapatkan ID integer

⎜   ↳ SynPS/2 Synaptics TouchPad                id=19   [slave  pointer  (2)]

dan saya ingin 19

export TouchPadID=$(xinput | grep 'TouchPad' | sed  -n "s/^.*id=\([[:digit:]]\+\).*$/\1/p")

Perhatikan sintaks kelas:

[[:digit:]]

dan kebutuhan untuk melarikan diri dari yang berikut +

Saya menganggap hanya satu baris yang cocok.

Tim Richardson
sumber
Ini persis apa yang saya coba lakukan. Terima kasih!
James
Versi yang sedikit lebih sederhana tanpa tambahan grep, dengan anggapan 'TouchPad' ada di sebelah kiri 'id':echo "SynPS/2 Synaptics TouchPad id=19 [slave pointer (2)]" | sed -nE "s/.*TouchPad.+id=([0-9]+).*/\1/p"
Amit Naidu