Sed - แทนที่อินสแตนซ์ k แรกของคำในไฟล์


24

ฉันต้องการแทนที่kอินสแตนซ์แรกของคำ

ฉันจะทำสิ่งนี้ได้อย่างไร

เช่น. ไฟล์ Say foo.txtมี 100 อินสแตนซ์ของคำว่า 'linux'

ฉันต้องแทนที่ 50 รายการแรกเท่านั้น


1
คุณสามารถอ้างถึงสิ่งนี้: unix.stackexchange.com/questions/21178/…
cuonglm

คุณต้องการเครื่องมือที่เหมาะสมหรือเป็นที่ยอมรับหรือไม่? คุณต้องการทำงานในบรรทัดคำสั่งหรือแก้ไขข้อความได้หรือไม่?
evilsoup

สิ่งใดก็ตามที่ทำงานบนบรรทัดคำสั่งเป็นที่ยอมรับได้
narendra-choudhary

คำตอบ:


31

ส่วนแรกด้านล่างอธิบายถึงการใช้sedเพื่อเปลี่ยน k- เกิดขึ้นครั้งแรกในบรรทัด ส่วนที่สองขยายวิธีนี้เพื่อเปลี่ยนเฉพาะการเกิด k ครั้งแรกในไฟล์โดยไม่คำนึงถึงบรรทัดที่ปรากฏ

โซลูชั่นที่มุ่งเน้นสาย

ด้วย sed มาตรฐานมีคำสั่งให้แทนที่การเกิดขึ้นที่ k ของคำบนบรรทัด ถ้าkเป็น 3 เช่น:

sed 's/old/new/3'

หรือหนึ่งสามารถแทนที่เกิดขึ้นทั้งหมดด้วย:

sed 's/old/new/g'

ทั้งสองอย่างนี้เป็นสิ่งที่คุณต้องการ

GNU sedเสนอส่วนขยายที่จะเปลี่ยนการเกิดตัว k และหลังจากนั้นทั้งหมด ถ้า k เป็น 3 ตัวอย่างเช่น:

sed 's/old/new/g3'

สามารถรวมกันเพื่อทำสิ่งที่คุณต้องการ ในการเปลี่ยน 3 เหตุการณ์แรก:

$ echo old old old old old | sed -E 's/\<old\>/\n/g4; s/\<old\>/new/g; s/\n/old/g'
new new new old old

\nมีประโยชน์ตรงไหนเพราะเรามั่นใจได้ว่ามันจะไม่เกิดขึ้นบนเส้น

คำอธิบาย:

เราใช้sedคำสั่งการแทนที่สามคำ:

  • s/\<old\>/\n/g4

    นี้ส่วนขยาย GNU เพื่อแทนที่สี่และเกิดขึ้นตามมาทั้งหมดด้วยold\n

    คุณลักษณะเพิ่มเติมของ regex \<ถูกใช้เพื่อให้ตรงกับจุดเริ่มต้นของคำและ\>เพื่อให้ตรงกับจุดสิ้นสุดของคำ สิ่งนี้รับประกันว่าจะจับคู่คำที่สมบูรณ์เท่านั้น regex ขยายต้องมีตัวเลือกในการ-Esed

  • s/\<old\>/new/g

    เพียงสามเกิดขึ้นครั้งแรกที่ยังคงอยู่และแทนที่พวกเขาทั้งหมดนี้ด้วยoldnew

  • s/\n/old/g

    เหตุการณ์ที่สี่และที่เหลือทั้งหมดของoldถูกแทนที่ด้วย\nในขั้นตอนแรก สิ่งนี้จะคืนพวกเขากลับสู่สถานะดั้งเดิม

โซลูชันที่ไม่ใช่ GNU

หาก GNU sed ไม่พร้อมใช้งานและคุณต้องการเปลี่ยน 3 รายการแรกเป็นเป็นoldให้newใช้สามsคำสั่ง:

$ echo old old old old old | sed -E -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'
new new new old old

นี้ทำงานได้ดีเมื่อkเป็นจำนวนน้อย kแต่ตาชั่งไม่ดีไปจนถึงขนาดใหญ่

