Bắt các nhóm từ một GreEx RegEx


380

Tôi đã có tập lệnh nhỏ này trong sh(Mac OSX 10.6) để xem qua một loạt các tệp. Google đã ngừng hữu ích vào thời điểm này:

files="*.jpg"
for f in $files
    do
        echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
        name=$?
        echo $name
    done

Cho đến nay (rõ ràng, đối với bạn, các chuyên gia vỏ) $namechỉ giữ 0, 1 hoặc 2, tùy thuộc vào việc greptìm thấy tên tệp có khớp với vấn đề được cung cấp hay không. Điều tôi muốn là nắm bắt những gì bên trong parens ([a-z]+)và lưu trữ nó vào một biến .

Tôi chỉ muốn sử dụng grep, nếu có thể . Nếu không, xin vui lòng không có Python hoặc Perl, v.v. sedhoặc một cái gì đó tương tự - Tôi mới sử dụng shell và muốn tấn công nó từ góc độ thuần túy * nix.

Ngoài ra, là một bonu siêu ngầu , tôi tò mò về cách tôi có thể nối chuỗi trong vỏ? Có phải nhóm tôi đã chụp là chuỗi "somename" được lưu trữ trong tên $ và tôi muốn thêm chuỗi ".jpg" vào cuối chuỗi, tôi có thể cat $name '.jpg'không?

Vui lòng giải thích những gì đang xảy ra, nếu bạn có thời gian.


30
Là grep thực sự tinh khiết unix hơn sed?
martin clayton

3
Ah, không có nghĩa là đề nghị đó. Tôi chỉ hy vọng rằng một giải pháp có thể được tìm thấy bằng cách sử dụng một công cụ mà tôi đặc biệt đang cố gắng học ở đây. Nếu không thể giải quyết bằng cách sử dụng grep, thì sedsẽ rất tuyệt, nếu có thể giải quyết bằng cách sử dụng sed.
Isaac

2
Tôi nên đặt một :) trên btw đó ...
martin clayton

Psh, hôm nay não tôi quá rán haha.
Isaac

2
@martinclayton Đó là một cuộc tranh luận thú vị. Tôi thực sự nghĩ rằng sed, (hoặc chính xác là ed) sẽ cũ hơn (và do đó tinh khiết hơn? Có thể?) Unix vì grep lấy tên của nó từ biểu thức ed g (lobal) / re (biểu thức gular) / p (rint).
đánh bại

Câu trả lời:


500

Nếu bạn đang sử dụng Bash, bạn thậm chí không phải sử dụng grep:

files="*.jpg"
regex="[0-9]+_([a-z]+)_[0-9a-z]*"
for f in $files    # unquoted in order to allow the glob to expand
do
    if [[ $f =~ $regex ]]
    then
        name="${BASH_REMATCH[1]}"
        echo "${name}.jpg"    # concatenate strings
        name="${name}.jpg"    # same thing stored in a variable
    else
        echo "$f doesn't match" >&2 # this could get noisy if there are a lot of non-matching files
    fi
done

Tốt hơn là đặt regex trong một biến. Một số mẫu sẽ không hoạt động nếu được bao gồm theo nghĩa đen.

Điều này sử dụng =~đó là toán tử khớp regex của Bash. Kết quả của trận đấu được lưu vào một mảng được gọi là $BASH_REMATCH. Nhóm chụp đầu tiên được lưu trữ trong chỉ mục 1, nhóm thứ hai (nếu có) trong chỉ mục 2, v.v. Chỉ số 0 là khớp hoàn toàn.

Bạn nên lưu ý rằng không có neo, regex này (và sử dụng grep) sẽ khớp với bất kỳ ví dụ nào sau đây và hơn thế nữa, có thể không phải là thứ bạn đang tìm kiếm:

123_abc_d4e5
xyz123_abc_d4e5
123_abc_d4e5.xyz
xyz123_abc_d4e5.xyz

Để loại bỏ các ví dụ thứ hai và thứ tư, hãy tạo regex của bạn như thế này:

^[0-9]+_([a-z]+)_[0-9a-z]*

