การแข่งขันแบบไม่โลภกับ SED regex (เลียนแบบ perl's. *?)


22

ฉันต้องการใช้sedที่จะเปลี่ยนอะไรในสตริงระหว่างครั้งแรกABและครั้งแรกเกิดAC(รวม) XXXด้วย

สำหรับตัวอย่างเช่นผมมีสายนี้ (สตริงนี้เหมาะสำหรับการทดสอบเท่านั้น):

ssABteAstACABnnACss

ssXXXABnnACssและฉันต้องการผลผลิตที่คล้ายกันนี้:


ฉันทำสิ่งนี้กับperl:

$ echo 'ssABteAstACABnnACss' | perl -pe 's/AB.*?AC/XXX/'
ssXXXABnnACss

sedแต่ฉันต้องการที่จะใช้มันกับ ต่อไปนี้ (ใช้ regex ที่เข้ากันได้กับ Perl) ไม่ทำงาน:

$ echo 'ssABteAstACABnnACss' | sed -re 's/AB.*?AC/XXX/'
ssXXXss

2
มันไม่สมเหตุสมผล คุณมีวิธีแก้ปัญหาการทำงานใน Perl แต่คุณต้องการใช้ Sed ทำไม?
Kusalananda

คำตอบ:


16

Sed regexes จับคู่การแข่งขันที่ยาวที่สุด Sed ไม่เทียบเท่ากับความโลภ

เห็นได้ชัดว่าสิ่งที่เราต้องการจะทำคือการแข่งขัน

  1. AB,
    ตามมาด้วย
  2. จำนวนเงินอื่นใดนอกเหนือจากACนั้น
    ตามด้วย
  3. AC

น่าเสียดายที่sedอย่าทำ # 2 - อย่างน้อยก็ไม่ใช่สำหรับนิพจน์ทั่วไปที่มีหลายตัวอักษร แน่นอนว่าสำหรับตัวเดียวแสดงออกปกติเช่น@(หรือแม้กระทั่ง[123]) เราสามารถทำหรือ[^@]* [^123]*และเพื่อให้เราสามารถแก้ไขข้อ จำกัด ของ sed ได้โดยการเปลี่ยนทุกสิ่งACเป็นเป็น@แล้วค้นหา

  1. AB,
    ตามมาด้วย
  2. จำนวนสิ่งอื่นที่ไม่ใช่ใด ๆ@,
    ตามมาด้วย
  3. @

แบบนี้:

sed 's/AC/@/g; s/AB[^@]*@/XXX/; s/@/AC/g'

ส่วนสุดท้ายการเปลี่ยนแปลงกรณีที่เปรียบของกลับไป@AC

แต่แน่นอนว่านี่เป็นวิธีที่ประมาทเนื่องจากอินพุตอาจมี@อักขระอยู่แล้วดังนั้นโดยการจับคู่พวกเขาเราจะได้รับผลบวกปลอม อย่างไรก็ตามเนื่องจากไม่มีตัวแปรเชลล์จะมีอักขระ NUL ( \x00) อยู่ในนั้นจึงน่าจะเป็นอักขระที่ดีที่จะใช้ในการทำงานด้านบนแทน@:

$ echo 'ssABteAstACABnnACss' | sed 's/AC/\x00/g; s/AB[^\x00]*\x00/XXX/; s/\x00/AC/g'
ssXXXABnnACss

การใช้ NUL ต้องใช้ GNU sed (เพื่อให้แน่ใจว่าฟีเจอร์ของ GNU นั้นเปิดใช้งานผู้ใช้จะต้องไม่ตั้งค่าตัวแปรเชลล์ POSIXLY_CORRECT)

หากคุณใช้ sed กับ-zธงของ GNU เพื่อจัดการอินพุตที่คั่นด้วย NUL เช่นผลลัพธ์ของfind ... -print0NUL แล้ว NUL จะไม่อยู่ในพื้นที่รูปแบบและ NUL เป็นตัวเลือกที่ดีสำหรับการทดแทนที่นี่

แม้ว่า NUL ไม่สามารถอยู่ในตัวแปร bash ได้ แต่ก็เป็นไปได้ที่จะรวมไว้ในprintfคำสั่ง หากสตริงอินพุตของคุณสามารถมีอักขระใด ๆ ได้รวมถึง NUL ให้ดูคำตอบของStéphane Chazelasซึ่งเพิ่มวิธีการหลบหนีที่ฉลาด


ฉันเพิ่งแก้ไขคำตอบของคุณเพื่อเพิ่มคำอธิบายที่ยาว รู้สึกอิสระที่จะตัดมันหรือย้อนกลับ
G-Man กล่าวว่า 'Reinstate Monica'

@ G-Man นั่นเป็นคำอธิบายที่ยอดเยี่ยม! ทำได้ดีมาก ขอขอบคุณ.
John1024

คุณสามารถechoหรือprintf`` 000 'ได้ใน bash (หรืออินพุตอาจมาจากไฟล์) แต่โดยทั่วไปแล้วข้อความจำนวนหนึ่งไม่น่าจะมี NULs
ilkkachu

@ilkkachu คุณพูดถูก สิ่งที่ฉันควรเขียนคือไม่มีตัวแปรหรือพารามิเตอร์ของเชลล์ที่สามารถมี NUL อัปเดตคำตอบแล้ว
John1024

สิ่งนี้จะปลอดภัยกว่านี้ไหมถ้าคุณเปลี่ยนACไปใช้AC@และกลับมาอีกครั้ง
Michael Vehrs

7

sedการใช้งานบางอย่างได้รับการสนับสนุน ssedมีโหมด PCRE:

ssed -R 's/AB.*?AC/XXX/g'

AT&T ast sedมีการเชื่อมโยงและการปฏิเสธเมื่อใช้regexps ที่เพิ่มขึ้น :

sed -A 's/AB(.*&(.*AC.*)!)AC/XXX/g'

คุณสามารถใช้เทคนิคนี้ได้: แทนที่สตริงสุดท้าย (ที่นี่AC) ด้วยอักขระเดี่ยวที่ไม่เกิดขึ้นในสตริงเริ่มต้นหรือสตริงสุดท้าย (เช่น:ที่นี่) เพื่อให้คุณสามารถทำได้s/AB[^:]*://และในกรณีที่อักขระอาจปรากฏในอินพุต ใช้กลไกการหลบหนีที่ไม่ขัดแย้งกับสตริงเริ่มต้นและสิ้นสุด

ตัวอย่าง:

sed 's/_/_u/g; # use _ as the escape character, escape it
     s/:/_c/g; # escape our replacement character
     s/AC/:/g; # replace the end string
     s/AB[^:]*:/XXX/g; # actual replacement
     s/:/AC/g; # restore the remaining end strings
     s/_c/:/g; # revert escaping
     s/_u/_/g'

ด้วย GNU sedวิธีการคือการใช้บรรทัดใหม่เป็นอักขระทดแทน เนื่องจากsedประมวลผลทีละหนึ่งบรรทัดบรรทัดใหม่จะไม่เกิดขึ้นในพื้นที่รูปแบบดังนั้นจึงสามารถทำสิ่งต่อไปนี้

sed 's/AC/\n/g;s/AB[^\n]*\n/XXX/g;s/\n/AC/g'

ซึ่งโดยทั่วไปจะไม่ทำงานกับคนอื่น ๆการใช้งานเพราะพวกเขาไม่ได้รับการสนับสนุนsed [^\n]ด้วย GNU sedคุณต้องแน่ใจว่าไม่รองรับ POSIX (เช่นเดียวกับตัวแปรสภาพแวดล้อม POSIXLY_CORRECT)


6

ไม่ sed regexes ไม่มีการจับคู่ที่ไม่โลภ

คุณสามารถตรงกับข้อความทั้งหมดขึ้นอยู่กับการเกิดขึ้นครั้งแรกACโดยใช้“สิ่งที่ไม่มีAC” ตามด้วยACซึ่งไม่เหมือนกันเป็นของ .*?ACPerl สิ่งนี้คือ“ สิ่งใดก็ตามที่ไม่มีอยู่AC” ไม่สามารถแสดงออกได้อย่างง่ายดายเหมือนนิพจน์ทั่วไป: มีนิพจน์ทั่วไปที่รับรู้ถึงการปฏิเสธของนิพจน์ทั่วไปเสมอ แต่เรกเกชันการปฏิเสธจะซับซ้อนอย่างรวดเร็ว และใน sed แบบพกพานี่เป็นไปไม่ได้เลยเพราะ negation regex ต้องการการจัดกลุ่มการสลับซึ่งมีอยู่ในนิพจน์ปกติที่ขยาย (เช่นใน awk) แต่ไม่ใช่ในนิพจน์พื้นฐานแบบพกพาขั้นพื้นฐาน sed บางเวอร์ชันเช่น GNU sed มีส่วนขยายไปยัง BRE ซึ่งทำให้สามารถแสดงนิพจน์ทั่วไปที่เป็นไปได้ทั้งหมด

sed 's/AB\([^A]*\|A[^C]\)*A*AC/XXX/'

เนื่องจากความยากลำบากในการปฏิเสธ regex สิ่งนี้จึงไม่ได้พูดคุยกัน คุณสามารถทำอะไรแทนการแปลงสายชั่วคราว ในการปรับใช้ที่ไม่ดีคุณสามารถใช้การขึ้นบรรทัดใหม่เป็นตัวทำเครื่องหมายเนื่องจากไม่สามารถปรากฏในบรรทัดอินพุต (และหากคุณต้องการเครื่องหมายหลายอันให้ใช้การขึ้นบรรทัดใหม่ตามด้วยอักขระที่แตกต่างกัน)

sed -e 's/AC/\
&/g' -e 's/AB[^\
]*\nAC/XXX/' -e 's/\n//g'

อย่างไรก็ตามระวังว่าแบ็กสแลช - นิวไลน์ไม่ทำงานในชุดอักขระที่มีรุ่นที่น่าดึงดูดใจ โดยเฉพาะอย่างยิ่งสิ่งนี้ไม่ทำงานใน GNU sed ซึ่งเป็นการใช้งาน sed บน Linux ที่ไม่ได้ฝังตัว ใน GNU sed คุณสามารถใช้\nแทน:

sed -e 's/AC/\
&/g' -e 's/AB[^\n]*\nAC/XXX/' -e 's/\n//g'

ในกรณีเฉพาะนี้ก็เพียงพอที่จะแทนที่ACบรรทัดแรกด้วยบรรทัดใหม่ วิธีที่ฉันนำเสนอข้างต้นเป็นเรื่องทั่วไปมากขึ้น

วิธีที่มีประสิทธิภาพมากขึ้นใน sed คือการบันทึกบรรทัดลงในพื้นที่พักไว้ลบทั้งหมดยกเว้นส่วนแรก "ที่น่าสนใจ" ของบรรทัดแลกเปลี่ยนพื้นที่พักและพื้นที่รูปแบบหรือผนวกพื้นที่รูปแบบไปยังพื้นที่พักและทำซ้ำ อย่างไรก็ตามหากคุณเริ่มทำสิ่งที่ซับซ้อนเช่นนี้คุณควรคิดถึงการเปลี่ยนไปใช้ awk จริงๆ Awk ไม่มีการจับคู่ที่ไม่โลภ แต่คุณสามารถแยกสตริงและบันทึกชิ้นส่วนเป็นตัวแปรได้


@ilkkachu ไม่มันไม่ได้ s/\n//gลบบรรทัดใหม่ทั้งหมด
Gilles 'หยุดความชั่วร้าย'

asdf ใช่ฉันไม่ดี
ilkkachu

3

การจับคู่ที่ไม่โลภโดย Christoph Sieghart

เคล็ดลับในการจับคู่ที่ไม่โลภในการจับคู่คือการจับคู่ตัวละครทั้งหมดยกเว้นตัวที่จบการแข่งขัน ฉันรู้ว่าไม่มีเกมง่ายๆ แต่ฉันได้สูญเสียเวลาอันมีค่าไปกับมันและสคริปต์เชลล์ควรเป็นไปอย่างรวดเร็วและง่ายดาย ดังนั้นในกรณีที่คนอื่นอาจต้องการ:

การจับคู่โลภ

% echo "<b>foo</b>bar" | sed 's/<.*>//g'
bar

การจับคู่ที่ไม่โลภ

% echo "<b>foo</b>bar" | sed 's/<[^>]*>//g'
foobar


3
คำว่า "ไม่มีเกมง่ายๆ" นั้นคลุมเครือ ในกรณีนี้มันไม่ชัดเจนว่าคุณ (หรือ Christoph Sieghart) คิดเรื่องนี้ผ่าน โดยเฉพาะอย่างยิ่งจะได้รับดีถ้าคุณได้แสดงให้เห็นถึงวิธีการแก้ปัญหาที่เฉพาะเจาะจงในคำถาม (ที่แสดงออกศูนย์ของมากขึ้น -of-มีผู้ติดตามโดยมากกว่าหนึ่งตัว ) คุณอาจพบว่าคำตอบนี้ใช้งานไม่ได้ในกรณีนั้น
สกอตต์

หลุมกระต่ายนั้นลึกกว่าที่ฉันคิดไว้ในตอนแรก คุณพูดถูกวิธีแก้ปัญหานั้นใช้งานไม่ได้กับนิพจน์ทั่วไปที่มีหลายตัวอักษร
gresolio

0

ในกรณีของคุณคุณสามารถคัดค้านการปิดอักขระด้วยวิธีนี้:

echo 'ssABteAstACABnnACss' | sed 's/AB[^C]*AC/XXX/'

2
คำถามกล่าวว่า“ ฉันต้องการแทนที่สิ่งใดระหว่างเหตุการณ์แรกABและเหตุการณ์แรกของACด้วยXXX…,” และให้ssABteAstACABnnACssเป็นตัวอย่างอินพุต คำตอบนี้ใช้ได้กับตัวอย่างนั้น แต่ไม่ตอบคำถามโดยทั่วไป ตัวอย่างเช่นssABteCstACABnnACssควรให้ผลลัพธ์aaXXXABnnACssแต่คำสั่งของคุณส่งผ่านบรรทัดนี้โดยไม่เปลี่ยนแปลง
G-Man พูดว่า 'Reinstate Monica'

0

การแก้ปัญหาค่อนข้างง่าย .*เป็นโลภ แต่มันไม่ได้โลภอย่างแน่นอน พิจารณาการจับคู่ssABteAstACABnnACssกับ AB.*ACregexp ACว่าต่อไปนี้.*ต้องเป็นจริงมีการแข่งขัน ปัญหาคือเนื่องจาก.*โลภต่อมาACจะจับคู่สุดท้าย ACมากกว่าอันแรก .*กินค่าแรกACในขณะที่ตัวอักษรACใน regexp ตรงกับอันสุดท้ายใน ssABteAstACABnn AC ss เพื่อป้องกันไม่ให้สิ่งนี้เกิดขึ้นเพียงแค่แทนที่สิ่งแรกACด้วยสิ่งที่ไร้สาระเพื่อแยกความแตกต่างจากสิ่งที่สองและจากสิ่งอื่น

echo ssABteAstACABnnACss | sed 's/AC/-foobar-/; s/AB.*-foobar-/XXX/'
ssXXXABnnACss

ความโลภ.*จะหยุดที่เท้าของ-foobar-ในssABteAst-foobar-ABnnACssเพราะไม่มีอื่น ๆ-foobar-กว่านี้-foobar-และ regexp -foobar- ต้องมีการแข่งขัน ปัญหาก่อนหน้าคือ regexp ACมีสองแมตช์ แต่เนื่องจาก.*โลภมากACจึงเลือกแมตช์สุดท้าย อย่างไรก็ตามมี-foobar-เพียงหนึ่งการแข่งขันที่เป็นไปได้และการแข่งขันนี้พิสูจน์ว่า.*ไม่โลภอย่างแน่นอน ป้ายรถประจำทางสำหรับการ.*เกิดขึ้นที่มีเพียงหนึ่งในการแข่งขันที่เหลืออยู่สำหรับส่วนที่เหลือของ regexp .*ต่อไปนี้

โปรดทราบว่าการแก้ปัญหานี้จะล้มเหลวหากACปรากฏขึ้นก่อนที่แรกABเพราะไม่ถูกต้องจะถูกแทนที่ด้วยAC -foobar-ตัวอย่างเช่นหลังจากการsedเปลี่ยนตัวครั้งแรกACssABteAstACABnnACssจะกลายเป็น-foobar-ssABteAstACABnnACss; ดังนั้นจึงไม่สามารถพบการแข่งขันAB.*-foobar-ได้ อย่างไรก็ตามหากลำดับอยู่เสมอ ... AB ... AC ... AB ... AC ... ดังนั้นโซลูชันนี้จะประสบความสำเร็จ


0

อีกทางเลือกหนึ่งคือการเปลี่ยนสตริงดังนั้นคุณต้องการจับคู่โลภ

echo "ssABtCeCAstACABnnACss" | rev | sed -E "s/(.*)CA.*BA(.*)/\1CA+-+-+-+-BA\2/" | rev

ใช้revเพื่อย้อนกลับสตริงย้อนกลับเกณฑ์การจับคู่ของคุณใช้sedในแบบปกติแล้วกลับผลลัพธ์ ...

ssAB-+-+-+-+ACABnnACss
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.