Apakah ada cara untuk memodifikasi file di tempat?

54

Saya memiliki file yang cukup besar (35Gb), dan saya ingin memfilter file ini di situ (yaitu saya tidak memiliki cukup ruang disk untuk file lain), khususnya saya ingin menangkap dan mengabaikan beberapa pola - apakah ada cara untuk lakukan ini tanpa menggunakan file lain?

Katakanlah saya ingin memfilter semua baris yang berisi foo:misalnya ...

Nim
sumber
3
@Tepanget: Saya pikir dia ingin menulis kembali ke file yang sama.
Faheem Mitha
5
"in situ" adalah frasa Latin yang berarti "in place". Secara harfiah, "dalam posisi".
Faheem Mitha
3
Dalam hal ini, pertanyaannya harus lebih jelas, seperti apakah ada cara untuk memodifikasi file di tempat ?
tshepang
5
@Tepang, "in situ" adalah frasa yang cukup umum digunakan dalam bahasa Inggris untuk menggambarkan hal itu - saya pikir judulnya cukup jelas ... @Gilles, saya pikir, lebih mudah menunggu lebih banyak ruang disk! ;)
Nim
2
@ Nim: Yah, saya pikir in-place lebih umum daripada in situ .
tshepang

Jawaban:

41

Pada tingkat panggilan sistem, ini harus dimungkinkan. Suatu program dapat membuka file target Anda untuk ditulis tanpa memotongnya dan mulai menulis apa yang dibaca dari stdin. Saat membaca EOF, file output dapat dipotong.

Karena Anda memfilter baris dari input, posisi penulisan file output harus selalu kurang dari posisi baca. Ini berarti Anda tidak boleh merusak input Anda dengan output baru.

Namun, menemukan program yang melakukan ini adalah masalahnya. dd(1)memiliki opsi conv=notruncyang tidak memotong file output saat terbuka, tetapi juga tidak memotong pada akhirnya, meninggalkan konten file asli setelah konten grep (dengan perintah seperti grep pattern bigfile | dd of=bigfile conv=notrunc)

Karena sangat sederhana dari perspektif system call, saya menulis sebuah program kecil dan mengujinya pada sistem loopback file penuh kecil (1MiB). Itu melakukan apa yang Anda inginkan, tetapi Anda benar-benar ingin menguji ini dengan beberapa file lain terlebih dahulu. Itu selalu akan berisiko menimpa file.

menimpa.c

/* This code is placed in the public domain by camh */

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h>

int main(int argc, char **argv)
{
        int outfd;
        char buf[1024];
        int nread;
        off_t file_length;

        if (argc != 2) {
                fprintf(stderr, "usage: %s <output_file>\n", argv[0]);
                exit(1);
        }
        if ((outfd = open(argv[1], O_WRONLY)) == -1) {
                perror("Could not open output file");
                exit(2);
        }
        while ((nread = read(0, buf, sizeof(buf))) > 0) {
                if (write(outfd, buf, nread) == -1) {
                        perror("Could not write to output file");
                        exit(4);
                }
        }
        if (nread == -1) {
                perror("Could not read from stdin");
                exit(3);
        }
        if ((file_length = lseek(outfd, 0, SEEK_CUR)) == (off_t)-1) {
                perror("Could not get file position");
                exit(5);
        }
        if (ftruncate(outfd, file_length) == -1) {
                perror("Could not truncate file");
                exit(6);
        }
        close(outfd);
        exit(0);
}

Anda akan menggunakannya sebagai:

grep pattern bigfile | overwrite bigfile

Saya kebanyakan memposting ini untuk dikomentari oleh orang lain sebelum Anda mencobanya. Mungkin orang lain tahu tentang program yang melakukan hal serupa yang lebih teruji.

