¿Hay alguna manera de modificar un file en el lugar?

Tengo un file bastante grande (35Gb) y me gustaría filtrar este file in situ (es decir, no tengo suficiente espacio en disco para otro file), específicamente quiero grep e ignorar algunos patrones. ¿Hay alguna manera de hacer esto sin usar otro file?

Digamos que quiero filtrar todas las líneas que contienen foo: por ejemplo …

Solutions Collecting From Web of "¿Hay alguna manera de modificar un file en el lugar?"

En el nivel de llamada del sistema esto debería ser posible. Un progtwig puede abrir su file de destino para escribir sin truncarlo y comenzar a escribir lo que lee de stdin. Al leer EOF, el file de salida se puede truncar.

Como está filtrando líneas desde la input, la position de escritura del file de salida siempre debe ser menor que la position de lectura. Esto significa que no debe corromper su input con la nueva salida.

Sin embargo, encontrar un progtwig que hace esto es el problema. dd(1) tiene la opción conv=notrunc que no trunca el file de salida al abrir, pero tampoco trunca al final, dejando el contenido del file original después del contenido grep (con un command como grep pattern bigfile | dd of=bigfile conv=notrunc )

Como es muy simple desde la perspectiva de una llamada al sistema, escribí un pequeño progtwig y lo probé en un sistema de files de bucle de retorno pequeño (1MiB). Hizo lo que quería, pero realmente quiere probar esto con algunos otros files primero. Siempre va a ser arriesgado sobrescribir un file.

sobrescribir.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 usarías como:

 grep pattern bigfile | overwrite bigfile 

Casi siempre publico esto para que otros comenten antes de probarlo. Quizás alguien más sepa de un progtwig que hace algo similar que está más probado.

Puede usar sed para editar files en su lugar (pero esto crea un file temporal intermedio):

Para eliminar todas las líneas que contienen foo :

 sed -i '/foo/d' myfile 

Para mantener todas las líneas que contienen foo :

 sed -i '/foo/!d' myfile 

Asumo que su command de filter es lo que llamaré un filter de contracción de prefijo , que tiene la propiedad de que el byte N en la salida nunca se escribe antes de haber leído al less N bytes de input. grep tiene esta propiedad (siempre y cuando solo filtre y no haga otras cosas como agregar numbers de línea para las coincidencias). Con un filter de este tipo, puede sobrescribir la input a medida que avanza. Por supuesto, debe asegurarse de no cometer ningún error, ya que la parte sobrescrita al principio del file se perderá para siempre.

La mayoría de las herramientas de Unix solo dan la opción de agregar a un file o truncarlo, sin posibilidad de sobreescribirlo. La única exception en la caja de herramientas estándar es dd , que se le puede decir que no trunque su file de salida. Entonces el plan es filtrar el command en dd conv=notrunc . Esto no cambia el tamaño del file, por lo que también tomamos la longitud del nuevo contenido y truncamos el file a esa longitud (de nuevo con dd ). Tenga en count que esta tarea es intrínsecamente no robusta: si se produce un error, está 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 

Usted puede escribir Perl rugoso equivalente. Aquí hay una implementación rápida que no intenta ser eficiente. Por supuesto, es posible que desee hacer su filtrado inicial directamente en ese idioma también.

 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 

Con cualquier shell tipo Bourne:

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

Por alguna razón, parece que las personas tienden a olvidarse de ese operador de networkingirección de lectura y escritura de 40 años de antigüedad¹.

Abrimos bigfile en modo lectura + escritura y (lo que más importa aquí) sin truncado en stdout mientras que bigfile está abierto (por separado) en cat 's stdin . Después de que grep haya terminado, y si se han eliminado algunas líneas, stdout ahora apunta a algún lugar dentro de bigfile , tenemos que deshacernos de lo que está más allá de este punto. De ahí el command perl que trunca el file ( truncate STDOUT ) en la position actual (como lo devuelve tell STDOUT ).

(el cat es para grep GNU que de lo contrario se queja si stdin y stdout apuntan al mismo file).


