ฉันจะใช้ไฟล์ในคำสั่งและเปลี่ยนทิศทางเอาต์พุตไปยังไฟล์เดียวกันโดยไม่ตัดทอนได้อย่างไร


98

โดยทั่วไปฉันต้องการใช้เป็นข้อความอินพุตจากไฟล์ลบบรรทัดออกจากไฟล์นั้นและส่งเอาต์พุตกลับไปที่ไฟล์เดียวกัน บางสิ่งบางอย่างตามเส้นเหล่านี้หากทำให้ชัดเจนขึ้น

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > file_name

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


คำตอบ:


85

คุณไม่สามารถทำได้เนื่องจาก bash ประมวลผลการเปลี่ยนเส้นทางก่อนจากนั้นเรียกใช้คำสั่ง ดังนั้นเมื่อ grep ดู file_name มันว่างเปล่าอยู่แล้ว คุณสามารถใช้ไฟล์ชั่วคราวได้

#!/bin/sh
tmpfile=$(mktemp)
grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > ${tmpfile}
cat ${tmpfile} > file_name
rm -f ${tmpfile}

เช่นนั้นให้พิจารณาใช้mktempเพื่อสร้างtmpfileแต่โปรดทราบว่าไม่ใช่ POSIX


47
สาเหตุที่คุณไม่สามารถทำได้: bash ประมวลผลการเปลี่ยนเส้นทางก่อนจากนั้นเรียกใช้คำสั่ง ดังนั้นเมื่อ grep ดู file_name มันว่างเปล่าอยู่แล้ว
glenn jackman

1
@glennjackman: โดย "กระบวนการเปลี่ยนเส้นทางคุณหมายความว่าในกรณีของ> มันเปิดไฟล์และล้างไฟล์และในกรณีของ >> มันจะเปิดขึ้นเท่านั้น"?
Razvan

2
ใช่ แต่ของโน้ตในสถานการณ์เช่นนี้>การเปลี่ยนเส้นทางจะเปิดไฟล์และตัดมันก่อนที่จะgrepเปิดตัวเปลือก
glenn jackman

1
ดูคำตอบของฉันหากคุณไม่ต้องการใช้ไฟล์ชั่วคราว แต่โปรดอย่าโหวตความคิดเห็นนี้
Zack Morris

แทนที่จะเป็นเช่นนี้ควรยอมรับคำตอบโดยใช้spongeคำสั่ง
vlz

98

ใช้ฟองน้ำสำหรับงานประเภทนี้ เป็นส่วนหนึ่งของ moreutils

ลองใช้คำสั่งนี้:

 grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | sponge file_name

4
ขอบคุณสำหรับคำตอบ. ในฐานะที่เป็นประโยชน์เพิ่มเติมหากคุณใช้ homebrew บน Mac สามารถใช้brew install moreutilsไฟล์.
Anthony Panozzo

2
หรือsudo apt-get install moreutilsบนระบบที่ใช้ Debian
โยนาห์

3
ประณาม! ขอบคุณที่แนะนำฉันให้รู้จักกับ moreutils =) มีโปรแกรมดีๆที่นั่น!
netigger

ขอบคุณมาก moreutils สำหรับการช่วยเหลือ! ฟองน้ำเหมือนเจ้านาย!
aqquadro

3
คำเตือน "ฟองน้ำ" เป็นอันตรายดังนั้นหากคุณมีข้อผิดพลาดในคำสั่งของคุณคุณสามารถล้างไฟล์อินพุตของคุณได้ (เหมือนที่ฉันลองใช้ฟองน้ำครั้งแรก) ตรวจสอบให้แน่ใจว่าคำสั่งของคุณใช้งานได้และ / หรือไฟล์อินพุตอยู่ภายใต้การควบคุมเวอร์ชันหากคุณพยายามทำซ้ำเพื่อให้คำสั่งใช้งานได้
user107172

19

ใช้ sed แทน:

sed -i '/seg[0-9]\{1,\}\.[0-9]\{1\}/d' file_name

1
iirc -iเป็นส่วนขยายเฉพาะของ GNU เพียงแค่สังเกต
c00kiemon5ter