trong đó nói rằng chuỗi phải bắt đầu bằng một hoặc nhiều chữ số. Các carat đại diện cho sự bắt đầu của chuỗi. Nếu bạn thêm ký hiệu đô la vào cuối regex, như thế này:

^[0-9]+_([a-z]+)_[0-9a-z]*$

sau đó, ví dụ thứ ba cũng sẽ bị loại vì dấu chấm không nằm trong số các ký tự trong biểu thức chính quy và ký hiệu đô la biểu thị phần cuối của chuỗi. Lưu ý rằng ví dụ thứ tư cũng thất bại trong trận đấu này.

Nếu bạn có GNU grep(khoảng 2,5 trở lên, tôi nghĩ, khi \Ktoán tử được thêm vào):

name=$(echo "$f" | grep -Po '(?i)[0-9]+_\K[a-z]+(?=_[0-9a-z]*)').jpg

Các \Knhà điều hành (chiều dài thay đổi nhìn đằng sau) gây ra các mô hình trước để phù hợp, nhưng không bao gồm các trận đấu trong kết quả. Tương đương độ dài cố định là (?<=)- mẫu sẽ được đưa vào trước dấu ngoặc đơn đóng. Bạn phải dùng\K nếu quantifiers có thể phù hợp với chuỗi có độ dài khác nhau (ví dụ +, *, {2,4}).

Các (?=)trận đấu khai thác cố định hoặc các mẫu chiều dài thay đổi và được gọi là "nhìn về phía trước". Nó cũng không bao gồm chuỗi phù hợp trong kết quả.

Để làm cho khớp không phân biệt chữ hoa chữ thường, (?i)toán tử được sử dụng. Nó ảnh hưởng đến các mô hình theo nó để vị trí của nó là đáng kể.

Regex có thể cần được điều chỉnh tùy thuộc vào việc có các ký tự khác trong tên tệp hay không. Bạn sẽ lưu ý rằng trong trường hợp này, tôi đưa ra một ví dụ về nối chuỗi cùng lúc với chuỗi con được bắt.


48
Trong câu trả lời này, tôi muốn nâng cấp dòng cụ thể có nội dung "Tốt hơn là đặt regex vào một biến. Một số mẫu sẽ không hoạt động nếu được bao gồm theo nghĩa đen."
Brandin

5
@FrancescoFrassinelli: Một ví dụ là một mẫu bao gồm khoảng trắng. Thật khó để thoát ra và bạn không thể sử dụng dấu ngoặc kép vì điều đó buộc nó từ một biểu thức chính quy thành một chuỗi thông thường. Cách chính xác để làm điều đó là sử dụng một biến. Báo giá có thể được sử dụng trong quá trình chuyển nhượng làm cho mọi thứ đơn giản hơn nhiều.
Tạm dừng cho đến khi có thông báo mới.

5
/KNgười vận hành đá.
razz

2
@Brandon: Nó hoạt động. Phiên bản Bash nào bạn đang sử dụng? Chỉ cho tôi những gì bạn đang làm mà không hiệu quả và có lẽ tôi có thể cho bạn biết lý do tại sao.
Tạm dừng cho đến khi có thông báo mới.

2
@mdelolmo: Câu trả lời của tôi bao gồm thông tin về grep. Nó cũng được OP chấp nhận và nâng cấp khá nhiều. Cảm ơn các downvote.
Tạm dừng cho đến khi có thông báo mới.

145

Điều này thực sự không thể với thuần túy grep, ít nhất là không nói chung.

Nhưng nếu mẫu của bạn phù hợp, bạn có thể sử dụng grepnhiều lần trong một đường ống để trước tiên giảm dòng của bạn xuống một định dạng đã biết, và sau đó trích xuất chỉ bit bạn muốn. (Mặc dù các công cụ thích cutsedtốt hơn nhiều về điều này).

Giả sử vì lý do tranh luận rằng mẫu của bạn đơn giản hơn một chút: [0-9]+_([a-z]+)_Bạn có thể trích xuất như thế này:

echo $name | grep -Ei '[0-9]+_[a-z]+_' | grep -oEi '[a-z]+'

