Czy istnieje sposób na zmodyfikowanie pliku w miejscu?

54

Mam dość duży plik (35 GB) i chciałbym filtrować ten plik in situ (tzn. Nie mam wystarczającej ilości miejsca na inny plik), a konkretnie chcę grepować i ignorować niektóre wzorce - czy istnieje sposób na zrobić to bez użycia innego pliku?

Powiedzmy, że chcę odfiltrować wszystkie wiersze zawierające foo:na przykład ...

Nim
źródło
3
@Tshepang: Myślę, że chce wrócić do tego samego pliku.
Faheem Mitha,
5
„in situ” to łacińska fraza oznaczająca „na miejscu”. Dosłownie „w pozycji”.
Faheem Mitha
3
W takim przypadku pytanie powinno być jaśniejsze, coś w rodzaju, czy istnieje sposób na zmodyfikowanie pliku w miejscu ?
tshepang 11.0411
5
@ Tshepang, „in situ” jest dość powszechnym wyrażeniem używanym w języku angielskim do dokładnego opisania tego - myślałem, że tytuł jest dość oczywisty ... @Gilles, pomyślałem, że łatwiej jest czekać na więcej miejsca na dysku! ;)
Nim,
2
@Nim: Myślę, że w miejscu jest bardziej powszechne niż na miejscu .
tshepang

Odpowiedzi:

41

Na poziomie wywołania systemowego powinno to być możliwe. Program może otworzyć plik docelowy do pisania bez obcinania go i rozpocząć zapisywanie tego, co czyta ze standardowego wejścia. Podczas odczytu EOF plik wyjściowy można obciąć.

Ponieważ filtrujesz linie z wejścia, pozycja zapisu pliku wyjściowego powinna zawsze być mniejsza niż pozycja odczytu. Oznacza to, że nie powinieneś uszkadzać danych wejściowych nowym wyjściem.

Problemem jest jednak znalezienie programu, który to robi. dd(1)ma opcję conv=notrunc, która nie obcina pliku wyjściowego przy otwartym, ale również nie obcina na końcu, pozostawiając oryginalną zawartość pliku po zawartości grep (przy pomocy polecenia podobnego grep pattern bigfile | dd of=bigfile conv=notrunc)

Ponieważ jest to bardzo proste z perspektywy wywołań systemowych, napisałem mały program i przetestowałem go na małym (1MiB) systemie plików z pełną pętlą zwrotną. Zrobił to, co chciałeś, ale naprawdę chcesz najpierw przetestować to z innymi plikami. Zastąpienie pliku zawsze będzie ryzykowne.

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

Użyłbyś go jako:

grep pattern bigfile | overwrite bigfile

Przeważnie publikuję to, aby inni mogli komentować, zanim spróbujesz. Być może ktoś inny wie o programie, który robi coś podobnego, co jest bardziej testowane.

camh
źródło
Chciałem zobaczyć, czy uda mi się uciec bez napisania czegoś na ten temat! :) Myślę, że to załatwi sprawę! Dzięki!
Nim,
2
+1 dla C; wydaje się działać, ale widzę potencjalny problem: plik jest odczytywany z lewej strony, podczas gdy prawa zapisuje do tego samego pliku i jeśli nie skoordynujesz tych dwóch procesów, możesz mieć problemy z nadpisaniem tego samego Bloki. Może być lepiej, aby integralność pliku używała mniejszego rozmiaru bloku, ponieważ większość podstawowych narzędzi prawdopodobnie użyje 8192. Może to spowolnić program na tyle, aby uniknąć konfliktów (ale nie można tego zagwarantować). Może wczytaj większe części do pamięci (nie wszystkie) i pisz mniejszymi blokami. Można również dodać nanosleep (2) / usleep (3).
Arcege
4
@Arcege: Pisanie nie odbywa się w blokach. Jeśli proces odczytu ma odczytane 2 bajty, a proces zapisu zapisuje 1 bajt, zmieni się tylko pierwszy bajt, a proces odczytu może kontynuować czytanie w bajcie 3 z oryginalną zawartością w tym momencie niezmienionym. Ponieważ grepnie wyśle ​​więcej danych niż odczytuje, pozycja zapisu powinna zawsze znajdować się za pozycją odczytu. Nawet jeśli piszesz w tym samym tempie co czytanie, nadal będzie dobrze. Spróbuj rot13 z tym zamiast grep, a potem jeszcze raz. md5sum przed i po, a zobaczysz to samo.
camh
6
Miły. Może to być cenny dodatek do moreutils Joeya Hessa . Możesz użyćdd , ale jest to uciążliwe.
Gilles „SO- przestańcie być źli”
„bigpile wzór grep | overwrite bigfile ”- mam to działające bez błędów, ale nie rozumiem - czy nie jest wymagane zastąpienie tego, co jest we wzorcu, innym tekstem? więc nie powinno to być coś w stylu: „bigpile grep pattern | overwrite / replace-text / bigfile '
Alexander Mills,
20

