C'è un modo per modificare un file sul posto?


54

Ho un file abbastanza grande (35Gb) e vorrei filtrare questo file in situ (cioè non ho abbastanza spazio su disco per un altro file), in particolare voglio grep e ignorare alcuni schemi - c'è un modo per farlo senza usare un altro file?

Diciamo che voglio filtrare tutte le righe che contengono foo:ad esempio ...


3
@Tshepang: penso che voglia riscrivere nello stesso file.
Faheem Mitha,

5
"in situ" è una frase latina che significa "sul posto". Letteralmente "in posizione".
Faheem Mitha,

3
In tal caso, la domanda dovrebbe essere più chiara, qualcosa di simile esiste un modo per modificare un file sul posto ?
Tshepang,

5
@Tshepang, "in situ" è una frase abbastanza comune usata in inglese per descriverlo esattamente - Ho pensato che il titolo fosse abbastanza autoesplicativo ... @Gilles, ho pensato che fosse tanto più facile aspettare più spazio su disco! ;)
Nim

2
@Nim: Beh, penso che sul posto sia più comune che in situ .
Tshepang,

Risposte:


41

A livello di chiamata di sistema ciò dovrebbe essere possibile. Un programma può aprire il file di destinazione per la scrittura senza troncarlo e iniziare a scrivere ciò che legge da stdin. Durante la lettura di EOF, il file di output può essere troncato.

Poiché si filtrano le righe dall'input, la posizione di scrittura del file di output deve essere sempre inferiore alla posizione di lettura. Questo significa che non dovresti corrompere i tuoi input con il nuovo output.

Tuttavia, trovare un programma che fa questo è il problema. dd(1)ha l'opzione conv=notruncche non tronca il file di output in apertura, ma non si tronca alla fine, lasciando il contenuto del file originale dopo il contenuto grep (con un comando simile grep pattern bigfile | dd of=bigfile conv=notrunc)

Poiché è molto semplice dal punto di vista delle chiamate di sistema, ho scritto un piccolo programma e l'ho testato su un piccolo file system (1MiB) full loopback. Ha fatto quello che volevi, ma prima vuoi testarlo con altri file. Sarà sempre rischioso sovrascrivere un file.

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

Lo useresti come:

grep pattern bigfile | overwrite bigfile

Lo pubblicherò principalmente per consentire ad altri di commentarlo prima di provarlo. Forse qualcun altro conosce un programma che fa qualcosa di simile che è più testato.


Volevo vedere se potevo andarmene senza scrivere qualcosa per questo! :) Immagino che questo farà il trucco! Grazie!
Nim,

2
+1 per C; sembra funzionare, ma vedo un potenziale problema: il file viene letto dal lato sinistro nel momento in cui il diritto sta scrivendo nello stesso file e, a meno che non si coordinino i due processi, si avrebbero sovrascrivere i problemi potenzialmente sullo stesso blocchi. Potrebbe essere meglio per l'integrità del file utilizzare blocchi di dimensioni inferiori poiché la maggior parte degli strumenti di base probabilmente utilizzerà 8192. Ciò potrebbe rallentare il programma abbastanza da evitare conflitti (ma non può garantire). Magari leggi porzioni più grandi in memoria (non tutte) e scrivi in ​​blocchi più piccoli. Potrebbe anche aggiungere un nanosleep (2) / usleep (3).
Arcege,

4
@Arcege: la scrittura non viene eseguita in blocchi. Se il processo di lettura ha letto 2 byte e il processo di scrittura scrive 1 byte, solo il primo byte cambierà e il processo di lettura può continuare a leggere al byte 3 con i contenuti originali a quel punto invariati. Poiché grepnon genererà più dati di quanti ne legga, la posizione di scrittura dovrebbe essere sempre dietro la posizione di lettura. Anche se stai scrivendo alla stessa velocità della lettura, sarà comunque ok. Prova rot13 con questo invece di grep, e poi di nuovo. md5sum prima e dopo e vedrai che è lo stesso.
Camh

6
Bello. Questa potrebbe essere una preziosa aggiunta ai maggiori dettagli di Joey Hess . Puoi usarlodd , ma è ingombrante.
Gilles 'SO- smetti di essere malvagio' l'