camh
sumber
Saya ingin melihat apakah saya bisa pergi tanpa menulis sesuatu untuk itu! :) Saya kira ini akan membantu! Terima kasih!
Nim
2
+1 untuk C; tampaknya berfungsi, tetapi saya melihat masalah potensial: file sedang dibaca dari sisi kiri pada saat hak menulis ke file yang sama dan kecuali jika Anda mengoordinasikan dua proses, Anda akan memiliki masalah menimpa berpotensi pada yang sama blok. Mungkin lebih baik untuk integritas file untuk menggunakan ukuran blok yang lebih kecil karena sebagian besar alat inti kemungkinan akan menggunakan 8192. Ini mungkin memperlambat program cukup untuk menghindari konflik (tetapi tidak dapat menjamin). Mungkin membaca bagian yang lebih besar ke dalam memori (tidak semua) dan menulis dalam blok yang lebih kecil. Bisa juga menambahkan nanosleep (2) / usleep (3).
Arcege
4
@Arcege: Menulis tidak dilakukan dalam blok. Jika proses membaca Anda telah membaca 2 byte dan proses penulisan Anda menulis 1 byte, hanya byte pertama yang akan berubah dan proses membaca dapat melanjutkan membaca pada byte 3 dengan konten asli pada saat itu tidak berubah. Karena greptidak akan menghasilkan lebih banyak data daripada yang dibaca, posisi tulis harus selalu di belakang posisi baca. Bahkan jika Anda menulis dengan kecepatan yang sama seperti membaca, itu masih akan baik-baik saja. Coba rot13 dengan ini alih-alih grep, dan kemudian lagi. md5sum sebelum dan sesudah dan Anda akan melihat yang sama.
camh
6
Bagus. Ini mungkin merupakan tambahan berharga untuk Joey Hess's otherutils . Anda dapat menggunakannyadd , tetapi tidak praktis.
Gilles 'SO- berhenti bersikap jahat'
'grep pattern bigfile | menimpa bigfile '- Saya membuat ini berfungsi tanpa kesalahan, tetapi yang saya tidak mengerti adalah - bukankah persyaratan untuk mengganti apa yang ada dalam pola dengan beberapa teks lainnya? jadi bukankah seharusnya sesuatu seperti: 'grep pattern bigfile | menimpa / mengganti-teks / bigfile '
Alexander Mills
20

Anda dapat menggunakan seduntuk mengedit file di tempat (tapi ini memang membuat file sementara antara):

Untuk menghapus semua baris yang mengandung foo:

sed -i '/foo/d' myfile

Untuk menjaga semua baris yang mengandung foo:

sed -i '/foo/!d' myfile
dogbane
sumber
menarik, apakah file temp ini harus memiliki ukuran yang sama dengan aslinya?
Nim
3
Ya, jadi itu mungkin tidak baik.
pjc50
17
Ini bukan yang diminta OP karena membuat file kedua.
Arcege
1
Solusi ini akan gagal pada sistem file read-only, di mana "read-only" berarti bahwa Anda $HOME akan dapat ditulisi, tetapi /tmpakan menjadi read-only (secara default). Misalnya, jika Anda memiliki Ubuntu dan Anda telah mem-boot ke Konsol Pemulihan, biasanya demikian. Juga, operator dokumen di sini <<<tidak akan bekerja di sana juga, karena /tmpharus r / w karena akan menulis file sementara ke sana juga. (lih. pertanyaan ini termasuk. stracekeluaran)
sintaks
ya ini tidak akan bekerja untuk saya juga, semua perintah sed yang saya coba akan mengganti file saat ini dengan file baru (meskipun flag --in-place).
Alexander Mills
19

Saya akan berasumsi bahwa perintah filter Anda adalah apa yang saya sebut filter prefix shrinking , yang memiliki properti yang byte N dalam output tidak pernah ditulis sebelum membaca setidaknya N byte input. grepmemiliki properti ini (asalkan hanya memfilter dan tidak melakukan hal-hal lain seperti menambahkan nomor baris untuk kecocokan). Dengan filter semacam itu, Anda dapat menimpa input saat Anda melanjutkan. Tentu saja, Anda harus yakin untuk tidak membuat kesalahan, karena bagian yang ditimpa pada awal file akan hilang selamanya.