เนื่องจาก seds ที่ไม่ใช่ GNU บางตัวไม่สนับสนุนการรวมคำสั่งกับเครื่องหมายอัฒภาคแต่ละคำสั่งที่นี่จึงถูกนำเสนอพร้อมตัว-eเลือกของตนเอง นอกจากนี้ยังอาจมีความจำเป็นต้องตรวจสอบว่าคุณsedสนับสนุนคำสัญลักษณ์ขอบเขตและ\<\>

วิธีแก้ปัญหาไฟล์

เราสามารถบอกให้ sed อ่านไฟล์ทั้งหมดจากนั้นทำการแทนที่ ตัวอย่างเช่นในการแทนที่การเกิดขึ้นสามครั้งแรกของการoldใช้ sed แบบ BSD:

sed -E -e 'H;1h;$!d;x' -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/'

คำสั่ง sed H;1h;$!d;xอ่านไฟล์ทั้งหมดใน

เนื่องจากข้างต้นไม่ได้ใช้ส่วนขยาย GNU ใด ๆ จึงควรทำงานกับ BSD (OSX) sed หมายเหตุคิดว่าวิธีนี้ต้องใช้sedที่สามารถจัดการกับสายยาว GNU น่าsedจะใช้ได้ ผู้ที่ใช้เวอร์ชั่นที่ไม่ใช่ GNU sedควรทดสอบความสามารถในการจัดการกับสายยาว

ด้วยความช่วยเหลือของ GNU เราสามารถใช้gเคล็ดลับที่อธิบายไว้ข้างต้นได้ แต่\nแทนที่ด้วยด้วย\x00เพื่อแทนที่เหตุการณ์สามเหตุการณ์แรก:

sed -E -e 'H;1h;$!d;x; s/\<old\>/\x00/g4; s/\<old\>/new/g; s/\x00/old/g'

วิธีการนี้วัดได้ดีและkมีขนาดใหญ่ แม้ว่าจะถือว่า\x00ไม่ได้อยู่ในสตริงเดิมของคุณ เนื่องจากเป็นไปไม่ได้ที่จะใส่อักขระ\x00ในสตริง bash จึงเป็นข้อสันนิษฐานที่ปลอดภัย


5
ใช้งานได้กับบรรทัดเท่านั้นและจะเปลี่ยนการปรากฏ 4 ครั้งแรกในทุกบรรทัด

1
@mikeserv ความคิดที่ยอดเยี่ยม! อัปเดตคำตอบแล้ว
John1024

(1) คุณพูดถึง GNU และไม่ใช่ GNU sed tr '\n' '|' < input_file | sed …และแนะนำ แต่แน่นอนว่าการแปลงอินพุตทั้งหมดเป็นหนึ่งบรรทัดและบางส่วนที่ไม่ใช่ GNU seds ไม่สามารถจัดการกับสายยาวโดยพลการ (2) คุณพูดว่า“ …ข้างต้นสตริงที่ยกมา'|'ควรถูกแทนที่ด้วยอักขระใด ๆ หรือสตริงของอักขระ…” แต่คุณไม่สามารถใช้trเพื่อแทนที่อักขระด้วยสตริง (ความยาว> 1) (3) -e 's/\<old\>/new/' -e 's/\<old\>/w/' | tr '\000' '\n'\>/newในตัวอย่างล่าสุดของคุณที่คุณพูด -e 's/\<old\>/new/' -e 's/\<old\>/new/' -e 's/\<old\>/new/' | tr '\000' '\n'นี้น่าจะเป็นสำหรับการพิมพ์ผิด
G-Man กล่าวว่า 'Reinstate Monica'

@ G-Man ขอบคุณมาก! ฉันได้อัพเดตคำตอบแล้ว
John1024

มันช่างน่าเกลียดเหลือเกิน
Louis Maddox

8

ใช้ Awk

คำสั่ง awk สามารถใช้เพื่อแทนที่เหตุการณ์ N แรกของคำด้วยการแทนที่
คำสั่งจะแทนที่หากคำนั้นตรงกันทั้งหมด

ในตัวอย่างด้านล่างฉันกำลังแทนที่27เหตุการณ์แรกของoldด้วยnew

ใช้งานย่อย

awk '{for(i=1;i<=NF;i++){if(x<27&&$i=="old"){x++;sub("old","new",$i)}}}1' file

คำสั่งนี้วนซ้ำในแต่ละฟิลด์จนกว่าจะตรงกับoldมันตรวจสอบตัวนับที่ต่ำกว่า 27 เพิ่มขึ้นและทดแทนการแข่งขันครั้งแรกในบรรทัด จากนั้นย้ายไปยังฟิลด์ / บรรทัดถัดไปและทำซ้ำ