'grep pattern bigfile | sovrascrivi bigfile "- ho fatto in modo che funzioni senza errori, ma ciò che non capisco è - non è necessario sostituire ciò che è nel modello con qualche altro testo? quindi non dovrebbe essere qualcosa del tipo: 'grep pattern bigfile | overwrite / replace-text / bigfile '
Alexander Mills,

20

È possibile utilizzare sedper modificare i file sul posto (ma ciò crea un file temporaneo intermedio):

Per rimuovere tutte le righe contenenti foo:

sed -i '/foo/d' myfile

Per mantenere tutte le righe contenenti foo:

sed -i '/foo/!d' myfile

interessante, questo file temporaneo dovrà avere le stesse dimensioni dell'originale?
Nim,

3
Sì, quindi probabilmente non va bene.
pjc50,

17
Questo non è ciò che l'OP richiede poiché crea un secondo file.
Arcege,

1
Questa soluzione fallirà su un file system di sola lettura, in cui "sola lettura" significa che $HOME sarà scrivibile, ma /tmpsarà di sola lettura (per impostazione predefinita). Ad esempio, se hai Ubuntu e hai avviato la Console di ripristino di emergenza, questo è comunemente il caso. Inoltre, anche l'operatore del documento qui <<<non funzionerà lì, poiché richiede /tmpdi essere r / w perché scriverà anche un file temporaneo. (cfr. questa domanda incl. a strace'd output)
syntaxerror

sì, non funzionerà neanche per me, tutti i comandi sed che ho provato sostituiranno il file corrente con un nuovo file (nonostante il flag --in-place).
Alexander Mills,

19

Presumo che il tuo comando di filtro sia quello che chiamerò prefisso filtro di riduzione , che ha la proprietà che il byte N nell'output non viene mai scritto prima di aver letto almeno N byte di input. grepha questa proprietà (fintanto che sta solo filtrando e non facendo altre cose come aggiungere numeri di riga per le partite). Con un tale filtro, puoi sovrascrivere l'input man mano che procedi. Ovviamente, devi essere sicuro di non commettere errori, poiché la parte sovrascritta all'inizio del file andrà persa per sempre.

La maggior parte degli strumenti unix offre solo la possibilità di aggiungere un file o troncarlo, senza possibilità di sovrascriverlo. L'unica eccezione nella casella degli strumenti standard è ddche si può dire di non troncare il suo file di output. Quindi il piano è di filtrare il comando in dd conv=notrunc. Questo non cambia la dimensione del file, quindi prendiamo anche la lunghezza del nuovo contenuto e tronciamo il file a quella lunghezza (di nuovo con dd). Nota che questa attività è intrinsecamente non affidabile: se si verifica un errore, sei da solo.

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

Puoi scrivere Perl approssimativamente equivalente. Ecco un'implementazione rapida che non cerca di essere efficiente. Naturalmente, potresti voler eseguire il filtro iniziale direttamente anche in quella lingua.

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

Con qualsiasi shell tipo Bourne:

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

Per qualche ragione, sembra che le persone tendano a dimenticare quell'operatore di reindirizzamento read + write standard di 40 anni¹ .

Apriamo bigfilein modalità di scrittura + lettura e (ciò che più conta qui) senza troncamento sulla stdoutmentre bigfileè aperto (a parte) sulla cats' stdin. Dopo che grepè terminato, e se ha rimosso alcune linee, stdoutora punta da qualche parte all'interno bigfile, dobbiamo liberarci di ciò che va oltre questo punto. Da qui il perlcomando che tronca il file ( truncate STDOUT) nella posizione corrente (come restituito da tell STDOUT).

( catè per GNU grepche altrimenti si lamenta se stdin e stdout puntano allo stesso file).


¹ Bene, mentre <>è stato nella shell Bourne dall'inizio alla fine degli anni settanta, inizialmente era privo di documenti e non correttamente implementato . Non era nell'implementazione originale del ash1989 e, sebbene sia un shoperatore di reindirizzamento POSIX (dai primi anni '90 come POSIX shsi basa su ksh88cui l'aveva sempre fatto), non è stato aggiunto a FreeBSD shper esempio fino al 2000, quindi portabilmente 15 anni vecchio è probabilmente più preciso. Si noti inoltre che il descrittore di file predefinito quando non specificato è <>in tutte le shell, tranne che in ksh93esso è cambiato da 0 a 1 in ksh93t + nel 2010 (interrompendo la compatibilità con le versioni precedenti e la conformità POSIX)


