Yerinde bir dosyayı değiştirmenin bir yolu var mı?


54

Oldukça büyük bir dosyaya (35Gb) sahibim ve bu dosyayı yerinde filtrelemek istiyorum (yani başka bir dosya için yeterli disk alanım yok), özellikle bazı kalıpları alıp yok saymak istiyorum - bir yol var bunu başka bir dosya kullanmadan mı yapabilirsin?

Diyelim ki içerdiği tüm satırları filtrelemek istiyorum foo:...


3
@Tshepang: Aynı dosyaya tekrar yazmak istiyor.
Faheem Mitha

5
"yerinde", "yerinde" anlamına gelen Latince bir ifadedir. Kelimenin tam anlamıyla, "pozisyonda".
Faheem Mitha

3
Bu durumda, soru net olmalı , bunun yerine bir dosyayı değiştirmenin bir yolu var mı?
tshepang

5
@Tshepang, "in situ", İngilizce'de tam olarak bunu tarif etmek için kullanılan oldukça yaygın bir cümledir - Başlığın oldukça açıklayıcı olduğunu düşündüm ... @Gilles, daha fazla disk alanı için beklemenin daha kolay olduğunu düşündüm! ;)
Nim

2
@Nim: Evet, bence yerinde daha yaygındır yerinde .
tshepang

Yanıtlar:


41

Sistem çağrısı seviyesinde bu mümkün olmalıdır. Bir program, hedef dosyanızı, kısaltmadan yazmak için açabilir ve stdin'den okuduğunu yazmaya başlar. EOF okunurken, çıktı dosyası kesilebilir.

Girdiden satırları filtrelediğiniz için, çıktı dosyası yazma konumu her zaman okuma konumundan düşük olmalıdır. Bu, girişinizi yeni çıktıyla bozmamanız gerektiği anlamına gelir.

Ancak, bunu yapan bir program bulmak sorun. çıktı dosyasını açıkken kesmeyen bir dd(1)seçeneğe sahiptir conv=notrunc, ancak sonunda da kısalmaz, orijinal dosya içeriğini grep içeriğinden sonra bırakır (gibi bir komutla grep pattern bigfile | dd of=bigfile conv=notrunc)

Sistem çağrısı açısından çok basit olduğu için küçük bir program yazdım ve küçük (1MiB) bir tam geridöngü dosya sisteminde test ettim. İstediğini yaptı, ama önce bunu başka dosyalarla test etmek istiyorsun. Bir dosyanın üzerine yazmak her zaman riskli olacaktır.

overwrite.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);
}

Bunu şu şekilde kullanırsınız:

grep pattern bigfile | overwrite bigfile

Bunu denemeden önce başkalarına yorum yazması için gönderiyorum. Belki başka birisi daha sınanmış benzer bir şey yapan bir program biliyordur.


Bunun için bir şeyler yazmadan kaçabilir miyim görmek istedim! :) Bu hile yapacak sanırım! Teşekkürler!
Nim

2
C için + 1; işe yarayacak gibi görünüyor, ancak potansiyel bir sorun görüyorum: sağ aynı dosyaya yazarken dosya sol taraftan okunuyor ve iki işlemi koordine etmediğiniz sürece, aynı potansiyel problemlerin üzerine yazmak zorunda kalacaksınız. blokları. Çekirdek araçların çoğu 8192 kullanacağından dosya bütünlüğü için daha küçük blok boyutu kullanmak daha iyi olabilir. Bu, çakışmaları önlemek için programı yavaşlatabilir (ancak garanti edemez). Belki daha büyük bölümleri belleğe okuyabilir (hepsini değil) ve daha küçük bloklar halinde yazabilirsiniz. Ayrıca bir nanosep (2) / usleep (3) ekleyebilir.
Arcege

4
@Arcege: Yazma bloklar halinde yapılmaz. Okuma işleminiz 2 bayt okuduysa ve yazma işleminiz 1 bayt yazarsa, yalnızca ilk bayt değişecektir ve okuma işlemi bu noktada orijinal içerikler değişmeden bayt 3'te okumaya devam edebilir. Yana grepokur çıkışını daha fazla veri daha yazma pozisyonu her zaman okuma pozisyonuna arkasında olmalıdır olacak. Okuma ile aynı oranda yazsanız bile, yine de iyi olacak. Rot13'ü grep yerine bununla tekrar deneyin. md5sum öncesi ve sonrası ve aynı olduğunu göreceksiniz.
camh

6
Güzel. Bu, Joey Hess'in daha fazla içeriğine değerli bir katkı olabilir . Sen kullanabilirsinizdd , ancak bu hantal bu.
Gilles 'SO- kötülük olmayı'