4
เมื่อวันที่ BSD * (และด้วยเหตุนี้ยัง OSX) ที่คุณสามารถพูด-i ''เพื่อขยายเป็นไม่ได้รับคำสั่งอย่างเคร่งครัด แต่-iตัวเลือกที่ไม่จำเป็นต้องมีบางข้อโต้แย้ง
tripleee

16

ลองวิธีง่ายๆนี้

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

ไฟล์ของคุณจะไม่ว่างเปล่าในครั้งนี้ :) และผลลัพธ์ของคุณจะถูกพิมพ์ไปยังเทอร์มินัลของคุณด้วย


1
ฉันชอบวิธีนี้! และหากคุณไม่ต้องการให้พิมพ์ในเทอร์มินัลคุณยังสามารถเปลี่ยนเส้นทางผลลัพธ์ไปยัง/dev/nullตำแหน่งที่คล้ายกันได้
Frozn

4
สิ่งนี้จะล้างเนื้อหาของไฟล์ที่นี่เช่นกัน นั่นเป็นเพราะความแตกต่างของ GNU / BSD หรือไม่? ฉันใช้ macOS ...
ssc

7

คุณไม่สามารถใช้ตัวดำเนินการเปลี่ยนเส้นทาง ( >หรือ>>) ไปยังไฟล์เดียวกันได้เนื่องจากมีลำดับความสำคัญสูงกว่าและจะสร้าง / ตัดทอนไฟล์ก่อนที่จะเรียกใช้คำสั่งด้วยซ้ำ เพื่อหลีกเลี่ยงการที่คุณควรใช้เครื่องมือที่เหมาะสมเช่นtee, sponge, sed -iหรือเครื่องมืออื่นใดที่สามารถเขียนผลไปยังแฟ้ม (เช่นsort file -o file)

โดยทั่วไปการเปลี่ยนทิศทางอินพุตไปยังไฟล์ต้นฉบับเดียวกันนั้นไม่สมเหตุสมผลและคุณควรใช้ตัวแก้ไขแบบแทนที่ที่เหมาะสมสำหรับสิ่งนั้นเช่น Ex editor (ส่วนหนึ่งของ Vim):

ex '+g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' -scwq file_name

ที่ไหน:

  • '+cmd'/ -c- รันคำสั่ง Ex / Vim
  • g/pattern/d- ลบเส้นที่ตรงกับรูปแบบโดยใช้global (help :g )
  • -s- โหมดเงียบ ( man ex)
  • -c wq- ดำเนินการ:writeและ:quitคำสั่ง

คุณอาจจะใช้sedเพื่อให้บรรลุเดียวกัน (ตามที่แสดงไว้ในคำตอบอื่น ๆ ) แต่ในสถานที่ ( -i) เป็นส่วนขยาย FreeBSD ที่ไม่ได้มาตรฐาน (อาจจะทำงานแตกต่างกันระหว่าง Unix / Linux) และพื้นก็เป็นs tream เอ็ด Itor ไม่แก้ไขไฟล์ . ดู: โหมด Ex มีการใช้งานจริงหรือไม่?


6

ทางเลือกหนึ่งซับ - ตั้งค่าเนื้อหาของไฟล์เป็นตัวแปร:

VAR=`cat file_name`; echo "$VAR"|grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' > file_name

4

เนื่องจากคำถามนี้เป็นผลการค้นหาอันดับต้น ๆ ในเครื่องมือค้นหานี่คือหนึ่งซับอิงจากhttps://serverfault.com/a/547331ที่ใช้ subshell แทนsponge(ซึ่งมักไม่ได้เป็นส่วนหนึ่งของการติดตั้งวานิลลาเช่น OS X) :

echo "$(grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name)" > file_name

กรณีทั่วไปคือ:

echo "$(cat file_name)" > file_name