¹ Bueno, mientras que <> ha estado en el shell Bourne desde el principio a finales de los años setenta, inicialmente no se documentó y no se implementó correctamente . No estaba en la implementación original de las ash de 1989 y, aunque es un operador de networkingirección POSIX sh (desde principios de los 90 porque POSIX sh se basa en ksh88 que siempre lo tenía), no se agregó a FreeBSD sh por ejemplo hasta 2000 , así que portátilmente 15 años de edad es probablemente más preciso. También tenga en count que el descriptor de file pnetworkingeterminado cuando no se especifica es <> en todos los shells, excepto que en ksh93 cambió de 0 a 1 en ksh93t + en 2010 (rompiendo la compatibilidad con versiones anteriores y el cumplimiento de POSIX)

Aunque esta es una vieja pregunta, me parece que es una pregunta perenne, y hay disponible una solución más general y más clara de lo que se ha sugerido hasta ahora. Crédito en el que se debe crédito: no estoy seguro de haberlo logrado sin considerar la mención de Stéphane Chazelas del <> operador de actualización.

Abrir un file para actualizar en un shell Bourne es de utilidad limitada. El intérprete de commands no le ofrece ninguna forma de search en un file ni forma de establecer su nueva longitud (si es más corta que la anterior). Pero eso se remedia fácilmente, tan fácilmente que me sorprende que no esté entre las utilidades estándar en /usr/bin .

Esto funciona:

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

Como hace esto (punta de sombrero para Stéphane):

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

(Estoy usando GNU grep. Tal vez algo ha cambiado desde que escribió su respuesta.)

Excepto que no tiene / usr / bin / ftruncate . Para un par de docenas de líneas de C, puede, consulte a continuación. Esta utilidad ftruncate trunca un descriptor de file arbitrario a una longitud arbitraria, por defecto a la salida estándar y la position actual.

El command anterior (primer ejemplo)

  • abre el descriptor de file 4 en T para la actualización. Al igual que con open (2), al abrir el file de esta manera, se coloca el offset actual en 0.
  • grep luego procesa T normalmente, y el shell networkingirige su salida a T través del descriptor 4.
  • ftruncate calls ftruncate (2) en el descriptor 4, configurando la longitud al valor del offset actual (exactamente donde lo dejó grep ).

La subshell luego sale, cerrando el descriptor 4. Aquí está ftruncado :

 #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) no es portable cuando se usa de esta manera. Para una generalidad absoluta, lea el último byte escrito, vuelva a abrir el file O_WRONLY, busque, escriba el byte y cierre.

Dado que la pregunta tiene 5 años, voy a decir que esta solución no es obvia. Se aprovecha al ejecutor para abrir un nuevo descriptor y el operador <> , ambos son arcanos. No puedo pensar en una utilidad estándar que manipule un inode por descriptor de file. (La syntax puede ser ftruncate >&4 , pero no estoy seguro de que sea una mejora.) Es considerablemente más corta que la respuesta exploratoria competente de Cam. Es un poco más claro que Stéphane's, IMO, a less que te guste Perl más que yo. Espero que alguien lo encuentre útil.

Una forma diferente de hacer lo mismo sería una versión ejecutable de lseek (2) que informa la compensación actual; la salida se puede usar para / usr / bin / truncado , que algunos Linux proporcionan.

ed es probablemente la opción correcta para editar un file in situ:

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

Puede usar un descriptor de file bash de lectura / escritura para abrir su file (para sobrescribirlo in-situ), luego sed y truncate … pero por supuesto, nunca permita que sus cambios sean más grandes que la cantidad de datos leídos hasta aquí.

Aquí está el script (usa: 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 assunetworking 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 

Aquí está la salida de testing

 # 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 

Me gustaría hacer un map de la memory del file, hacer todo en el lugar usando los caracteres * de la memory, luego desasignar el file y truncarlo.

No exactamente in situ, pero esto podría ser útil en circunstancias similares.
Si el espacio en el disco es un problema, primero comprima el file (dado que es text, esto dará una gran networkingucción) luego use sed (o grep, o lo que sea) de la manera habitual en el medio de una tubería de compression / descompression.

 # 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 

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