Sebagian besar alat unix hanya memberikan pilihan untuk menambahkan file atau memotongnya, tanpa kemungkinan menimpanya. Satu-satunya pengecualian dalam kotak alat standar adalah dd, yang dapat dikatakan tidak memotong file outputnya. Jadi rencananya adalah menyaring perintah ke dd conv=notrunc. Ini tidak mengubah ukuran file, jadi kami juga mengambil panjang konten baru dan memotong file dengan panjang itu (lagi dengan dd). Perhatikan bahwa tugas ini pada dasarnya tidak kuat - jika terjadi kesalahan, Anda sendiri.

export LC_ALL=C
n=$({ grep -v foo <big_file |
      tee /dev/fd/3 |
      dd of=big_file conv=notrunc; } 3>&1 | wc -c)
dd if=/dev/null of=big_file bs=1 seek=$n

Anda dapat menulis Perl kasar yang setara. Berikut ini adalah implementasi cepat yang tidak mencoba menjadi efisien. Tentu saja, Anda mungkin ingin melakukan pemfilteran awal secara langsung dalam bahasa itu juga.

grep -v foo <big_file | perl -e '
  close STDOUT;
  open STDOUT, "+<", $ARGV[0] or die;
  while (<STDIN>) {print}
  truncate STDOUT, tell STDOUT or die
' big_file
Gilles 'SANGAT berhenti menjadi jahat'
sumber
16

Dengan shell seperti Bourne:

{
  cat < bigfile | grep -v to-exclude
  perl -e 'truncate STDOUT, tell STDOUT'
} 1<> bigfile

Untuk beberapa alasan, tampaknya orang cenderung melupakan operator pengalihan baca + tulis berusia 40 tahun dan standar .

Kami membuka bigfiledi baca modus + write dan (apa yang paling penting di sini) tanpa pemotongan pada stdoutsaat bigfileterbuka (secara terpisah) pada cat's stdin. Setelah grepdihentikan, dan jika telah menghapus beberapa baris, stdoutsekarang menunjuk ke suatu tempat di dalam bigfile, kita perlu menyingkirkan apa yang melampaui titik ini. Oleh karena itu perlperintah yang memotong file ( truncate STDOUT) pada posisi saat ini (seperti yang dikembalikan oleh tell STDOUT).

( catadalah untuk GNU grepyang sebaliknya mengeluh jika stdin dan stdout menunjuk ke file yang sama).


¹ Ya, meski <>sudah ada di kulit Bourne sejak awal di akhir tahun tujuh puluhan, awalnya tidak berdokumen dan tidak diimplementasikan dengan baik . Itu bukan dalam implementasi asli ashdari tahun 1989 dan, sementara itu adalah shoperator pengalihan POSIX (sejak awal 90-an sebagai POSIX shdidasarkan pada ksh88yang selalu memilikinya), itu tidak ditambahkan ke FreeBSD shmisalnya sampai tahun 2000, jadi 15 tahun lama mungkin lebih akurat. Perhatikan juga bahwa deskriptor file default ketika tidak ditentukan ada <>di semua shell, kecuali bahwa di ksh93dalamnya berubah dari 0 menjadi 1 di ksh93t + pada 2010 (melanggar kompatibilitas ke belakang dan kepatuhan POSIX)

Stéphane Chazelas
sumber
2
Bisakah Anda menjelaskannya perl -e 'truncate STDOUT, tell STDOUT'? Ini bekerja untuk saya tanpa memasukkannya. Adakah cara untuk mencapai hal yang sama tanpa menggunakan Perl?
Aaron Blenkush
1
@ AaronBlenkush, lihat edit.
Stéphane Chazelas
1
Sangat brilian - terima kasih. Saya ada di sana, tetapi tidak ingat ini .... Referensi untuk standar "36 tahun" akan menyenangkan, karena tidak disebutkan di en.wikipedia.org/wiki/Bourne_shell . Dan untuk apa itu digunakan? Saya melihat referensi untuk perbaikan bug di SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). yang merupakan salah satu petunjuk.
nealmcb
2
@nealmcb, lihat edit.
Stéphane Chazelas
@ StéphaneChazelas Bagaimana perbandingan solusi Anda dengan jawaban ini ? Tampaknya melakukan hal yang sama tetapi terlihat lebih sederhana.
akhan
9