แก้ไขวิธีแก้ปัญหาข้างต้นมีข้อแม้บางประการ:

  • printf '%s' <string>ควรใช้แทนecho <string>เพื่อให้ไฟล์ที่มี-nไม่ก่อให้เกิดพฤติกรรมที่ไม่ต้องการ
  • แถบการแทนที่คำสั่งต่อท้ายบรรทัดใหม่ ( นี่คือบั๊ก / คุณสมบัติของเชลล์เช่น bash ) ดังนั้นเราควรต่อท้ายอักขระ postfix เช่นxเอาต์พุตและลบออกด้านนอกผ่านการขยายพารามิเตอร์ของตัวแปรชั่วคราวเช่น${v%x}เช่น
  • การใช้ตัวแปรชั่วคราวจะ$vเหยียบค่าของตัวแปรที่มีอยู่$vในสภาพแวดล้อมเชลล์ปัจจุบันดังนั้นเราควรซ้อนนิพจน์ทั้งหมดไว้ในวงเล็บเพื่อรักษาค่าก่อนหน้า
  • ข้อผิดพลาด / คุณลักษณะอื่นของเชลล์เช่น bash คือการแทนที่คำสั่งจะแถบอักขระที่พิมพ์ไม่ได้เช่นnullจากเอาต์พุต ฉันรับการตรวจสอบนี้โดยการโทรและการดูในฐานสิบหกด้วยdd if=/dev/zero bs=1 count=1 >> file_name cat file_name | xxd -pแต่echo $(cat file_name) | xxd -pถูกปล้น. ดังนั้นคำตอบนี้ควรไม่ได้นำไปใช้กับไฟล์ไบนารีหรืออะไรที่ใช้อักขระ unprintable เป็นลินช์ชี้ให้เห็น

วิธีแก้ปัญหาทั่วไป (อัลเบตช้าลงเล็กน้อยหน่วยความจำมากขึ้นและยังคงลอกอักขระที่พิมพ์ไม่ได้) คือ:

(v=$(cat file_name; printf x); printf '%s' ${v%x} > file_name)

ทดสอบจากhttps://askubuntu.com/a/752451 :

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do (v=$(cat file_uniquely_named.txt; printf x); printf '%s' ${v%x} > file_uniquely_named.txt); done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

ควรพิมพ์:

hello
world

ในขณะที่เรียกcat file_uniquely_named.txt > file_uniquely_named.txtในเชลล์ปัจจุบัน:

printf "hello\nworld\n" > file_uniquely_named.txt && for ((i=0; i<1000; i++)); do cat file_uniquely_named.txt > file_uniquely_named.txt; done; cat file_uniquely_named.txt; rm file_uniquely_named.txt

พิมพ์สตริงว่าง

ฉันยังไม่ได้ทดสอบสิ่งนี้กับไฟล์ขนาดใหญ่ (อาจเกิน 2 หรือ 4 GB)

ฉันได้ยืมคำตอบนี้จากฮาร์ท Simhaและคอส


2
แน่นอนว่าจะใช้ไม่ได้กับไฟล์ขนาดใหญ่ นี่ไม่อาจเป็นทางออกที่ดีหรือใช้ได้ผลตลอดเวลา สิ่งที่เกิดขึ้นคือ bash รันคำสั่งก่อนจากนั้นโหลด stdout ของcatechoและใส่เป็นอาร์กิวเมนต์แรก แน่นอนว่าตัวแปรที่ไม่สามารถพิมพ์ได้จะไม่แสดงผลอย่างถูกต้องและทำให้ข้อมูลเสียหาย อย่าพยายามเปลี่ยนเส้นทางไฟล์กลับไปที่ตัวมันเองเพราะมันจะไม่ดี
ลินช์

1

นอกจากนี้ยังมีed(เป็นทางเลือกอื่นsed -i):

# cf. http://wiki.bash-hackers.org/howto/edit-ed
printf '%s\n' H 'g/seg[0-9]\{1,\}\.[0-9]\{1\}/d' wq |  ed -s file_name

1

คุณสามารถทำได้โดยใช้กระบวนการทดแทน

มันเป็นการแฮ็กเล็กน้อยแม้ว่า bash จะเปิดไปป์ทั้งหมดแบบอะซิงโครนัสและเราต้องหลีกsleepเลี่ยงสิ่งนั้นโดยใช้YMMV

ในตัวอย่างของคุณ:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) สร้างไฟล์ชั่วคราวที่รับเอาต์พุตจาก grep
  • sleep 1 ล่าช้าไปหนึ่งวินาทีเพื่อให้ grep มีเวลาในการแยกวิเคราะห์ไฟล์อินพุต
  • ในที่สุดก็cat > file_nameเขียนผลลัพธ์

1

คุณสามารถใช้ slurp กับ POSIX Awk:

!/seg[0-9]\{1,\}\.[0-9]\{1\}/ {
  q = q ? q RS $0 : $0
}
END {
  print q > ARGV[1]
}