การแทนที่ฟิลด์ด้วยตนเอง

awk '{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

คล้ายกับคำสั่งก่อน แต่มันแล้วมีเครื่องหมายที่สนามมันก็ขึ้นอยู่กับ($i)มันก็เปลี่ยนค่าของข้อมูลที่ได้จากการoldnew

ทำการตรวจสอบก่อน

awk '/old/&&x<27{for(i=1;i<=NF;i++)if(x<27&&$i=="old"&&$i="new")x++}1' file

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

ผล

เช่น

old bold old old old
old old nold old old
old old old gold old
old gold gold old old
old old old man old old
old old old old dog old
old old old old say old
old old old old blah old

ไปยัง

new bold new new new
new new nold new new
new new new gold new
new gold gold new new
new new new man new new
new new new new dog new
new new old old say old
old old old old blah old

คนแรก (ใช้ย่อย) ทำสิ่งที่ผิดถ้าสตริง "เก่า" นำหน้าคำ *เก่า; เช่น“ ให้ทองคำแก่ชายชรา” →“ มอบ gnew แก่ชายชรา”
G-Man พูดว่า 'Reinstate Monica'

@ G-Man ใช่ฉันลืม$iบิตมันได้รับการแก้ไขขอบคุณ :)

7

สมมติว่าคุณต้องการแทนที่สตริงสามอินสแตนซ์แรกเท่านั้น ...

seq 11 100 311 | 
sed -e 's/1/\
&/g'              \ #s/match string/\nmatch string/globally 
-e :t             \ #define label t
-e '/\n/{ x'      \ #newlines must match - exchange hold and pattern spaces
-e '/.\{3\}/!{'   \ #if not 3 characters in hold space do
-e     's/$/./'   \ #add a new char to hold space
-e      x         \ #exchange hold/pattern spaces again
-e     's/\n1/2/' \ #replace first occurring '\n1' string w/ '2' string
-e     'b t'      \ #branch back to label t
-e '};x'          \ #end match function; exchange hold/pattern spaces
-e '};s/\n//g'      #end match function; remove all newline characters

หมายเหตุ: ข้างต้นอาจไม่ทำงานกับความคิดเห็นฝังตัว
... หรือในตัวอย่างของฉันใน '1' ...

เอาท์พุท:

22
211
211
311

ฉันใช้สองเทคนิคที่น่าทึ่ง ในครั้งแรกที่เกิดขึ้นของทุกคนบนเส้นจะถูกแทนที่ด้วย1 \n1ด้วยวิธีนี้เมื่อฉันทำการเปลี่ยนแบบเรียกซ้ำครั้งถัดไปฉันสามารถแน่ใจได้ว่าจะไม่แทนที่การเกิดขึ้นสองครั้งหากสตริงการแทนที่ของฉันมีสตริงการแทนที่ของฉัน ตัวอย่างเช่นถ้าฉันแทนที่heด้วยheyมันจะยังคงทำงาน

ฉันทำสิ่งนี้เช่น:

s/1/\
&/g

ประการที่สองฉันนับการแทนที่ด้วยการเพิ่มตัวละครในhพื้นที่เก่าสำหรับแต่ละเหตุการณ์ เมื่อฉันถึงสามไม่เกิดขึ้นอีก หากคุณใช้สิ่งนี้กับข้อมูลของคุณและเปลี่ยนการเปลี่ยน\{3\}ทั้งหมดที่คุณต้องการและที่/\n1/อยู่ในสิ่งที่คุณต้องการเปลี่ยนคุณควรเปลี่ยนเฉพาะที่คุณต้องการเท่านั้น

ฉันทำทุก-eอย่างเพื่อให้อ่านได้เท่านั้น POSIXly มันสามารถเขียนดังนี้:

nl='
'; sed "s/1/\\$nl&/g;:t${nl}/\n/{x;/.\{3\}/!{${nl}s/$/./;x;s/\n1/2/;bt$nl};x$nl};s/\n//g"

และด้วย GNU sed:

sed 's/1/\n&/g;:t;/\n/{x;/.\{3\}/!{s/$/./;x;s/\n1/2/;bt};x};s/\n//g'

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