Możesz użyć seddo edycji plików w miejscu (ale to tworzy pośredni plik tymczasowy):

Aby usunąć wszystkie wiersze zawierające foo:

sed -i '/foo/d' myfile

Aby zachować wszystkie wiersze zawierające foo:

sed -i '/foo/!d' myfile
dogbane
źródło
ciekawe, czy ten plik tymczasowy musi mieć taki sam rozmiar jak oryginał?
Nim,
3
Tak, więc to chyba nie jest dobre.
pjc50 11.04.11
17
Nie o to prosi OP, ponieważ tworzy drugi plik.
Arcege
1
To rozwiązanie zawiedzie w systemie plików tylko do odczytu, gdzie „tylko do odczytu” oznacza, że $HOME będziesz mieć możliwość zapisu, ale /tmpbędzie tylko do odczytu (domyślnie). Na przykład, jeśli masz Ubuntu i uruchomiłeś konsolę odzyskiwania, zwykle tak jest. Również operator dokumentu tutaj też <<<nie będzie tam działał, ponieważ wymaga /tmpon r / w, ponieważ zapisze tam również plik tymczasowy. (por. to pytanie wraz z stracewyjściem „d”)
składnia błąd
tak, to mi też nie zadziała, wszystkie polecenia sed, które próbowałem, zastąpią bieżący plik nowym plikiem (pomimo flagi --in-place).
Alexander Mills,
19

Zakładam, że twoje polecenie filter jest tym, co nazywam filtrem zmniejszającym przedrostek , który ma właściwość polegającą na tym, że bajt N w danych wyjściowych nigdy nie jest zapisywany przed odczytaniem co najmniej N bajtów danych wejściowych. grepma tę właściwość (o ile tylko filtruje i nie wykonuje innych czynności, takich jak dodawanie numerów wierszy dla dopasowań). Za pomocą takiego filtra możesz nadpisywać wprowadzane dane. Oczywiście musisz być pewien, że nie popełnisz błędu, ponieważ nadpisana część na początku pliku zostanie utracona na zawsze.

Większość narzędzi uniksowych daje jedynie opcję dołączenia do pliku lub obcięcia go, bez możliwości zastąpienia go. Jedynym wyjątkiem w standardowym zestawie narzędzi jest to dd, że można powiedzieć, aby nie obcinał swojego pliku wyjściowego. Zatem plan polega na przefiltrowaniu polecenia dd conv=notrunc. Nie zmienia to rozmiaru pliku, więc pobieramy również długość nowej zawartości i skracamy plik do tej długości (ponownie za pomocą dd). Pamiętaj, że to zadanie z natury nie jest niezawodne - jeśli wystąpi błąd, jesteś sam.

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

Możesz napisać szorstki odpowiednik Perla. Oto szybkie wdrożenie, które nie próbuje być skuteczne. Oczywiście możesz również przeprowadzić wstępne filtrowanie bezpośrednio w tym języku.

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 „SO- przestań być zły”
źródło
16

Z każdą powłoką podobną do Bourne'a:

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

Z jakiegoś powodu wydaje się, że ludzie zapominają o tym 40-latku¹ i standardowym operatorze przekierowania odczytu i zapisu.

Otwieramy bigfilew trybie odczytu i zapisu + (co najważniejsze tutaj) bez obcięcia na stdoutczas bigfilejest otwarty (osobno) na cat„s stdin. Po grepzakończeniu i jeśli usunął niektóre linie, stdoutteraz wskazuje gdzieś wewnątrz bigfile, musimy pozbyć się tego, co jest poza tym punktem. Stąd perlpolecenie, które obcina plik ( truncate STDOUT) w bieżącej pozycji (zwróconej przez tell STDOUT).

( catdotyczy GNU, grepktóry w przeciwnym razie narzeka, jeśli stdin i stdout wskazują ten sam plik).


¹ Cóż, chociaż <>znajdował się w powłoce Bourne'a od początku lat siedemdziesiątych, początkowo był nieudokumentowany i nie został właściwie zaimplementowany . Nie było to w oryginalnej implementacji ashz 1989 roku i chociaż jest to shoperator przekierowywania POSIX (od wczesnych lat 90., ponieważ shoparty jest na POSIX, ksh88który zawsze go miał), nie został dodany do FreeBSD shna przykład do 2000 roku, więc przenośnie 15 lat stary jest prawdopodobnie dokładniejszy. Zauważ też, że domyślny deskryptor pliku, gdy nie jest określony, znajduje się <>we wszystkich powłokach, z wyjątkiem tego, że w 2010 r ksh93. Zmienił się z 0 na 1 w ksh93t + (łamanie kompatybilności wstecznej i zgodności z POSIX)