ตัวอย่าง


1
อาจจะชี้ให้เห็นว่า "slurp" หมายถึง "อ่านไฟล์ทั้งหมดในหน่วยความจำ" หากคุณมีไฟล์อินพุตขนาดใหญ่คุณอาจต้องการหลีกเลี่ยงสิ่งนั้น
tripleee

1

สิ่งนี้เป็นไปได้อย่างมากคุณต้องแน่ใจว่าเมื่อคุณเขียนผลลัพธ์คุณกำลังเขียนมันลงในไฟล์อื่น สามารถทำได้โดยการลบไฟล์หลังจากเปิด file descriptor ไปแล้ว แต่ก่อนที่จะเขียน:

exec 3<file ; rm file; COMMAND <&3 >file ;  exec 3>&-

หรือทีละบรรทัดเพื่อทำความเข้าใจให้ดีขึ้น:

exec 3<file       # open a file descriptor reading 'file'
rm file           # remove file (but fd3 will still point to the removed file)
COMMAND <&3 >file # run command, with the removed file as input
exec 3>&-         # close the file descriptor

ยังคงเป็นเรื่องที่ต้องเสี่ยงเพราะหาก COMMAND ทำงานไม่ถูกต้องคุณจะสูญเสียเนื้อหาของไฟล์ ซึ่งสามารถบรรเทาได้โดยการกู้คืนไฟล์หาก COMMAND ส่งคืนรหัสออกที่ไม่ใช่ศูนย์:

exec 3<file ; rm file; COMMAND <&3 >file || cat <&3 >file ; exec 3>&-

นอกจากนี้เรายังสามารถกำหนดฟังก์ชันเชลล์เพื่อให้ใช้งานได้ง่ายขึ้น:

# Usage: replace FILE COMMAND
replace() { exec 3<$1 ; rm $1; ${@:2} <&3 >$1 || cat <&3 >$1 ; exec 3>&- }

ตัวอย่าง:

$ echo aaa > test
$ replace test tr a b
$ cat test
bbb

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


0

ลองทำตามนี้

echo -e "AAA\nBBB\nCCC" > testfile

cat testfile
AAA
BBB
CCC

echo "$(grep -v 'AAA' testfile)" > testfile
cat testfile
BBB
CCC

คำอธิบายสั้น ๆ หรือแม้แต่ความคิดเห็นอาจเป็นประโยชน์
รวย

ฉันคิดว่ามันใช้ได้เพราะการคาดคะเนสตริงดำเนินการก่อนตัวดำเนินการเปลี่ยนเส้นทาง แต่ฉันไม่รู้แน่ชัด
ВикторПупкин

0

สิ่งต่อไปนี้จะทำให้สำเร็จในสิ่งเดียวกันspongeโดยไม่ต้องmoreutils:

    shuf --output=file --random-source=/dev/zero 

--random-source=/dev/zeroเทคนิคส่วนหนึ่งshufในการทำสิ่งที่ตนไม่ต้องทำสับใด ๆ เลยดังนั้นมันจะ buffer ป้อนข้อมูลของคุณโดยไม่ต้องเปลี่ยนมัน

อย่างไรก็ตามเป็นเรื่องจริงที่ว่าการใช้ไฟล์ชั่วคราวนั้นดีที่สุดด้วยเหตุผลด้านประสิทธิภาพ ดังนั้นนี่คือฟังก์ชั่นที่ฉันเขียนไว้ซึ่งจะทำเพื่อคุณโดยทั่วไป:

# Pipes a file into a command, and pipes the output of that command
# back into the same file, ensuring that the file is not truncated.
# Parameters:
#    $1: the file.
#    $2: the command. (With $3... being its arguments.)
# See https://stackoverflow.com/a/55655338/773113

function siphon
{
    local tmp=$(mktemp)
    local file="$1"
    shift
    $* < "$file" > "$tmp"
    mv "$tmp" "$file"
}

-2

ฉันมักจะใช้โปรแกรมทีเพื่อทำสิ่งนี้:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name | tee file_name

สร้างและลบ tempfile ด้วยตัวเอง


ขออภัยteeไม่รับประกันว่าจะได้ผล ดูaskubuntu.com/a/752451/335781
studgeek
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.