'grep pattern bigfile | 'bigfile' üzerine yaz - 'Bu çalışmayı hatasız yaptım, ama anlamadığım şey şu - desende olanı başka bir metinle değiştirmek gerekmiyor mu? öyleyse şöyle bir şey olmamalı: 'grep pattern bigfile | Üzerine Yaz / Değiştir metin / bigfile '
Alexander Mills

20

sedDosyaları yerinde düzenlemek için kullanabilirsiniz (ancak bu, geçici bir geçici dosya oluşturur):

Aşağıdaki tüm satırları kaldırmak için foo:

sed -i '/foo/d' myfile

Tüm satırları içeren tutmak için foo:

sed -i '/foo/!d' myfile

ilginç, bu geçici dosyanın orijinaliyle aynı boyutta olması gerekir mi?
Nim

3
Evet, bu muhtemelen hiç iyi değil.
pjc50

17
Bu OP'nin istediği şey değil çünkü ikinci bir dosya oluşturuyor.
Arcege

1
"Salt okunur" sizin demektir nereye Bu çözüm salt okunur dosya sistemi üzerinde başarısız olur $HOME olur yazılabilir olabilir, ama /tmpolacak salt okunur (varsayılan olarak). Örneğin, Ubuntu'nuz varsa ve Kurtarma Konsolu'na önyükleme yaptıysanız, genellikle durum budur. Ayrıca, burada-belge operatörü <<<gerek duyduğu, ya orada çalışmaz /tmpolmaya r / w o da orada içine geçici bir dosya yazacağız çünkü. (cf. bu soru a strace'd çıktısı dahil )
sözdizimi

evet bu benim için de işe yaramayacak, denediğim tüm sed komutları (--in-flag işaretine rağmen) mevcut dosyayı yeni bir dosya ile değiştirecektir.
Alexander Mills,

19

Filtre komutunuzun, çıktıdaki en az N bayt okumayı okumadan önce asla N yazılmayan bir özelliği olan bir önek küçültme filtresi dediğim şey olduğunu kabul edeceğim . grepBu özelliğe sahiptir (yalnızca filtrelemek ve eşleşmeler için satır numaraları eklemek gibi başka şeyler yapmamak kaydıyla). Böyle bir filtreyle, ilerledikçe girişin üzerine yazabilirsiniz. Elbette, herhangi bir hata yapmadığınızdan emin olmanız gerekir, çünkü dosyanın başında yazılan kısım sonsuza dek kaybedilir.

Çoğu unix aracı yalnızca üzerine yazma seçeneği olmadan bir dosyaya ekleme veya kısaltma seçeneği sunar. Standart araç kutusundaki istisna, ddbunun çıktı dosyasını kesmemesi gerektiği söylenebilir. Yani plan emri filtrelemek dd conv=notrunc. Bu, dosyanın boyutunu değiştirmez, bu nedenle yeni içeriğin uzunluğunu da alır ve dosyayı bu uzunluğa kadar keseriz (yine dd). Bu görevin doğal olarak sağlam olmadığını unutmayın - bir hata oluşursa, kendi başınızasınız.

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

Kabaca eşdeğer Perl yazabilirsiniz. İşte verimli olmaya çalışmayan hızlı bir uygulama. Elbette, ilk filtrelemenizi doğrudan o dilde yapmak isteyebilirsiniz.

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

16

Herhangi bir Bourne benzeri kabukla:

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

Bazı nedenlerden dolayı, insanlar bu 40 yaşındaki¹ ve standart okuma + yazma yönlendirme operatörünü unutabilirler .

Biz açmak bigfileokuma + yazma modunda ve kesintilerin ne olmadan (en çok ne burada önemli olan) stdoutsüre bigfileaçık (ayrı) 'dir cat's stdin. grepSona erdikten sonra ve eğer bazı çizgileri kaldırdıysa, stdoutşimdi içindeki bir noktaya işaret ederse, bigfilebu noktanın ötesinde olanlardan kurtulmamız gerekir. Bu nedenle, perldosyayı ( truncate STDOUT) geçerli konumda (döndürdüğü gibi) kesen komut tell STDOUT.

( aksi halde stdin ve stdout aynı dosyayı işaret ederse şikayet eden catGNU içindir grep).


¹ <>Yetmişli yılların sonlarında baştan beri Bourne kabuğundaki haldeyken , başlangıçta belgelenmemiş ve uygun şekilde uygulanmamıştır . ash1989'dan beri orijinal uygulamasında değildi ve bir POSIX shyönlendirme operatörü olmasına rağmen (POSIX'in 90'lı yılların başlarında her zaman sahip olduğu temelde sholduğu ksh88için), FreeBSD'ye sh2000 yılına kadar eklenmedi , bu yüzden 15 yıl eski muhtemelen daha doğrudur. Ayrıca, belirtilmediğinde varsayılan dosya tanımlayıcısının <>tüm kabuklarda olduğunu unutmayın, ancak ksh932010'da ksh93t + 'da 0'dan 1'e değiştirildi (geriye dönük uyumluluk ve POSIX uyumluluğu)


2
Açıklayabilir misin perl -e 'truncate STDOUT, tell STDOUT'? Bu dahil etmeden benim için çalışıyor. Perl kullanmadan aynı şeyi başarmak için herhangi bir yolu?
Aaron Blenkush

1
@AaronBlenkush, düzenlemeye bakın.
Stéphane Chazelas

1
Kesinlikle brilliant - teşekkür ederim. O zamanlar oradaydım ama bunu hatırlamıyorum .... "36 yaşında" standarda yapılan bir referans eğlenceli olurdu, çünkü en.wikipedia.org/wiki/Bourne_shell adresinde bahsedilmedi . Ve ne için kullanıldı? SunOS 5.6'daki bir hata düzeltmesine bir referans görüyorum: redirection "<>" fixed and documented (used in /etc/inittab f.i.). bu bir ipucu.
nealmcb

2
@nealmcb, düzenlemeye bakın.
Stéphane Chazelas

@ StéphaneChazelas Çözümünüz bu cevapla nasıl karşılaştırılır ? Görünüşe göre aynı şeyi yapar ama daha basit görünüyor.
akhan

9

Bu eski bir soru olmasına rağmen, bana çok uzun bir soru gibi görünüyor ve şu ana kadar önerilenden daha genel, daha net bir çözüm var. Kredilerin verildiği yerdeki kredi: Stéphane Chazelas'ın <>güncelleme operatöründen bahsettiğini düşünmeden elde edeceğime emin değilim .

Bir Bourne kabuğundaki güncelleme için dosyayı açmak sınırlı bir yardımcı programdır. Kabuk, bir dosya için arama yapmanıza ve yeni uzunluğunu ayarlamanıza imkan vermez (eskisinden daha kısaysa). Ama bu kolayca çözüldü, bu yüzden kolayca içinde bulunan standart uygulamalar arasında olmadığına şaşırdım /usr/bin.

Bu çalışıyor:

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

Bunun gibi (Stéphane'ye şapka bahşiş verir):

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

(GNU grep kullanıyorum. Belki de cevabını yazdığından beri bir şeyler değişmiştir.)

Ancak, / usr / bin / ftruncate öğeniz yok . Birkaç düzine C çizgisi için aşağıya bakınız. Bu ftruncate yardımcı programı, isteğe bağlı bir dosya tanımlayıcısını, isteğe bağlı bir uzunluğa keser, varsayılan olarak standart çıktıya ve geçerli konuma göre ayarlar.

Yukarıdaki komut (1. örnek)

  • Tgüncelleme için dosya tanımlayıcı 4'ü açar . Open (2) 'de olduğu gibi, dosyayı bu şekilde açmak geçerli ofseti 0'da konumlandırır.
  • grep daha sonra Tnormal bir şekilde işler ve kabuk çıktısını Ttanımlayıcı 4 üzerinden yönlendirir .
  • ftruncate , tanımlayıcı 4'teki ftruncate (2) işlevini çağırır, uzunluğu geçerli ofset değerine ayarlar (tam olarak grep bıraktığı yer).

Alt kabuk daha sonra tanımlayıcı 4'ü kapatarak çıkar. İşte 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;
}

Not: Bu şekilde kullanıldığında ftruncate (2) taşınabilir değildir. Mutlak genellik için son yazılan baytı okuyun, dosyayı O_WRONLY içinde yeniden açın, arayın, bayt yazın ve kapatın.

Sorunun 5 yaşında olduğu göz önüne alındığında, bu çözümün açık olmadığını söyleyeceğim. Her ikisi de arcane olan yeni bir tanımlayıcı ve operatör açmak için exec'ten faydalanır <>. Dosya tanıtıcısına göre bir inode'u işleyen standart bir yardımcı program düşünemiyorum. (Sözdizimi olabilir ftruncate >&4, ancak bir iyileşme olduğundan emin değilim.) Bu, Camh'ın yetkin, keşif yanıtından oldukça kısa. Perl'i benden daha fazla sevmiyorsan, IMP, Stéphane'den biraz daha net. Umarım birileri onu yararlı bulur.

Aynı şeyi yapmanın farklı bir yolu, mevcut ofseti bildiren lseek (2) 'nin çalıştırılabilir bir sürümü olacaktır; çıktı , bazı Linuxi'lerin sağladığı / usr / bin / truncate için kullanılabilir.


5

ed muhtemelen yerinde bir dosyayı düzenlemek için doğru seçimdir:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS

Fikirden hoşlanıyorum, fakat farklı edsürümler farklı davranmıyorsa ..... bu 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, değişiklikleri kaydetmenin adlandırılmış dosyayı etkilemeyeceğini ima ediyorsan, yanılıyorsun. Bu teklifi, değişikliklerinizi siz kaydedene kadar yansıtılmadığını söylemek için yorumluyorum . edDosya bir arabellek okunduğundan 35GB dosyaları düzenlemek için bir gool çözümü olmadığını kabul ediyorum .
glenn jackman

2
Tam dosyanın ara belleğe yükleneceği anlamına geliyordu .. ama belki sadece aradığı bölümleri ara belleğe yüklüyordu .. bir süredir merak ettim ... olabilir in-situ düzenleme yapmak ... sadece bir denemek gerekecek büyük o makul bir çözümdür çalışırsa ... dosyayı ama yazarken, bu ilham neyi olabileceğini düşünmeye başladım sed ( büyük veri parçalarıyla çalışmaktan kurtulmuş ... 'ed' !
öğesinin

Yazma işleminin eddosyayı kısalttığından ve yeniden yazdığından eminim . Böylece bu OP'nin istediği şekilde diskteki verileri değiştirmez. Ayrıca, dosya belleğe yüklenemeyecek kadar büyükse çalışamaz.
Nick Matteo,

5

Dosyanızı açmak için (yerinde yerinde yazmak için) bir bash okuma / yazma dosya tanımlayıcısı kullanabilirsiniz, daha sonra sedve truncate... ama elbette, değişikliklerin şimdiye kadar okunan veri miktarından daha büyük olmasına izin vermeyin .

İşte script (kullanır: bash değişken $ 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

İşte test çıktısı

# 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

3

Dosyayı hafızaya eşler, çıplak hafızayı göstermek için char * işaretçileri kullanarak her şeyi yerinde yapar, sonra dosyayı çıkarır ve keserdim.


3
+1, ancak yalnızca 64-bit CPU ve OS'lerin yaygın olarak bulunabilmesi, bunu şimdi 35 GB bir dosya ile yapmayı mümkün kılıyor. Hala 32-bit sistemler üzerinde olanlar (bu sitenin kitlesinin bile büyük çoğunluğu, sanırım) bu çözümü kullanamayacaklar.
Warren Young,

2

Tam olarak yerinde değil, ancak bu benzer durumlarda kullanılabilir.
Disk alanı bir sorunsa, önce dosyayı sıkıştırın (metin olduğundan bu büyük bir azalma sağlar), sonra bir sıkıştırılmamış / sıkıştırılmış boru hattının ortasında her zamanki gibi sed (veya grep veya her neyse) kullanın.

# 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

2
Ancak kesinlikle gzip, sıkıştırılmış sürümü, sıkıştırılmış sürümle değiştirmeden önce diske yazıyor; bu nedenle, diğer seçeneklerin aksine, en azından bu kadar fazla alana ihtiyacınız var. Ama daha güvenli, eğer yeriniz varsa (ki ben ....)
nealmcb

Bu, iki yerine yalnızca bir sıkıştırma yapmak için daha da optimize edilebilecek akıllıca bir çözümdür:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen

0

Bu soruyu google kullananların yararına, doğru cevap, ihmal edilebilir performans kazancı için dosyanızı bozma riski taşıyan belirsiz kabuk özellikleri aramayı bırakmak ve bunun yerine bu kalıbın bir çeşitlemesini kullanmaktır:

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

Sadece bu oldukça nadir bir durumda , bunun bir nedenden ötürü mümkün olmadığı durumlarda, bu sayfadaki diğer cevaplardan herhangi birini ciddi olarak düşünmelisiniz (kesinlikle okunması ilginç olsa da). OP'nin ikinci bir dosya oluşturmak için disk alanına sahip olmadığının bilincinin tam da böyle bir durum olduğunu kabul edeceğim. Buna rağmen, örneğin @Ed Randall ve @Basile Starynkevitch tarafından sağlananlar gibi başka seçenekler de vardır.


1
Yanlış anlayabilirim ama OP orjinalinin istediği ile ilgisi yok. aka geçici dosya için yeterli boş alan olmadan bigfile inline düzenleme.
Kiwy

@Kiwy Bu sorunun diğer izleyicilerine yönelik (şimdiye kadar yaklaşık 15.000 kişi olan) bir cevaptır. "Yerinde bir dosyayı değiştirmenin bir yolu var mı?" OP'nin spesifik kullanım durumundan daha geniş bir alaka düzeyi vardır.
Todd Owen

-3

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


3
Dosya büyükse ve greppedveriler komut satırının izin verdiği süreyi aşıyorsa bu işe yaramaz . daha sonra verileri bozar
Anthon
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.