Stéphane Chazelas
źródło
2
Czy możesz to wyjaśnić perl -e 'truncate STDOUT, tell STDOUT'? Działa dla mnie bez uwzględnienia tego. Jest jakiś sposób na osiągnięcie tego samego bez Perla?
Aaron Blenkush
1
@AaronBlenkush, patrz edycja.
Stéphane Chazelas
1
Absolutnie genialne - dziękuję. Byłem wtedy, ale nie pamiętam tego ... Odniesienie do standardu „36 lat” byłoby fajne, ponieważ nie jest wspomniane na en.wikipedia.org/wiki/Bourne_shell . I do czego był używany? Widzę odniesienie do poprawki błędu w SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). to jedna wskazówka.
nealmcb
2
@nealmcb, patrz edycja.
Stéphane Chazelas,
@ StéphaneChazelas Jak Twoje rozwiązanie wypada w porównaniu z tą odpowiedzią ? Najwyraźniej robi to samo, ale wygląda na prostsze.
akhan
9

Chociaż jest to stare pytanie, wydaje mi się, że jest to pytanie odwieczne i dostępne jest bardziej ogólne, jaśniejsze rozwiązanie, niż dotychczas sugerowano. Kredyt tam, gdzie należny jest kredyt: nie jestem pewien, czy wymyśliłbym to bez wzmianki o Stéphane Chazelas o <>operatorze aktualizacji.

Otwarcie pliku do aktualizacji w powłoce Bourne'a ma ograniczone zastosowanie. Powłoka nie umożliwia wyszukiwania pliku ani ustawiania jego nowej długości (jeśli jest krótsza niż stara). Ale łatwo to naprawić, więc jestem zaskoczony, że nie jest to standardowe narzędzie /usr/bin.

To działa:

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

Podobnie jak to (czapka dla Stéphane'a):

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

(Używam GNU grep. Być może coś się zmieniło, odkąd napisał swoją odpowiedź).

Tyle że nie masz / usr / bin / ftruncate . Aby zobaczyć kilkadziesiąt linii C, możesz zobaczyć poniżej. To narzędzie ftruncate obcina dowolny deskryptor pliku do dowolnej długości, domyślnie ustawiając standardowe wyjście i bieżącą pozycję.

Powyższe polecenie (pierwszy przykład)

  • otwiera deskryptor pliku 4 w Tcelu aktualizacji. Podobnie jak w przypadku open (2), otwarcie pliku w ten sposób ustawia bieżące przesunięcie na 0.
  • grep następnie przetwarza Tnormalnie, a powłoka przekierowuje swoje wyjście na Tdeskryptor 4.
  • ftruncate wywołuje ftruncate (2) na deskryptorze 4, ustawiając długość na wartość bieżącego przesunięcia (dokładnie tam, gdzie zostawił go grep ).

Następnie podpowłoka kończy działanie, zamykając deskryptor 4. Oto 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;
}

Uwaga: ftruncate (2) nie może być importowany, jeśli jest używany w ten sposób. Aby uzyskać absolutną ogólność, przeczytaj ostatni zapisany bajt, ponownie otwórz plik O_WRONLY, wyszukaj, zapisz bajt i zamknij.

Biorąc pod uwagę, że pytanie ma 5 lat, powiem, że to rozwiązanie jest nieoczywiste. Korzysta z exec, aby otworzyć nowy deskryptor, a <>operator, oba są tajemne. Nie mogę wymyślić standardowego narzędzia, które manipuluje i-węzłem za pomocą deskryptora pliku. (Składnia może być ftruncate >&4, ale nie jestem pewien, czy poprawa.) Jest znacznie krótsza niż kompetentna, eksploracyjna odpowiedź camh. Jest tylko trochę jaśniejszy niż Stéphane, IMO, chyba że bardziej lubisz Perla niż ja. Mam nadzieję, że ktoś uzna to za przydatne.

Innym sposobem na zrobienie tego samego byłaby wykonywalna wersja lseek (2), która zgłasza bieżące przesunięcie; wyjście może być wykorzystane do / usr / bin / truncate , które zapewniają niektóre Linuxi.

James K. Lowden
źródło
5

