Bagaimana saya bisa menggunakan file dalam perintah dan mengarahkan output ke file yang sama tanpa memotongnya?


98

Pada dasarnya saya ingin mengambil teks input dari file, menghapus baris dari file itu, dan mengirim output kembali ke file yang sama. Sesuatu di sepanjang garis ini jika itu membuatnya lebih jelas.

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

Namun, ketika saya melakukan ini, saya berakhir dengan file kosong. Ada pemikiran?


Jawaban:


85

Anda tidak dapat melakukannya karena bash memproses pengalihan terlebih dahulu, lalu menjalankan perintah. Jadi pada saat grep melihat nama_file, itu sudah kosong. Anda dapat menggunakan file sementara.

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

seperti itu, pertimbangkan mktempuntuk menggunakan untuk membuat tmpfile tetapi perhatikan bahwa ini bukan POSIX.


47
Alasan mengapa Anda tidak bisa melakukan itu: bash memproses pengalihan terlebih dahulu, lalu menjalankan perintah. Jadi pada saat grep melihat nama_file, itu sudah kosong.
glenn jackman

1
@ Glennjackman: dengan "pengalihan proses yang Anda maksud bahwa dalam kasus> ia membuka file dan membersihkannya dan dalam kasus >> itu hanya membukanya"?
Razvan

2
ya, tetapi perhatikan dalam situasi ini, >pengalihan akan membuka file dan memotongnya sebelum shell diluncurkan grep.
glenn jackman

1
Lihat jawaban saya jika Anda tidak ingin menggunakan file sementara, tapi tolong jangan suka komentar ini.
Zack Morris

Alih-alih ini, jawaban menggunakan spongeperintah harus diterima.
vlz

98

Gunakan spons untuk tugas semacam ini. Bagian dari moreutils.

Coba perintah ini:

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

4
Terima kasih atas jawabannya. Sebagai tambahan yang mungkin membantu, jika Anda menggunakan homebrew di Mac, Anda dapat menggunakan brew install moreutils.
Anthony Panozzo

2
Atau sudo apt-get install moreutilspada sistem berbasis Debian.
Yunus

3
Sial! Terima kasih telah memperkenalkan saya pada moreutils =) beberapa program bagus di sana!
netigger

terima kasih banyak, moreutils untuk penyelamatannya! spons seperti bos!
aqquadro

3
Perhatian, "spons" merusak, jadi jika Anda memiliki kesalahan dalam perintah Anda, Anda dapat menghapus file masukan Anda (seperti yang saya lakukan saat pertama kali mencoba spons). Pastikan perintah Anda berfungsi, dan / atau file input berada di bawah kontrol versi jika Anda mencoba mengulang untuk membuat perintah berfungsi.
pengguna107172

19

Gunakan sed sebagai gantinya:

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

1
iirc -iadalah ekstensi GNU saja, hanya mencatat.
c00kiemon5ter

4
Di * BSD (dan juga OSX) Anda dapat mengatakan -i ''bahwa ekstensi tidak sepenuhnya wajib, tetapi -iopsi tersebut memerlukan beberapa argumen.
tripleee

16

coba yang sederhana ini

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

File Anda tidak akan kosong kali ini :) dan keluaran Anda juga akan dicetak ke terminal Anda.


1
Saya suka solusi ini! Dan jika Anda tidak ingin itu dicetak di terminal, Anda masih dapat mengarahkan output ke /dev/nullatau tempat serupa.
Frozn

4
Ini juga menghapus konten file di sini. Apakah itu karena perbedaan GNU / BSD? Saya menggunakan macOS ...
ssc

7

Anda tidak dapat menggunakan operator pengalihan ( >atau >>) ke file yang sama, karena memiliki prioritas yang lebih tinggi dan akan membuat / memotong file bahkan sebelum perintah dipanggil. Untuk menghindari itu, Anda harus menggunakan alat yang tepat seperti tee, sponge, sed -iatau alat lain yang dapat menulis hasil ke file (misalnya sort file -o file).

Pada dasarnya mengarahkan input ke file asli yang sama tidak masuk akal dan Anda harus menggunakan editor di tempat yang sesuai untuk itu, misalnya Ex editor (bagian dari Vim):

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

dimana:

  • '+cmd'/ -c- jalankan perintah Ex / Vim
  • g/pattern/d- hapus garis yang cocok dengan pola menggunakan global ( help :g)
  • -s- mode diam ( man ex)
  • -c wq- jalankan :writedan :quitperintah

Anda dapat menggunakan seduntuk mencapai yang sama (seperti yang sudah ditunjukkan dalam jawaban lainnya), namun di tempat ( -i) adalah non-standar ekstensi FreeBSD (dapat bekerja secara berbeda antara Unix / Linux) dan pada dasarnya itu adalah s tream ed itor, bukan file editor . Lihat: Apakah mode Ex memiliki kegunaan praktis?


6

Alternatif satu liner - setel konten file sebagai variabel:

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

4

Karena pertanyaan ini adalah hasil teratas di mesin pencari, berikut adalah satu baris berdasarkan https://serverfault.com/a/547331 yang menggunakan subkulit daripada sponge(yang seringkali bukan bagian dari instalasi vanilla seperti OS X) :

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

Kasus umumnya adalah:

echo "$(cat file_name)" > file_name