2
Puoi spiegare il perl -e 'truncate STDOUT, tell STDOUT'? Funziona per me senza includerlo. Qualche modo per ottenere la stessa cosa senza usare Perl?
Aaron Blenkush,

1
@AaronBlenkush, vedi modifica.
Stéphane Chazelas,

1
Assolutamente geniale - grazie. Ero lì allora, ma non ricordo questo .... Un riferimento per lo standard "36 anni" sarebbe divertente, dal momento che non è menzionato su en.wikipedia.org/wiki/Bourne_shell . E a cosa serviva? Vedo un riferimento a una correzione di bug in SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). che è un suggerimento.
nealmcb,

2
@nealmcb, vedi modifica.
Stéphane Chazelas,

@ StéphaneChazelas Come si confronta la tua soluzione con questa risposta ? Apparentemente fa la stessa cosa ma sembra più semplice.
Akhan

9

Anche se questa è una vecchia domanda, mi sembra che sia una domanda perenne, ed è disponibile una soluzione più generale e più chiara di quanto sia stato suggerito finora. Credito dove è dovuto il credito: non sono sicuro che me lo sarei inventato senza considerare la menzione di Stéphane Chazelas <>dell'operatore di aggiornamento.

L'apertura di un file per l'aggiornamento in una shell Bourne è di utilità limitata. La shell non ti dà modo di cercare un file e di impostare la sua nuova lunghezza (se inferiore a quella precedente). Ma questo è facilmente risolto, così facilmente sono sorpreso che non sia tra le utility standard in /usr/bin.

Questo funziona:

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

Allo stesso modo (punta del cappello a Stéphane):

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

(Sto usando GNU grep. Forse qualcosa è cambiato da quando ha scritto la sua risposta.)

Tranne che non hai / usr / bin / ftruncate . Per una dozzina di linee di C, puoi, vedi sotto. Questa utility ftruncate tronca un descrittore di file arbitrario a una lunghezza arbitraria, impostando automaticamente l'output standard e la posizione corrente.

Il comando sopra (primo esempio)

  • apre il descrittore di file 4 Tper l'aggiornamento. Proprio come con open (2), l'apertura del file in questo modo posiziona l'offset corrente su 0.
  • grep quindi elabora Tnormalmente e la shell reindirizza il proprio output Ttramite il descrittore 4.
  • ftruncate chiama ftruncate (2) sul descrittore 4, impostando la lunghezza sul valore dell'offset corrente (esattamente dove grep lo ha lasciato).

La subshell quindi esce, chiudendo il descrittore 4. Ecco qui :

#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) non è portabile quando utilizzato in questo modo. Per una generalità assoluta, leggere l'ultimo byte scritto, riaprire il file O_WRONLY, cercare, scrivere il byte e chiudere.

Dato che la domanda ha 5 anni, sto per dire che questa soluzione è impercettibile. Sfrutta exec per aprire un nuovo descrittore e l' <>operatore, entrambi arcani. Non riesco a pensare a un'utilità standard che manipola un inode dal descrittore di file. (La sintassi potrebbe essere ftruncate >&4, ma non sono sicuro che sia un miglioramento.) È notevolmente più breve della risposta esplorativa competente di Camh. È solo un po 'più chiaro di Stéphane, IMO, a meno che non ti piaccia Perl più di me. Spero che qualcuno lo trovi utile.

Un modo diverso di fare la stessa cosa sarebbe una versione eseguibile di lseek (2) che riporta l'offset corrente; l'output potrebbe essere usato per / usr / bin / truncate , che alcuni Linuxi forniscono.


5

ed è probabilmente la scelta giusta per modificare un file sul posto:

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