ed jest prawdopodobnie właściwym wyborem do edycji pliku w miejscu:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS
Glenn Jackman
źródło
Podoba mi się ten pomysł, ale chyba że różne edwersje zachowują się inaczej ..... to pochodzi z 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, jeśli sugerujesz, że zapisanie zmian nie wpłynie na nazwany plik, jesteś niepoprawny. Interpretuję ten cytat, aby powiedzieć, że twoje zmiany nie zostaną odzwierciedlone, dopóki ich nie zapiszesz. Przyznaję, że ednie jest to dobre rozwiązanie do edycji plików 35 GB, ponieważ plik jest wczytywany do bufora.
glenn jackman
2
Myślałem, że oznacza to, że pełny plik zostanie załadowany do bufora .. ale być może tylko sekcje, które on musi załadować do bufora .. Byłem ciekawy ed przez jakiś czas ... Myślałem, że to mógłbym edytować na miejscu ... Muszę tylko wypróbować duży plik ... Jeśli to działa, jest to rozsądne rozwiązanie, ale kiedy piszę, zaczynam myśleć, że to może zainspirować sed ( wolny od pracy z dużymi fragmentami danych ... Zauważyłem, że 'ed' faktycznie akceptuje przesyłanie strumieniowe ze skryptu (z prefiksem !), więc może mieć kilka ciekawych sztuczek w zanadrzu.
Peter.O
Jestem pewien, że operacja zapisu edskraca plik i przepisuje go. Więc to nie zmieni danych na dysku w miejscu, jak chce OP. Ponadto nie może działać, jeśli plik jest zbyt duży, aby załadować go do pamięci.
Nick Matteo
5

Możesz użyć deskryptora pliku bash do odczytu / zapisu, aby otworzyć swój plik (aby go zastąpić na miejscu), a następnie sedi truncate... ale oczywiście nie dopuść do tego, aby zmiany były większe niż ilość odczytanych danych .

Oto skrypt (używa: zmienna bash $ 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

Oto wynik testu

# 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
źródło
3

Chciałbym zmapować plik w pamięci, zrobić wszystko w miejscu za pomocą wskaźników char * do nagiej pamięci, a następnie odwzorować plik i obciąć go.

bmcnett
źródło
3
+1, ale tylko dlatego, że powszechna dostępność 64-bitowych procesorów i systemów operacyjnych umożliwia to teraz przy użyciu pliku 35 GB. Ci, którzy wciąż korzystają z systemów 32-bitowych (podejrzewam, że nawet większość odbiorców tej witryny) nie będą mogli skorzystać z tego rozwiązania.
Warren Young
2

Nie do końca na miejscu, ale - może to być przydatne w podobnych okolicznościach.
Jeśli miejsce na dysku stanowi problem, najpierw skompresuj plik (ponieważ jest to tekst, co da ogromną redukcję), a następnie użyj sed (lub grep, lub cokolwiek innego) w zwykły sposób w środku potoku dekompresji / kompresji.

# 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
źródło
2
Ale z pewnością gzip zapisuje wersję skompresowaną na dysku przed zamianą na wersję skompresowaną, więc potrzebujesz co najmniej tyle dodatkowego miejsca, w przeciwieństwie do innych opcji. Ale bezpieczniej jest, jeśli masz przestrzeń (której ja nie mam ...)
nealmcb
To sprytne rozwiązanie, które można dodatkowo zoptymalizować, aby wykonać tylko jedną kompresję zamiast dwóch:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen
0

Z korzyścią dla każdego, kto przejrzy to pytanie, poprawną odpowiedzią jest przestanie szukać niejasnych funkcji powłoki, które grożą uszkodzeniem pliku z powodu nieznacznego wzrostu wydajności, i zamiast tego należy użyć pewnej odmiany tego wzorca:

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

Tylko w niezwykle rzadkiej sytuacji , gdy z jakiegoś powodu nie jest to możliwe, powinieneś poważnie rozważyć inne odpowiedzi na tej stronie (chociaż z pewnością są interesujące do przeczytania). Przyznaję, że zagadka OP polegająca na braku miejsca na dysku do utworzenia drugiego pliku jest właśnie taką sytuacją. Chociaż nawet wtedy są dostępne inne opcje, np. Dostarczone przez @Ed Randall i @Basile Starynkevitch.

Todd Owen
źródło
1
Mogę nie rozumieć, ale nie ma to nic wspólnego z tym, o co pierwotnie poprosił PO. czyli bezpośrednia edycja dużego pliku bez wystarczającej ilości miejsca na dysku na plik tymczasowy.
Kiwy,
@Kiwy Jest to odpowiedź skierowana do innych widzów tego pytania (z których dotychczas było prawie 15 000). Pytanie „Czy istnieje sposób na zmodyfikowanie pliku w miejscu?” ma szersze znaczenie niż konkretny przypadek użycia PO.
Todd Owen
-3

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

użytkownik54620
źródło
3
Nie działa to, jeśli plik jest duży, a greppeddane przekraczają długość dozwoloną przez wiersz poleceń. następnie psuje dane
Anthon