Meskipun ini adalah pertanyaan lama, menurut saya ini adalah pertanyaan abadi, dan solusi yang lebih umum, lebih jelas tersedia daripada yang telah dikemukakan sejauh ini. Kredit di mana kredit jatuh tempo: Saya tidak yakin saya akan memunculkannya tanpa mempertimbangkan Stéphane Chazelas tentang <>operator pembaruan.

Membuka file untuk pembaruan dalam shell Bourne adalah utilitas terbatas. Shell tidak memberi Anda cara untuk mencari pada file, dan tidak ada cara untuk mengatur panjang baru (jika lebih pendek dari yang lama). Tapi itu mudah diatasi, jadi mudah saya terkejut itu bukan salah satu utilitas standar di /usr/bin.

Ini bekerja:

$ grep -n foo T
8:foo
$ (exec 4<>T; grep foo T >&4 && ftruncate 4) && nl T; 
     1  foo

Seperti halnya ini (ujung topi ke Stéphane):

$ { grep foo T && ftruncate; } 1<>T  && nl T; 
     1  foo

(Saya menggunakan GNU grep. Mungkin ada yang berubah sejak dia menulis jawabannya.)

Kecuali, Anda tidak memiliki / usr / bin / ftruncate . Untuk beberapa lusin baris C, Anda bisa, lihat di bawah. Utilitas funcuncate ini memotong deskriptor file sewenang-wenang hingga panjang sewenang-wenang, default ke output standar dan posisi saat ini.

Perintah di atas (contoh 1)

  • membuka file descriptor 4 pada Tuntuk pembaruan. Sama seperti dengan open (2), membuka file dengan cara ini menempatkan offset saat ini pada 0.
  • grep kemudian memproses secara Tnormal, dan shell mengarahkan ulang outputnya ke Tmelalui deskriptor 4.
  • ftruncate panggilan ftruncate (2) dari deskriptor 4, pengaturan panjang dengan nilai arus offset (persis di mana grep meninggalkannya).

Subshell kemudian keluar, deskriptor penutup 4. Berikut ini ftruncate :

#include <err.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

int
main( int argc, char *argv[] ) {
  off_t i, fd=1, len=0;
  off_t *addrs[2] = { &fd, &len };

  for( i=0; i < argc-1; i++ ) {
    if( sscanf(argv[i+1], "%lu", addrs[i]) < 1 ) {
      err(EXIT_FAILURE, "could not parse %s as number", argv[i+1]);
    }
  }

  if( argc < 3 && (len = lseek(fd, 0, SEEK_CUR)) == -1 ) {
    err(EXIT_FAILURE, "could not ftell fd %d as number", (int)fd);
  }


  if( 0 != ftruncate((int)fd, len) ) {
    err(EXIT_FAILURE, argc > 1? argv[1] : "stdout");
  }

  return EXIT_SUCCESS;
}

NB, ftruncate (2) tidak dapat diport ketika digunakan dengan cara ini. Untuk generalitas absolut, baca byte tertulis terakhir, buka kembali file O_WRONLY, cari, tulis byte, dan tutup.

Mengingat bahwa pertanyaannya adalah 5 tahun, saya akan mengatakan solusi ini tidak jelas. Dibutuhkan dari eksekutif untuk membuka deskriptor baru, dan <>operator, yang keduanya misterius. Saya tidak bisa memikirkan utilitas standar yang memanipulasi inode oleh deskriptor file. (Sintaksnya bisa ftruncate >&4, tapi saya tidak yakin itu perbaikan.) Jauh lebih pendek dari jawaban yang kompeten dan kompeten camh. Itu hanya sedikit lebih jelas daripada Stéphane, IMO, kecuali jika Anda lebih suka Perl daripada saya. Saya harap seseorang menemukannya bermanfaat.

