ฉันจะแทนที่สตริงในไฟล์ได้อย่างไร?


752

การแทนที่สตริงในไฟล์ตามเกณฑ์การค้นหาบางอย่างเป็นงานที่พบบ่อยมาก ฉันสามารถ

  • แทนที่สตริงfooด้วยbarในไฟล์ทั้งหมดในไดเรกทอรีปัจจุบันหรือไม่
  • ทำแบบเดียวกันซ้ำสำหรับไดเรกทอรีย่อย?
  • แทนที่เฉพาะถ้าชื่อไฟล์ตรงกับสายอื่น?
  • แทนที่เฉพาะในกรณีที่พบสตริงในบริบท?
  • แทนที่หากสตริงอยู่ในหมายเลขบรรทัดที่แน่นอน?
  • แทนที่สตริงจำนวนมากด้วยการแทนที่เดียวกัน
  • แทนที่หลายสตริงด้วยการแทนที่ที่แตกต่างกัน

2
นี่มีวัตถุประสงค์เพื่อเป็นคำถามและคำตอบที่เป็นที่ยอมรับในเรื่องนี้ (ดูการสนทนาเมตานี้) โปรดแก้ไขคำตอบของฉันด้านล่างหรือเพิ่มของคุณเอง
terdon

คำตอบ:


1010

1. การแทนที่การเกิดขึ้นทั้งหมดของสตริงหนึ่งด้วยอีกสตริงหนึ่งในไฟล์ทั้งหมดในไดเรกทอรีปัจจุบัน:

สิ่งเหล่านี้มีไว้สำหรับกรณีที่คุณ รู้ว่าไดเรกทอรีนั้นมีเพียงไฟล์ปกติและคุณต้องการประมวลผลไฟล์ที่ไม่ได้ซ่อนไว้ทั้งหมด หากไม่เป็นเช่นนั้นให้ใช้วิธีการใน 2

ทั้งหมดsedการแก้ปัญหาในคำตอบนี้ถือว่า sedGNU ถ้าใช้ FreeBSD หรือ OS / X แทนที่ด้วย-i -i ''นอกจากนี้โปรดทราบว่าการใช้-iสวิตช์กับเวอร์ชันใด ๆsedนั้นมีผลต่อความปลอดภัยของระบบไฟล์บางอย่างและไม่สามารถทำได้ในสคริปต์ใด ๆ ที่คุณวางแผนที่จะแจกจ่ายในทางใดทางหนึ่ง

  • ไม่เรียกซ้ำไฟล์ในไดเรกทอรีนี้เท่านั้น:

    sed -i -- 's/foo/bar/g' *
    perl -i -pe 's/foo/bar/g' ./* 
    

    ( perlอันใดอันหนึ่งจะล้มเหลวสำหรับชื่อไฟล์ที่ลงท้ายด้วย|หรือช่องว่าง )

  • เรียกซ้ำไฟล์ปกติ ( รวมถึงไฟล์ที่ซ่อนอยู่ ) ในไดเรกทอรีย่อยนี้และทั้งหมด

    find . -type f -exec sed -i 's/foo/bar/g' {} +

    หากคุณใช้ zsh:

    sed -i -- 's/foo/bar/g' **/*(D.)

    (อาจล้มเหลวหากรายการมีขนาดใหญ่เกินไปดูzargsการแก้ไข)

    Bash ไม่สามารถตรวจสอบไฟล์ปกติได้โดยตรงต้องการลูป (วงเล็บปีกกาหลีกเลี่ยงการตั้งค่าตัวเลือกทั่วโลก):

    ( shopt -s globstar dotglob;
        for file in **; do
            if [[ -f $file ]] && [[ -w $file ]]; then
                sed -i -- 's/foo/bar/g' "$file"
            fi
        done
    )
    

    ไฟล์จะถูกเลือกเมื่อเป็นไฟล์จริง (-f) และสามารถเขียนได้ (-w)