นี่คือฟังก์ชั่นเชลล์เล็ก ๆ ที่รวมมันเข้ากับคำสั่งที่ถูกเรียกใช้งานอย่างง่าย:

firstn() { sed "s/$2/\
&/g;:t 
    /\n/{x
        /.\{$(($1))"',\}/!{
            s/$/./; x; s/\n'"$2/$3"'/
            b t
        };x
};s/\n//g'; }

ดังนั้นด้วยสิ่งที่ฉันสามารถทำได้:

seq 11 100 311 | firstn 7 1 5

... และรับ ...

55
555
255
311

...หรือ...

seq 10 1 25 | firstn 6 '\(.\)\([1-5]\)' '\15\2'

... เพื่อรับ ...

10
151
152
153
154
155
16
17
18
19
20
251
22
23
24
25

... หรือเพื่อให้ตรงกับตัวอย่างของคุณ(ตามลำดับความสำคัญน้อยกว่า) :

yes linux | head -n 10 | firstn 5 linux 'linux is an os kernel'
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux is an os kernel
linux
linux
linux
linux
linux

4

ทางเลือกสั้น ๆ ใน Perl:

perl -pe 'BEGIN{$n=3} 1 while s/old/new/ && ++$i < $n' your_file

เปลี่ยนค่าของ `$ n $ เป็นความชอบของคุณ

มันทำงานอย่างไร:

  • ทุกสายจะช่วยให้พยายามที่จะทดแทนnewสำหรับold( s/old/new/) และเมื่อใดก็ตามที่สามารถมันเพิ่มตัวแปร$i( ++$i)
  • มันยังคงทำงานบนบรรทัด ( 1 while ...) ตราบใดที่มันมีการ$nทดแทนน้อยกว่าทั้งหมดและสามารถทำการทดแทนอย่างน้อยหนึ่งรายการในบรรทัดนั้น

4

ใช้เปลือกวงและex!

{ for i in {1..50}; do printf %s\\n '0/old/s//new/'; done; echo x;} | ex file.txt

ใช่มันโง่เล็กน้อย

;)

หมายเหตุ: สิ่งนี้อาจล้มเหลวหากไฟล์มีอินสแตนซ์น้อยกว่า 50 oldรายการ (ฉันไม่ได้ทำการทดสอบ) ถ้าเป็นเช่นนั้นก็จะทำให้ไฟล์ไม่ได้รับการแก้ไข


ดีกว่าใช้ Vim

vim file.txt
qqgg/old<CR>:s/old/new/<CR>q49@q
:x

คำอธิบาย:

q                                # Start recording macro
 q                               # Into register q
  gg                             # Go to start of file
    /old<CR>                     # Go to first instance of 'old'
            :s/old/new/<CR>      # Change it to 'new'
                           q     # Stop recording
                            49@q # Replay macro 49 times

:x  # Save and exit

: s // new <CR> ควรทำงานได้ดีเพราะ regex ที่ว่างเปล่าใช้การค้นหาที่ใช้ล่าสุดครั้งล่าสุด
eike

3

วิธีแก้ปัญหาที่ง่าย แต่ไม่เร็วมากคือการวนซ้ำคำสั่งที่อธิบายไว้ใน /programming/148451/how-to-use-sed-to-replace-only-the-first-occurrence-in-a -ไฟล์

for i in $(seq 50) ; do sed -i -e "0,/oldword/s//newword/"  file.txt  ; done

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


+1 สำหรับการระบุว่าการแทนที่ "เก่า" ด้วย "ตัวหนา" อาจทำให้เกิดปัญหาได้
G-Man กล่าวว่า 'Reinstate Monica'

2

ด้วย GNU awkคุณสามารถตั้งค่าตัวคั่นเรคคอร์ดRSเป็นคำที่ต้องการแทนที่ด้วยเขตแดนของคำ จากนั้นเป็นกรณีของการตั้งค่าตัวคั่นเร็กคอร์ดในเอาท์พุทเป็นคำแทนที่สำหรับเรกkคอร์ดแรกในขณะที่ยังคงรักษาตัวแยกเรกคอร์ดดั้งเดิมสำหรับส่วนเหลือ

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, NR <= limit? replacement: RT}' file

หรือ

awk -vRS='\\ylinux\\y' -vreplacement=unix -vlimit=50 \
'{printf "%s%s", $0, limit--? replacement: RT}' file
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.