Cara berbeda untuk melakukan hal yang sama adalah versi lseek (2) yang dapat dieksekusi yang melaporkan offset saat ini; outputnya bisa digunakan untuk / usr / bin / truncate , yang disediakan oleh beberapa Linuxi.

James K. Lowden
sumber
5

ed mungkin merupakan pilihan yang tepat untuk mengedit file di tempat:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS
glenn jackman
sumber
Saya suka ide itu, tetapi kecuali edversi yang berbeda berperilaku berbeda ..... ini dari man ed(GNU Ed 1.4) ...If invoked with a file argument, then a copy of file is read into the editor's buffer. Changes are made to this copy and not directly to file itself.
Peter.O
@ fred, jika Anda menyiratkan bahwa menyimpan perubahan tidak akan memengaruhi file bernama, Anda salah. Saya menafsirkan kutipan itu untuk mengatakan bahwa perubahan Anda tidak tercermin sampai Anda menyimpannya. Saya mengakui itu edbukan solusi gool untuk mengedit file 35GB karena file tersebut dibaca ke dalam buffer.
glenn jackman
2
Saya berpikir bahwa itu berarti file lengkap akan dimuat ke buffer .. tapi mungkin hanya bagian yang dibutuhkan dimuat ke buffer .. Saya ingin tahu tentang ed untuk sementara waktu ... Saya pikir itu bisa melakukan editing in-situ ... aku hanya harus mencoba besar file ... Jika berhasil itu adalah solusi yang masuk akal, tapi seperti yang saya tulis, aku mulai berpikir bahwa ini mungkin apa yang terinspirasi sed ( terbebas dari bekerja dengan potongan data besar ... Saya perhatikan bahwa 'ed' sebenarnya dapat menerima input yang dialirkan dari sebuah skrip (diawali dengan !), jadi mungkin ada beberapa trik yang lebih menarik di lengannya
Peter.O
Saya cukup yakin operasi tulis di edmemotong file dan menulis ulang. Jadi ini tidak akan mengubah data pada disk di tempat sesuai keinginan OP. Selain itu, tidak dapat berfungsi jika file terlalu besar untuk dimuat dalam memori.
Nick Matteo
5

Anda dapat menggunakan deskriptor file bash baca / tulis untuk membuka file Anda (untuk menimpanya di tempat), lalu seddan truncate... tetapi tentu saja, jangan pernah izinkan perubahan Anda menjadi lebih besar dari jumlah data yang dibaca sejauh ini .

Ini skripnya (using: bash variable $ BASHPID)

# Create a test file
  echo "going abc"  >junk
  echo "going def" >>junk
  echo "# ORIGINAL file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )
#
# Assign file to fd 3, and open it r/w
  exec 3<> junk  
#
# Choose a unique filename to hold the new file size  and the pid 
# of the semi-asynchrounous process to which 'tee' streams the new file..  
  [[ ! -d "/tmp/$USER" ]] && mkdir "/tmp/$USER" 
  f_pid_size="/tmp/$USER/pid_size.$(date '+%N')" # %N is a GNU extension: nanoseconds
  [[ -f "$f_pid_size" ]] && { echo "ERROR: Work file already exists: '$f_pid_size'" ;exit 1 ; }
#
# run 'sed' output to 'tee' ... 
#  to modify the file in-situ, and to count the bytes  
  <junk sed -e "s/going //" |tee >(echo -n "$BASHPID " >"$f_pid_size" ;wc -c >>"$f_pid_size") >&3
#
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
# The byte-counting process is not a child-process, 
# so 'wait' doesn't work... but wait we must...  
  pid_size=($(cat "$f_pid_size")) ;pid=${pid_size[0]}  
  # $f_pid_size may initially contain only the pid... 
  # get the size when pid termination is assured
  while [[ "$pid" != "" ]] ; do
    if ! kill -0 "$pid" 2>/dev/null; then
       pid=""  # pid has terminated. get the byte count
       pid_size=($(cat "$f_pid_size")) ;size=${pid_size[1]}
    fi
  done
  rm "$f_pid_size"
#@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@
#
  exec 3>&- # close fd 3.
  newsize=$(cat newsize)
  echo "# MODIFIED file (before truncating)";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
 truncate -s $newsize junk
 echo "# NEW (truncated) file";cat junk |tee >( wc=($(wc)); echo "# ${wc[0]} lines, ${wc[2]} bytes" ;echo )  cat junk
#
exit

Ini adalah hasil tes

# ORIGINAL file
going abc
going def
# 2 lines, 20 bytes

# MODIFIED file (before truncating)
abc
def
c
going def
# 4 lines, 20 bytes

# NEW (truncated) file
abc
def
# 2 lines, 8 bytes
Peter.O
sumber
3

Saya akan memetakan file-memori, melakukan semuanya di tempat menggunakan char * pointer ke memori telanjang, kemudian menghapus peta file dan memotongnya.

bmcnett
sumber
3
+1, tetapi hanya karena ketersediaan luas dari 64-bit CPU dan OS memungkinkan untuk melakukan itu dengan file 35 GB sekarang. Mereka yang masih menggunakan sistem 32-bit (saya kira sebagian besar pemirsa situs ini) tidak akan dapat menggunakan solusi ini.
Warren Young
2

Tidak persis in-situ tetapi - ini bisa digunakan dalam keadaan yang serupa.
Jika ruang disk adalah masalah, kompres file terlebih dahulu (karena ini adalah teks, ini akan memberikan pengurangan besar) kemudian gunakan sed (atau grep, atau apa pun) dengan cara biasa di tengah-tengah pipa kompres / kompres.

# Reduce size from ~35Gb to ~6Gb
$ gzip MyFile

# Edit file, creating another ~6Gb file
$ gzip -dc <MyFile.gz | sed -e '/foo/d' | gzip -c >MyEditedFile.gz
Ed Randall
sumber
2
Tapi tentu saja gzip menulis versi terkompresi ke disk sebelum menggantinya dengan versi terkompresi, jadi Anda membutuhkan setidaknya banyak ruang ekstra, tidak seperti opsi lain. Tetapi lebih aman, jika Anda punya ruang (yang saya tidak ....)
nealmcb
Ini adalah solusi cerdas yang dapat lebih dioptimalkan untuk melakukan hanya satu kompresi, bukan dua:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen
0

Demi siapa pun yang menelusuri pertanyaan ini, jawaban yang benar adalah berhenti mencari fitur shell yang tidak jelas yang berisiko merusak file Anda untuk mendapatkan peningkatan kinerja yang dapat diabaikan, dan alih-alih gunakan beberapa variasi dari pola ini:

grep "foo" file > file.new && mv file.new file

Hanya dalam situasi yang sangat tidak umum bahwa ini karena suatu alasan tidak layak, sebaiknya Anda dengan serius mempertimbangkan jawaban lain di halaman ini (walaupun mereka tentu menarik untuk dibaca). Saya akan mengakui bahwa teka-teki OP karena tidak memiliki ruang disk untuk membuat file kedua persis situasi seperti itu. Meskipun demikian, ada opsi lain yang tersedia, misalnya seperti yang disediakan oleh @Ed Randall dan @Basile Starynkevitch.

Todd Owen
sumber
1
Saya mungkin salah paham tetapi tidak ada hubungannya dengan apa yang diminta OP semula. alias pengeditan sebaris bigfile tanpa cukup ruang disk untuk file sementara.
Kiwy
@ Kiwy Ini adalah jawaban yang ditujukan untuk pemirsa lain dari pertanyaan ini (yang sejauh ini sudah ada hampir 15.000). Pertanyaan "Apakah ada cara untuk memodifikasi file di tempat?" memiliki relevansi yang lebih luas daripada kasus penggunaan khusus OP.
Todd Owen
-3

echo -e "$(grep pattern bigfile)" >bigfile

pengguna54620
sumber
3
Ini tidak berfungsi jika file besar dan greppeddata melebihi panjang dari apa yang diperbolehkan commandline. kemudian merusak data
Anthon