Sunting, solusi di atas memiliki beberapa peringatan:

  • printf '%s' <string>harus digunakan sebagai pengganti echo <string>agar file yang berisi -ntidak menyebabkan perilaku yang tidak diinginkan.
  • Strip substitusi perintah mengikuti baris baru ( ini adalah bug / fitur shell seperti bash ) jadi kita harus menambahkan karakter postfix seperti xke output dan menghapusnya di luar melalui perluasan parameter dari variabel sementara seperti ${v%x}.
  • Menggunakan variabel sementara akan $vmenginjak nilai variabel yang ada $vdi lingkungan shell saat ini, jadi kita harus menyarangkan seluruh ekspresi dalam tanda kurung untuk mempertahankan nilai sebelumnya.
  • Bug / fitur lain dari shell seperti bash adalah substitusi perintah menghapus karakter yang tidak dapat dicetak seperti nulldari output. Saya memverifikasi ini dengan memanggil dd if=/dev/zero bs=1 count=1 >> file_namedan melihatnya dalam hex dengan cat file_name | xxd -p. Tapi echo $(cat file_name) | xxd -pdilucuti. Jadi, jawaban ini tidak boleh digunakan pada file biner atau apa pun yang menggunakan karakter yang tidak dapat dicetak, seperti yang ditunjukkan Lynch .

Solusi umum (albiet sedikit lebih lambat, memori lebih intensif dan masih menghilangkan karakter yang tidak dapat dicetak) adalah:

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

Uji dari 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

Harus mencetak:

hello
world

Sedangkan memanggil cat file_uniquely_named.txt > file_uniquely_named.txtdi shell saat ini:

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

Mencetak string kosong.

Saya belum menguji ini pada file besar (mungkin lebih dari 2 atau 4 GB).

Saya telah meminjam jawaban ini dari Hart Simha dan kos .


2
Tentu saja ini tidak akan berfungsi dengan file besar. Ini tidak mungkin menjadi solusi yang baik atau bekerja sepanjang waktu. Apa yang terjadi adalah bash mengeksekusi perintah terlebih dahulu dan kemudian memuat stdout dari catdan meletakkannya sebagai argumen pertama echo. Tentu saja variabel yang tidak dapat dicetak tidak akan menghasilkan keluaran dengan benar dan merusak data. Jangan mencoba untuk mengarahkan kembali file ke dirinya sendiri, itu tidak baik.
Lynch

1

Ada juga ed(sebagai alternatif 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

Anda dapat melakukannya dengan menggunakan proses-substitusi .

Ini sedikit peretasan karena bash membuka semua pipa secara tidak sinkron dan kami harus mengatasinya menggunakan sleepbegitu YMMV.

Dalam contoh Anda:

grep -v 'seg[0-9]\{1,\}\.[0-9]\{1\}' file_name > >(sleep 1 && cat > file_name)
  • >(sleep 1 && cat > file_name) membuat file sementara yang menerima keluaran dari grep
  • sleep 1 penundaan sedetik untuk memberi grep waktu untuk mengurai file input
  • akhirnya cat > file_namemenulis hasilnya

1

Anda dapat menggunakan slurp dengan POSIX Awk:

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

Contoh


1
Mungkin harus ditunjukkan bahwa "slurp" berarti "membaca seluruh file ke dalam memori". Jika Anda memiliki file input yang besar, mungkin Anda ingin menghindarinya.
tripleee

1

Ini sangat mungkin, Anda hanya perlu memastikan bahwa pada saat Anda menulis hasilnya, Anda menulisnya ke file yang berbeda. Ini dapat dilakukan dengan menghapus file setelah membuka deskriptor file, tetapi sebelum menulis padanya:

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

Atau baris demi baris, untuk lebih memahaminya:

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

Ini masih berisiko untuk dilakukan, karena jika COMMAND gagal berjalan dengan benar, Anda akan kehilangan konten file. Itu dapat dikurangi dengan memulihkan file jika COMMAND mengembalikan kode keluar bukan nol:

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

Kita juga bisa mendefinisikan fungsi shell agar lebih mudah digunakan:

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

Contoh:

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

Juga, perhatikan bahwa ini akan menyimpan salinan lengkap dari file asli (hingga deskriptor file ketiga ditutup). Jika Anda menggunakan Linux, dan file yang Anda proses terlalu besar untuk memuat dua kali pada disk, Anda dapat memeriksa skrip ini yang akan menyalurkan file ke perintah yang ditentukan blok-demi-blok sambil membatalkan alokasi yang sudah diproses blok. Seperti biasa, baca peringatan di halaman penggunaan.


0

Coba ini

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

cat testfile
AAA
BBB
CCC

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

Penjelasan singkat atau bahkan komentar mungkin bisa membantu.
Kaya

Saya pikir, ini berfungsi karena ekstrapolasi string dijalankan sebelum operator pengalihan, tetapi saya tidak tahu persis
Виктор Пупкин

0

Yang berikut ini akan mencapai hal yang sama dengan yang spongedilakukannya, tanpa memerlukan moreutils:

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

Bagian --random-source=/dev/zerotrik shufmelakukan sesuatu tanpa melakukan pengacakan sama sekali, jadi itu akan menyangga masukan Anda tanpa mengubahnya.

Namun, memang benar bahwa menggunakan file sementara adalah yang terbaik, karena alasan kinerja. Jadi, berikut adalah fungsi yang telah saya tulis yang akan melakukannya untuk Anda secara umum:

# 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"
}

Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.