Mi piace l'idea, ma a meno che edversioni diverse si comportino in modo diverso ..... questo proviene da 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, se stai insinuando che il salvataggio delle modifiche non influirà sul file indicato, non sei corretto. Interpreto quella citazione per dire che le tue modifiche non si riflettono finché non le salvi. Ammetto che ednon è una soluzione gool per la modifica di file da 35 GB poiché il file viene letto in un buffer.
Glenn Jackman,

2
Stavo pensando che significava che il file completo sarebbe stato caricato nel buffer .. ma forse solo le sezioni che è necessario caricare nel buffer .. Sono stato curioso di sapere per un po '... Ci ho pensato potrei fare editing in situ ... dovrò solo provare un grosso file ... Se funziona è una soluzione ragionevole, ma mentre scrivo, sto iniziando a pensare che questo potrebbe essere ciò che ha ispirato sed ( liberato dal lavoro con grandi blocchi di dati ... Ho notato che "ed" può effettivamente accettare input in streaming da uno script (con prefisso !), quindi potrebbe avere qualche
asso nella

Sono abbastanza sicuro che l'operazione di scrittura edtronca il file e lo riscrive. Quindi questo non altererà i dati sul disco sul posto come desidera l'OP. Inoltre, non può funzionare se il file è troppo grande per essere caricato in memoria.
Nick Matteo,

5

Puoi usare un descrittore di file di lettura / scrittura bash per aprire il tuo file (per sovrascriverlo in situ), quindi sede truncate... ma ovviamente, non permettere mai che le tue modifiche siano maggiori della quantità di dati letti finora .

Ecco lo script (usa: variabile 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

Ecco l'output del test

# 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

Mappare la memoria del file, farei qualsiasi cosa sul posto usando i puntatori char * sulla memoria vuota, quindi deselezionerei il file e lo troncassi.


3
+1, ma solo perché la diffusa disponibilità di CPU e sistemi operativi a 64 bit rende possibile farlo ora con un file da 35 GB. Quelli ancora su sistemi a 32 bit (sospetto che la stragrande maggioranza del pubblico di questo sito) non sarà in grado di utilizzare questa soluzione.
Warren Young,

2

Non esattamente in situ ma - questo potrebbe essere utile in circostanze simili.
Se lo spazio su disco è un problema, comprimere prima il file (dato che si tratta di testo, ciò ridurrà enormemente), quindi utilizzare sed (o grep, o qualsiasi altra cosa) nel solito modo nel mezzo di una pipeline di decompressione / compressione.

# 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
Ma sicuramente gzip sta scrivendo la versione compressa sul disco prima di sostituirla con la versione compressa, quindi è necessario almeno molto spazio extra, a differenza delle altre opzioni. Ma è più sicuro, se hai lo spazio (cosa che io non ....)
nealmcb

Questa è una soluzione intelligente che può essere ulteriormente ottimizzata per eseguire solo una compressione anziché due:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen

0

A beneficio di chiunque cerchi su Google questa domanda, la risposta corretta è smettere di cercare oscure funzionalità della shell che rischiano di corrompere il file per un guadagno trascurabile in termini di prestazioni e invece utilizzare alcune varianti di questo schema:

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

Solo nella situazione estremamente rara che ciò non è fattibile per qualche motivo, dovresti prendere in seria considerazione qualsiasi delle altre risposte in questa pagina (sebbene siano certamente interessanti da leggere). Concedo che l'enigma dell'OP di non avere spazio su disco per creare un secondo file è esattamente una situazione del genere. Anche se anche in questo caso, sono disponibili altre opzioni, ad esempio fornite da @Ed Randall e @Basile Starynkevitch.


1
Potrei fraintendere ma non ha nulla a che fare con ciò che l'OP ha chiesto originariamente. aka modifica in linea di bigfile senza spazio su disco sufficiente per il file temporaneo.
Kiwy,

@Kiwy È una risposta rivolta ad altri spettatori di questa domanda (di cui finora ci sono stati quasi 15.000). La domanda "Esiste un modo per modificare un file sul posto?" ha una rilevanza più ampia rispetto al caso d'uso specifico del PO.
Todd Owen,

-3

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


3
Questo non funziona se il file è grande e i greppeddati superano la lunghezza consentita dalla riga di comando. quindi corrompe i dati
Anthon,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.