Dòng đầu tiên grepsẽ xóa bất kỳ dòng nào không khớp với tổng thể của bạn, dòng thứ hai grep(đã --only-matchingđược chỉ định) sẽ hiển thị phần alpha của tên. Điều này chỉ hoạt động vì mô hình phù hợp: "phần alpha" đủ cụ thể để lấy ra những gì bạn muốn.

(Ngoài ra: Cá nhân tôi sẽ sử dụng grep+ cutđể đạt được những gì bạn đang có sau : echo $name | grep {pattern} | cut -d _ -f 2. Điều này được cutphân tích dòng thành các trường bằng cách tách trên dấu phân cách_ và trả về chỉ trường 2 (số trường bắt đầu từ 1)).

Triết lý của Unix là có các công cụ làm một việc, và làm tốt và kết hợp chúng để đạt được các nhiệm vụ không tầm thường, vì vậy tôi cho rằng grep+ sedetc là cách làm Unixy hơn :-)


3
for f in $files; do name=tiếng vang $ f | grep -oEi '[0-9] + _ ([az] +) _ [0-9a-z] *' | cắt -d _ -f 2 ;Aha!
Isaac

2
tôi không đồng ý với "triết lý" đó. nếu bạn có thể sử dụng shell trong các khả năng được xây dựng mà không cần gọi các lệnh bên ngoài, thì tập lệnh của bạn sẽ có hiệu suất nhanh hơn rất nhiều. có một số công cụ chồng chéo trong chức năng. ví dụ grep và sed và awk. tất cả đều thực hiện các thao tác chuỗi, nhưng awk nổi bật hơn tất cả vì nó có thể làm được nhiều hơn thế. Trên thực tế, tất cả những chuỗi các lệnh, như greps kép hoặc grep + sed ở trên có thể được rút ngắn bằng cách thực hiện chúng với một quy trình awk.
ghostdog74

7
@ ghostdog74: Không có gì phải bàn cãi ở đây rằng việc kết nối nhiều hoạt động nhỏ cùng nhau thường kém hiệu quả hơn so với thực hiện tất cả ở một nơi, nhưng tôi khẳng định rằng triết lý Unix là rất nhiều công cụ làm việc cùng nhau. Chẳng hạn, tar chỉ lưu trữ các tệp, nó không nén chúng, và vì nó xuất ra STDOUT theo mặc định, bạn có thể dẫn nó qua mạng bằng netcat hoặc nén nó bằng bzip2, v.v. Theo tôi, tôi củng cố quy ước và chung ethos rằng các công cụ Unix sẽ có thể làm việc cùng nhau trong các đường ống.
RobM

cắt là tuyệt vời - cảm ơn vì tiền boa! Đối với các công cụ so với đối số hiệu quả, tôi thích sự đơn giản của các công cụ xích.
ether_joe

đạo cụ cho tùy chọn o của grep, rất hữu ích
chiliNUT

96

Tôi nhận ra rằng một câu trả lời đã được chấp nhận cho điều này, nhưng từ góc độ "thuần túy * nix purist", có vẻ như công cụ phù hợp cho công việc là pcregrep, điều này dường như chưa được đề cập. Hãy thử thay đổi các dòng:

    echo $f | grep -oEi '[0-9]+_([a-z]+)_[0-9a-z]*'
    name=$?

theo sau:

    name=$(echo $f | pcregrep -o1 -Ei '[0-9]+_([a-z]+)_[0-9a-z]*')

để chỉ lấy nội dung của nhóm bắt giữ 1.

Công pcregrepcụ sử dụng tất cả các cú pháp giống như bạn đã sử dụng grep, nhưng thực hiện các chức năng mà bạn cần.

Tham số này -ohoạt động giống như grepphiên bản nếu ở chế độ trống, nhưng nó cũng chấp nhận tham số số trong pcregrepđó cho biết nhóm chụp nào bạn muốn hiển thị.

Với giải pháp này, có một sự thay đổi tối thiểu cần thiết trong kịch bản. Bạn chỉ cần thay thế một tiện ích mô-đun bằng một tiện ích khác và điều chỉnh các tham số.

Lưu ý thú vị: Bạn có thể sử dụng nhiều đối số -o để trả về nhiều nhóm chụp theo thứ tự xuất hiện trên dòng.