2. แทนที่เฉพาะในกรณีที่ชื่อไฟล์ตรงกับสายอื่น / มีนามสกุลเฉพาะ / เป็นประเภทที่แน่นอน ฯลฯ :

  • ไฟล์ที่ไม่เรียกซ้ำในไดเรกทอรีนี้เท่านั้น:

    sed -i -- 's/foo/bar/g' *baz*    ## all files whose name contains baz
    sed -i -- 's/foo/bar/g' *.baz    ## files ending in .baz
    
  • เรียกซ้ำไฟล์ปกติในสิ่งนี้และทุกไดเรกทอรีย่อย

    find . -type f -name "*baz*" -exec sed -i 's/foo/bar/g' {} +

    หากคุณกำลังใช้ bash (วงเล็บปีกกาหลีกเลี่ยงการตั้งค่าตัวเลือกทั่วโลก):

    ( shopt -s globstar dotglob
        sed -i -- 's/foo/bar/g' **baz*
        sed -i -- 's/foo/bar/g' **.baz
    )
    

    หากคุณใช้ zsh:

    sed -i -- 's/foo/bar/g' **/*baz*(D.)
    sed -i -- 's/foo/bar/g' **/*.baz(D.)
    

    --ทำหน้าที่ที่จะบอกsedว่าไม่มีธงอื่น ๆ จะได้รับในบรรทัดคำสั่ง -นี้จะเป็นประโยชน์ในการป้องกันชื่อไฟล์ที่เริ่มต้นด้วย

  • หากไฟล์เป็นประเภทที่แน่นอนตัวอย่างเช่นไฟล์เรียกใช้งาน (ดูman findตัวเลือกเพิ่มเติม):

    find . -type f -executable -exec sed -i 's/foo/bar/g' {} +

    zsh:

    sed -i -- 's/foo/bar/g' **/*(D*)

3. แทนที่เฉพาะในกรณีที่พบสตริงในบริบทที่กำหนด

  • แทนที่fooด้วยbarหากมีในbazภายหลังในบรรทัดเดียวกัน:

    sed -i 's/foo\(.*baz\)/bar\1/' file

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

  • แทนที่fooด้วยbarเฉพาะเมื่อfooพบในคอลัมน์ 3d (ฟิลด์) ของไฟล์อินพุต (สมมติว่าฟิลด์ที่คั่นด้วยช่องว่าง):

    gawk -i inplace '{gsub(/foo/,"baz",$3); print}' file

    (ต้องการgawk4.1.0 หรือใหม่กว่า)

  • สำหรับฟิลด์อื่นให้ใช้โดย$Nที่Nจำนวนของฟิลด์ที่สนใจ สำหรับตัวคั่นฟิลด์อื่น ( :ในตัวอย่างนี้) ใช้:

    gawk -i inplace -F':' '{gsub(/foo/,"baz",$3);print}' file

    โซลูชันอื่นที่ใช้perl:

    perl -i -ane '$F[2]=~s/foo/baz/g; $" = " "; print "@F\n"' foo 

    หมายเหตุ: ทั้งawkและperlโซลูชันจะมีผลต่อระยะห่างในไฟล์ (ลบช่องว่างนำหน้าและต่อท้ายและแปลงลำดับช่องว่างเป็นอักขระช่องว่างหนึ่งช่องในบรรทัดที่ตรงกัน) สำหรับฟิลด์อื่นให้ใช้โดย$F[N-1]ที่Nเป็นหมายเลขฟิลด์ที่คุณต้องการและสำหรับการใช้ตัวคั่นฟิลด์อื่น (การ$"=":"ตั้งค่าตัวคั่นฟิลด์เอาต์พุตเป็น:):

    perl -i -F':' -ane '$F[2]=~s/foo/baz/g; $"=":";print "@F"' foo 
  • แทนที่fooด้วยbarเฉพาะในบรรทัดที่ 4:

    sed -i '4s/foo/bar/g' file
    gawk -i inplace 'NR==4{gsub(/foo/,"baz")};1' file
    perl -i -pe 's/foo/bar/g if $.==4' file
    

4. การดำเนินการแทนที่หลายรายการ: แทนที่ด้วยสตริงที่แตกต่างกัน

  • คุณสามารถรวมsedคำสั่ง:

    sed -i 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file

    โปรดทราบว่าเรื่องการสั่งซื้อ ( sed 's/foo/bar/g; s/bar/baz/g'จะแทนfooด้วยbaz)

  • หรือคำสั่ง Perl

    perl -i -pe 's/foo/bar/g; s/baz/zab/g; s/Alice/Joan/g' file
  • หากคุณมีรูปแบบจำนวนมากจะเป็นการง่ายกว่าที่จะบันทึกรูปแบบและการแทนที่ในsedไฟล์สคริปต์:

    #! /usr/bin/sed -f
    s/foo/bar/g
    s/baz/zab/g
    
  • หรือหากคุณมีคู่รูปแบบมากเกินไปที่จะเป็นไปได้ข้างต้นคุณสามารถอ่านรูปแบบคู่จากไฟล์ (รูปแบบที่คั่นด้วยช่องว่างสองรูปแบบรูปแบบ $ และ $ รูปแบบแทน $ ต่อบรรทัด):

    while read -r pattern replacement; do   
        sed -i "s/$pattern/$replacement/" file
    done < patterns.txt
    
  • นั่นจะค่อนข้างช้าสำหรับรายการรูปแบบยาวและไฟล์ข้อมูลขนาดใหญ่ดังนั้นคุณอาจต้องการอ่านรูปแบบและสร้างsedสคริปต์จากพวกเขาแทน ต่อไปนี้จะถือว่าตัวคั่น<space>คั่นรายการของคู่MATCH <space> REPLACE ที่เกิดขึ้นหนึ่งต่อบรรทัดในไฟล์patterns.txt:

    sed 's| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|' <patterns.txt |
    sed -f- ./editfile >outfile
    

    รูปแบบข้างต้นเป็นส่วนใหญ่โดยพลการและตัวอย่างเช่นไม่อนุญาตให้มีการ<พื้นที่>ในทั้งMATCHหรือแทนที่ วิธีนี้เป็นวิธีที่ทั่วไปมาก: โดยทั่วไปแล้วหากคุณสามารถสร้างสตรีมเอาต์พุตที่ดูเหมือนsedสคริปต์คุณสามารถระบุสตรีมนั้นเป็นsedสคริปต์โดยระบุsedไฟล์สคริปต์เป็น-stdin

  • คุณสามารถรวมและเชื่อมสคริปต์หลายตัวเข้าด้วยกันในลักษณะเดียวกัน:

    SOME_PIPELINE |
    sed -e'#some expression script'  \
        -f./script_file -f-          \
        -e'#more inline expressions' \
    ./actual_edit_file >./outfile
    

    POSIX sedจะเชื่อมสคริปต์ทั้งหมดเข้าด้วยกันตามลำดับที่ปรากฏบนบรรทัดคำสั่ง ไม่มีความต้องการเหล่านี้สิ้นสุดใน\newline

  • grep สามารถทำงานในลักษณะเดียวกัน:

    sed -e'#generate a pattern list' <in |
    grep -f- ./grepped_file
    
  • เมื่อทำงานกับสตริงคงที่เป็นรูปแบบเป็นแนวปฏิบัติที่ดีที่จะหลีกเลี่ยงเมตาอักขระทั่วไป คุณสามารถทำได้ค่อนข้างง่าย:

    sed 's/[]$&^*\./[]/\\&/g
         s| *\([^ ]*\) *\([^ ]*\).*|s/\1/\2/g|
    ' <patterns.txt |
    sed -f- ./editfile >outfile
    

5. การดำเนินการแทนที่หลายรายการ: แทนที่รูปแบบหลายรายการด้วยสตริงเดียวกัน

  • แทนที่ใด ๆfoo, barหรือbazกับfoobar

    sed -Ei 's/foo|bar|baz/foobar/g' file
  • หรือ

    perl -i -pe 's/foo|bar|baz/foobar/g' file

2
@ StéphaneChazelasขอบคุณสำหรับการแก้ไขมันแน่นอนแก้ไขหลายสิ่ง อย่างไรก็ตามโปรดอย่าลบข้อมูลที่เกี่ยวข้องกับการทุบตี zshทุกคนไม่ได้ใช้ โดยทั้งหมดเพิ่มzshข้อมูล แต่ไม่มีเหตุผลที่จะลบสิ่งทุบตี นอกจากนี้ฉันรู้ว่าการใช้เชลล์สำหรับการประมวลผลข้อความไม่เหมาะ แต่มีบางกรณีที่จำเป็น ฉันแก้ไขสคริปต์ต้นฉบับของฉันในเวอร์ชันที่ดีกว่าซึ่งจะสร้างsedสคริปต์แทนการใช้เชลล์ลูปในการวิเคราะห์ สิ่งนี้มีประโยชน์หากคุณมีรูปแบบหลายร้อยคู่ตัวอย่างเช่น
terdon

2
@terdon คุณใช้ bash ไม่ถูกต้อง ทุบตีก่อน 4.3 จะตาม symlink เมื่อมากไปน้อย ทุบตีก็ไม่เทียบเท่า(.)คุณสมบัติรอบตัวดังนั้นจึงไม่สามารถใช้ที่นี่ได้ (คุณพลาดไปบ้าง - เช่นกัน) for loop ไม่ถูกต้อง (หายไป -r) และหมายถึงการส่งไฟล์หลายครั้งและไม่ได้รับประโยชน์ใด ๆ มากกว่าสคริปต์แบบเซด
Stéphane Chazelas

7
@terdon อะไร--หลังจาก sed -iและก่อนที่คำสั่งทดแทนจะระบุ?
Geek

5
@ Geek นั่นคือสิ่งที่ POSIX -มันหมายถึงการสิ้นสุดของตัวเลือกและช่วยให้คุณผ่านการขัดแย้งที่เริ่มต้นด้วย -fooใช้มันเพื่อให้แน่ใจว่าคำสั่งที่จะทำงานบนไฟล์ที่มีชื่อเหมือน ก็ไม่มีการ-fแยกจะเป็นตัวเลือก
terdon

1
ใช้ความระมัดระวังในการเรียกใช้คำสั่งแบบเรียกซ้ำบางคำสั่งใน git repository ตัวอย่างเช่นโซลูชันที่ให้ไว้ในส่วนที่ 1 ของคำตอบนี้จะแก้ไขไฟล์ git ภายในใน.gitไดเรกทอรีและแก้ไขการเช็คเอาต์ของคุณ ดีกว่าที่จะดำเนินการภายใน / บนไดเรกทอรีที่เฉพาะเจาะจงตามชื่อ
Pistos

75

ดีอาร์อีพี acement เครื่องมือ Linux เป็นRPLที่ถูกเขียนเดิมสำหรับโครงการ Debian จึงสามารถใช้ได้กับapt-get install rplใน Debian distro มาและอาจจะให้คนอื่น ๆ แต่มิฉะนั้นคุณสามารถดาวน์โหลด tar.gzไฟล์ในSourgeForge

ตัวอย่างการใช้งานที่ง่ายที่สุด:

 $ rpl old_string new_string test.txt

โปรดทราบว่าถ้าสตริงมีช่องว่างควรจะอยู่ในเครื่องหมายคำพูด โดยค่าเริ่มต้นrplดูแลตัวอักษรพิมพ์ใหญ่แต่ไม่ได้เป็นคำที่สมบูรณ์แต่คุณสามารถเปลี่ยนค่าเริ่มต้นเหล่านี้ด้วยตัวเลือก-i(ไม่ต้องใช้ตัวพิมพ์ใหญ่) และ-w(ทั้งคำ) คุณยังสามารถระบุหลายไฟล์ :

 $ rpl -i -w "old string" "new string" test.txt test2.txt

หรือแม้แต่ระบุนามสกุล ( -x) เพื่อค้นหาหรือค้นหาซ้ำ ( -R) ในไดเรกทอรี:

 $ rpl -x .html -x .txt -R old_string new_string test*

นอกจากนี้คุณยังสามารถค้นหา / แทนที่ในโหมดโต้ตอบด้วย-pตัวเลือก (พร้อมท์):

ผลลัพธ์แสดงจำนวนไฟล์ / สตริงที่ถูกแทนที่และประเภทของการค้นหา (กรณีเป็น / ละเอียดอ่อนทั้งคำ / บางส่วน) แต่สามารถเงียบได้ด้วยตัวเลือก-q( โหมดเงียบ ) หรือ verbose มากขึ้นแสดงรายการหมายเลขบรรทัดที่มี ตรงกับแต่ละไฟล์และไดเรกทอรีที่มีตัวเลือก-v( โหมด verbose )

ตัวเลือกอื่น ๆ ที่ควรค่าแก่การจดจำคือ-e(ให้เกียรติสแคปe ) ที่อนุญาตregular expressionsให้คุณค้นหาแท็บ ( \t), บรรทัดใหม่ ( \n), เป็นต้น แม้คุณสามารถใช้-fเพื่อบังคับใช้สิทธิ์ (แน่นอนเฉพาะเมื่อผู้ใช้มีสิทธิ์เขียน) และ-dเพื่อรักษาเวลาการปรับเปลี่ยน ')

สุดท้ายถ้าคุณไม่แน่ใจว่าจะทำอะไรให้ใช้-s( โหมดจำลอง )


2
ดีกว่ามากที่ข้อเสนอแนะและความเรียบง่ายกว่า sed ฉันแค่หวังว่ามันจะได้รับอนุญาตให้ดำเนินการกับชื่อไฟล์แล้วมันก็จะสมบูรณ์แบบตามที่เป็นอยู่
Kzqai

1
ฉันชอบ -s (โหมดจำลอง) :-)
erm3nda

25

วิธีทำการค้นหาและแทนที่มากกว่าหลายไฟล์แนะนำ:

คุณสามารถใช้ find และ sed แต่ฉันพบว่า perl บรรทัดเล็ก ๆ นี้ทำงานได้ดี

perl -pi -w -e 's/search/replace/g;' *.php
  • -e หมายถึงรันรหัสบรรทัดต่อไปนี้
  • -i หมายถึงการแก้ไขในสถานที่
  • - เขียนคำเตือน
  • -p วนรอบไฟล์อินพุตพิมพ์แต่ละบรรทัดหลังจากสคริปต์ถูกนำไปใช้

ผลลัพธ์ที่ดีที่สุดของฉันมาจากการใช้ Perl และ grep (เพื่อให้แน่ใจว่าไฟล์มีการแสดงออกการค้นหา)

perl -pi -w -e 's/search/replace/g;' $( grep -rl 'search' )

13

คุณสามารถใช้ Vim ในโหมด Ex:

แทนที่สตริง ALF ด้วย BRA ในไฟล์ทั้งหมดในไดเรกทอรีปัจจุบันหรือไม่?

for CHA in *
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

ทำแบบเดียวกันซ้ำสำหรับไดเรกทอรีย่อย?

find -type f -exec ex -sc '%s/ALF/BRA/g' -cx {} ';'

แทนที่เฉพาะถ้าชื่อไฟล์ตรงกับสายอื่น?

for CHA in *.txt
do
  ex -sc '%s/ALF/BRA/g' -cx "$CHA"
done

แทนที่เฉพาะในกรณีที่พบสตริงในบริบท?

ex -sc 'g/DEL/s/ALF/BRA/g' -cx file

แทนที่หากสตริงอยู่ในหมายเลขบรรทัดที่แน่นอน?

ex -sc '2s/ALF/BRA/g' -cx file

แทนที่สตริงจำนวนมากด้วยการแทนที่เดียวกัน

ex -sc '%s/\vALF|ECH/BRA/g' -cx file

แทนที่หลายสตริงด้วยการแทนที่ที่แตกต่างกัน

ex -sc '%s/ALF/BRA/g|%s/FOX/GOL/g' -cx file

13

ฉันใช้สิ่งนี้:

grep -r "old_string" -l | tr '\n' ' ' | xargs sed -i 's/old_string/new_string/g'
  1. old_stringรายชื่อไฟล์ทั้งหมดที่มี

  2. แทนที่การขึ้นบรรทัดใหม่ในผลที่มีช่องว่าง sed(เพื่อให้รายชื่อของไฟล์ที่สามารถเลี้ยง

  3. รันsedไฟล์เหล่านั้นเพื่อแทนที่สตริงเก่าด้วยใหม่

อัปเดต:ผลลัพธ์ข้างต้นจะล้มเหลวในชื่อไฟล์ที่มีช่องว่าง ให้ใช้:

grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'


โปรดทราบว่าสิ่งนี้จะล้มเหลวหากชื่อไฟล์ใด ๆ ของคุณมีช่องว่างแท็บหรือบรรทัดใหม่ ใช้grep --null -lr "old_string" | xargs --null sed -i 's/old_string/new_string/g'จะทำให้มันจัดการกับชื่อไฟล์โดยพลการ
terdon

ขอบคุณเพื่อน. เพิ่มการอัปเดตและปล่อยให้โค้ดเก่าเป็นสิ่งที่น่าสนใจซึ่งอาจเป็นประโยชน์กับบางคนที่ไม่ทราบถึงพฤติกรรมนี้
o_o_o--

6

จากมุมมองของผู้ใช้เป็นเครื่องมือที่ดีและง่าย Unix qsubstที่จะทำงานได้อย่างสมบูรณ์แบบมี ตัวอย่างเช่น,

% qsubst foo bar *.c *.h

จะแทนที่fooด้วยbarในไฟล์ C ทั้งหมดของฉัน คุณสมบัติที่ดีคือqsubstจะทำการแทนที่การค้นหานั่นคือมันจะแสดงให้ฉันเห็นในแต่ละครั้งที่เกิดขึ้นfooและถามว่าฉันต้องการแทนที่หรือไม่ [คุณสามารถแทนที่-goตัวเลือกแบบไม่มีเงื่อนไข (ไม่มีการถาม) ด้วยตัวเลือกและมีตัวเลือกอื่น ๆ เช่น-wหากคุณต้องการแทนที่fooเมื่อมันเป็นทั้งคำเท่านั้น]

วิธีรับ: qsubstถูกคิดค้นโดย der Mouse (จาก McGill) และโพสต์ไปยังcomp.unix.sources 11 (7)ในเดือนสิงหาคม 1987 มีรุ่นที่ปรับปรุงแล้ว ตัวอย่างเช่นรุ่น NetBSD qsubst.c,v 1.8 2004/11/01รวบรวมและทำงานอย่างสมบูรณ์บน mac ของฉัน


2

ฉันต้องการสิ่งที่จะให้ตัวเลือกแห้งและจะทำงานซ้ำกับ glob และหลังจากพยายามที่จะทำด้วยawkและsedฉันให้ขึ้นและแทนที่จะทำมันในหลาม

สคริปต์ค้นหาซ้ำไฟล์ทั้งหมดที่ตรงกับรูปแบบ glob (เช่น--glob="*.html") สำหรับ regex และแทนที่ด้วย regex ทดแทน:

find_replace.py [--dir=my_folder] \
    --search-regex=<search_regex> \
    --replace-regex=<replace_regex> \
    --glob=[glob_pattern] \
    --dry-run

ทุกตัวเลือกที่ยาวเช่นมีตัวเลือกในระยะสั้นที่สอดคล้องกันคือ--search-regex -sเรียกใช้ด้วย-hเพื่อดูตัวเลือกทั้งหมด

ตัวอย่างเช่นสิ่งนี้จะพลิกวันที่ทั้งหมดจาก2017-12-31เป็น31-12-2017:

python replace.py --glob=myfile.txt \
    --search-regex="(\d{4})-(\d{2})-(\d{2})" \
    --replace-regex="\3-\2-\1" \
    --dry-run --verbose
import os
import fnmatch
import sys
import shutil
import re

import argparse

def find_replace(cfg):
    search_pattern = re.compile(cfg.search_regex)

    if cfg.dry_run:
        print('THIS IS A DRY RUN -- NO FILES WILL BE CHANGED!')

    for path, dirs, files in os.walk(os.path.abspath(cfg.dir)):
        for filename in fnmatch.filter(files, cfg.glob):

            if cfg.print_parent_folder:
                pardir = os.path.normpath(os.path.join(path, '..'))
                pardir = os.path.split(pardir)[-1]
                print('[%s]' % pardir)
            filepath = os.path.join(path, filename)

            # backup original file
            if cfg.create_backup:
                backup_path = filepath + '.bak'

                while os.path.exists(backup_path):
                    backup_path += '.bak'
                print('DBG: creating backup', backup_path)
                shutil.copyfile(filepath, backup_path)

            with open(filepath) as f:
                old_text = f.read()

            all_matches = search_pattern.findall(old_text)

            if all_matches:

                print('Found {} matches in file {}'.format(len(all_matches), filename))

                new_text = search_pattern.sub(cfg.replace_regex, old_text)

                if not cfg.dry_run:
                    with open(filepath, "w") as f:
                        print('DBG: replacing in file', filepath)
                        f.write(new_text)
                else:
                    for idx, matches in enumerate(all_matches):
                        print("Match #{}: {}".format(idx, matches))

                    print("NEW TEXT:\n{}".format(new_text))

            elif cfg.verbose:
                print('File {} does not contain search regex "{}"'.format(filename, cfg.search_regex))


if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='''DESCRIPTION:
    Find and replace recursively from the given folder using regular expressions''',
                                     formatter_class=argparse.RawDescriptionHelpFormatter,
                                     epilog='''USAGE:
    {0} -d [my_folder] -s <search_regex> -r <replace_regex> -g [glob_pattern]

    '''.format(os.path.basename(sys.argv[0])))

    parser.add_argument('--dir', '-d',
                        help='folder to search in; by default current folder',
                        default='.')

    parser.add_argument('--search-regex', '-s',
                        help='search regex',
                        required=True)

    parser.add_argument('--replace-regex', '-r',
                        help='replacement regex',
                        required=True)

    parser.add_argument('--glob', '-g',
                        help='glob pattern, i.e. *.html',
                        default="*.*")

    parser.add_argument('--dry-run', '-dr',
                        action='store_true',
                        help="don't replace anything just show what is going to be done",
                        default=False)

    parser.add_argument('--create-backup', '-b',
                        action='store_true',
                        help='Create backup files',
                        default=False)

    parser.add_argument('--verbose', '-v',
                        action='store_true',
                        help="Show files which don't match the search regex",
                        default=False)

    parser.add_argument('--print-parent-folder', '-p',
                        action='store_true',
                        help="Show the parent info for debug",
                        default=False)

    config = parser.parse_args(sys.argv[1:])

    find_replace(config)

Here เป็นเวอร์ชันที่อัปเดตของสคริปต์ซึ่งเน้นคำค้นหาและการแทนที่ด้วยสีที่ต่างกัน


1
ฉันไม่เข้าใจว่าทำไมคุณถึงทำสิ่งที่ซับซ้อนนี้ สำหรับการเรียกซ้ำใช้อย่างใดอย่างทุบตี (หรือเปลือกของคุณเทียบเท่า) globstarตัวเลือกและ**globs findหรือ sedสำหรับการทำงานแห้งเพียงแค่ใช้ หากคุณไม่ใช้-iตัวเลือกนี้จะไม่มีการเปลี่ยนแปลงใด ๆ สำหรับการใช้งานสำรองsed -i.bak(หรือperl -i .bak); grep PATTERN file || echo fileสำหรับไฟล์ที่ไม่ตรงกับการใช้งาน และทำไมในโลกนี้คุณจะมีงูหลามขยาย glob แทนที่จะปล่อยให้เปลือกทำ ทำไมscript.py --glob=foo*แทนที่จะเป็นแค่script.py foo*?
terdon

1
เหตุใดฉันจึงง่ายมาก: (1) เหนือสิ่งอื่นใดความสะดวกในการดีบั๊ก (2) ใช้เครื่องมือเดียวที่มีเอกสารอย่างดีพร้อมชุมชนที่สนับสนุน (3) ไม่รู้จักsedและawkไม่เต็มใจที่จะลงทุนเวลาพิเศษในการเรียนรู้ (4) ความสามารถในการอ่าน (5) วิธีนี้จะทำงานบนระบบที่ไม่ใช่ posix (ไม่ใช่ว่าฉันต้องการสิ่งนี้ แต่อาจมีคนอื่น)
ccpizza

1

ripgrep (ชื่อคำสั่งrg) เป็นgrepเครื่องมือ แต่รองรับการค้นหาและแทนที่เช่นกัน

$ cat ip.txt
dark blue and light blue
light orange
blue sky
$ # by default, line number is displayed if output destination is stdout
$ # by default, only lines that matched the given pattern is displayed
$ # 'blue' is search pattern and -r 'red' is replacement string
$ rg 'blue' -r 'red' ip.txt
1:dark red and light red
3:red sky

$ # --passthru option is useful to print all lines, whether or not it matched
$ # -N will disable line number prefix
$ # this command is similar to: sed 's/blue/red/g' ip.txt
$ rg --passthru -N 'blue' -r 'red' ip.txt
dark red and light red
light orange
red sky


rg ไม่รองรับตัวเลือกแบบแทนที่ดังนั้นคุณต้องทำด้วยตัวเอง

$ # -N isn't needed here as output destination is a file
$ rg --passthru 'blue' -r 'red' ip.txt > tmp.txt && mv tmp.txt ip.txt
$ cat ip.txt
dark red and light red
light orange
red sky


ดูเอกสารประกอบสนิม regexสำหรับไวยากรณ์นิพจน์ทั่วไปและคุณสมบัติ -Pสวิทช์จะช่วยให้PCRE2รสชาติ rgรองรับ Unicode โดยค่าเริ่มต้น

$ # non-greedy quantifier is supported
$ echo 'food land bark sand band cue combat' | rg 'foo.*?ba' -r 'X'
Xrk sand band cue combat

$ # unicode support
$ echo 'fox:αλεπού,eagle:αετός' | rg '\p{L}+' -r '($0)'
(fox):(αλεπού),(eagle):(αετός)

$ # set operator example, remove all punctuation characters except . ! and ?
$ para='"Hi", there! How *are* you? All fine here.'
$ echo "$para" | rg '[[:punct:]--[.!?]]+' -r ''
Hi there! How are you? All fine here.

$ # use -P if you need even more advanced features
$ echo 'car bat cod map' | rg -P '(bat|map)(*SKIP)(*F)|\w+' -r '[$0]'
[car] bat [cod] map


เช่นเดียวgrepกับ-Fตัวเลือกนี้จะช่วยให้จับคู่สตริงคงที่เป็นตัวเลือกที่มีประโยชน์ซึ่งฉันรู้สึกว่าsedควรนำไปใช้เช่นกัน

$ printf '2.3/[4]*6\nfoo\n5.3-[4]*9\n' | rg --passthru -F '[4]*' -r '2'
2.3/26
foo
5.3-29


อีกทางเลือกที่สะดวกคือการ-Uเปิดใช้งานการจับคู่แบบหลายบรรทัด

$ # (?s) flag will allow . to match newline characters as well
$ printf '42\nHi there\nHave a Nice Day' | rg --passthru -U '(?s)the.*ice' -r ''
42
Hi  Day


rg สามารถจัดการไฟล์สไตล์ดอสได้เช่นกัน

$ # same as: sed -E 's/\w+(\r?)$/123\1/'
$ printf 'hi there\r\ngood day\r\n' | rg --passthru --crlf '\w+$' -r '123'
hi 123
good 123


ข้อดีอีกอย่างของrgมันคือมันน่าจะเร็วกว่าsed

$ # for small files, initial processing time of rg is a large component
$ time echo 'aba' | sed 's/a/b/g' > f1
real    0m0.002s
$ time echo 'aba' | rg --passthru 'a' -r 'b' > f2
real    0m0.007s

$ # for larger files, rg is likely to be faster
$ # 6.2M sample ASCII file
$ wget https://norvig.com/big.txt    
$ time LC_ALL=C sed 's/\bcat\b/dog/g' big.txt > f1
real    0m0.060s
$ time rg --passthru '\bcat\b' -r 'dog' big.txt > f2
real    0m0.048s
$ diff -s f1 f2
Files f1 and f2 are identical

$ time LC_ALL=C sed -E 's/\b(\w+)(\s+\1)+\b/\1/g' big.txt > f1
real    0m0.725s
$ time rg --no-pcre2-unicode --passthru -wP '(\w+)(\s+\1)+' -r '$1' big.txt > f2
real    0m0.093s
$ diff -s f1 f2
Files f1 and f2 are identical
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.