3
pcregrepkhông có sẵn theo mặc định Mac OS X, đó là những gì OP sử dụng
grebneke

4
Tôi pcregrepdường như không hiểu chữ số sau -o: "Thư tùy chọn không xác định '1' trong" -o1 ". Cũng không đề cập đến chức năng đó khi nhìn vàopcregrep --help
Peter Herdenborg

1
@WAF xin lỗi, đoán tôi nên đưa thông tin đó vào bình luận của tôi. Tôi đang dùng Centos 6.5 và phiên bản pcregrep rất cũ : 7.8 2008-09-05.
Peter Herdenborg

2
vâng, rất giúp đỡ, vdecho 'r123456 foo 2016-03-17' | pcregrep -o1 'r([0-9]+)' 123456
zhuguowei 17/03/2016

5
pcregrep8.41 (được cài đặt với apt-get install pcregrepbật Ubuntu 16.03) không nhận ra công -Eitắc. Nó hoạt động hoàn hảo mà không có nó, mặc dù. Trên macOS, với pcregrepcài đặt qua homebrew(cũng là 8.41) như @ Biếnpatel đề cập ở trên, ít nhất là trên High Sierra, -Ecông tắc cũng không được công nhận.
Ville

27

Không thể chỉ trong grep tôi tin

cho sed:

name=`echo $f | sed -E 's/([0-9]+_([a-z]+)_[0-9a-z]*)|.*/\2/'`

Tôi sẽ nhận một cú đâm vào phần thưởng mặc dù:

echo "$name.jpg"

2
Thật không may, sedgiải pháp đó không hoạt động. Nó chỉ đơn giản là in ra tất cả mọi thứ trong thư mục của tôi.
Isaac

đã cập nhật, sẽ xuất ra một dòng trống nếu không có kết quả khớp, vì vậy hãy chắc chắn kiểm tra xem
cobbal

Bây giờ nó chỉ xuất ra các dòng trống!
Isaac

sed này có vấn đề Nhóm đầu tiên của dấu ngoặc đơn bao gồm tất cả mọi thứ. Tất nhiên \ 2 sẽ không có gì.
ghostdog74

nó hoạt động cho một số trường hợp thử nghiệm đơn giản ... \ 2 có được nhóm bên trong
cobbal

16

Đây là một giải pháp sử dụng gawk. Đó là thứ tôi thấy tôi cần sử dụng thường xuyên vì vậy tôi đã tạo một chức năng cho nó

function regex1 { gawk 'match($0,/'$1'/, ary) {print ary['${2:-'1'}']}'; }

sử dụng chỉ cần làm

$ echo 'hello world' | regex1 'hello\s(.*)'
world

Ý tưởng tuyệt vời, nhưng dường như không hoạt động với các không gian trong regrec - chúng cần được thay thế bằng \s. Bạn biết cách để sửa nó không?
Adam Ryczkowski

4

Một gợi ý cho bạn - bạn có thể sử dụng mở rộng tham số để xóa phần tên khỏi dấu gạch dưới cuối cùng trở đi và tương tự khi bắt đầu:

f=001_abc_0za.jpg
work=${f%_*}
name=${work#*_}

Rồi namesẽ có giá trị abc.

Xem tài liệu dành cho nhà phát triển của Apple , tìm kiếm về phía trước 'Mở rộng tham số'.


điều này sẽ không kiểm tra ([az] +).
ghostdog74

@levislevis - Điều đó đúng, nhưng, như nhận xét của OP, nó làm những gì cần thiết.
martin clayton

2

nếu bạn có bash, bạn có thể sử dụng Globing mở rộng

shopt -s extglob
shopt -s nullglob
shopt -s nocaseglob
for file in +([0-9])_+([a-z])_+([a-z0-9]).jpg
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

hoặc là

ls +([0-9])_+([a-z])_+([a-z0-9]).jpg | while read file
do
   IFS="_"
   set -- $file
   echo "This is your captured output : $2"
done

Điều đó có vẻ hấp dẫn. Có lẽ bạn có thể nối một lời giải thích cho nó? Hoặc, nếu bạn rất có khuynh hướng, liên kết đến một tài nguyên đặc biệt sâu sắc giải thích nó? Cảm ơn!
Isaac
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.