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

54

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

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

Nim
fuente
3
@ Tshepang: Creo que quiere volver a escribir en el mismo archivo.
Faheem Mitha
55
"in situ" es una frase latina que significa "en el lugar". Literalmente, "en posición".
Faheem Mitha
3
En ese caso, la pregunta debería ser más clara, algo así como ¿hay alguna manera de modificar un archivo en el lugar ?
tshepang
55
@Tshepang, "in situ" es una frase bastante común que se usa en inglés para describir exactamente eso. Pensé que el título se explicaba por sí mismo ... @Gilles, pensé que era mucho más fácil esperar más espacio en el disco. ;)
Nim
2
@Nim: Bueno, creo que in situ es más común que in situ .
tshepang

Respuestas:

41

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

Como está filtrando líneas desde la entrada, la posición de escritura del archivo de salida siempre debe ser menor que la posición de lectura. Esto significa que no debe corromper su entrada con la nueva salida.

Sin embargo, encontrar un programa que haga esto es el problema. dd(1)tiene la opción conv=notruncque no trunca el archivo de salida al abrir, pero tampoco se trunca al final, dejando el contenido del archivo original después del contenido grep (con un comando 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 programa y lo probé en un pequeño sistema de archivos de bucle completo (1MiB). Hizo lo que quería, pero primero desea probar esto con otros archivos primero. Siempre será arriesgado sobrescribir un archivo.

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

grep pattern bigfile | overwrite bigfile

Principalmente publico esto para que otros lo comenten antes de que lo pruebes. Quizás alguien más sepa de un programa que haga algo similar que esté más probado.

camh
fuente
¡Quería ver si podía escapar sin escribir algo para ello! :) ¡Supongo que esto hará el truco! ¡Gracias!
Nim
2
+1 para C; parece funcionar, pero veo un problema potencial: el archivo se está leyendo desde el lado izquierdo en el momento en que el derecho está escribiendo en el mismo archivo y, a menos que coordine los dos procesos, tendría problemas de sobrescritura potencialmente en el mismo bloques Podría ser mejor para la integridad del archivo usar un tamaño de bloque más pequeño ya que la mayoría de las herramientas centrales probablemente usarán 8192. Esto podría ralentizar el programa lo suficiente como para evitar conflictos (pero no puede garantizarlo). Tal vez lea porciones más grandes en la memoria (no todas) y escriba en bloques más pequeños. También podría agregar un nanosleep (2) / usleep (3).
Arcege
44
@Arcege: la escritura no se realiza en bloques. Si su proceso de lectura ha leído 2 bytes y su proceso de escritura escribe 1 byte, solo cambiará el primer byte y el proceso de lectura puede continuar leyendo en el byte 3 con el contenido original en ese punto sin cambios. Como grepno generará más datos de los que lee, la posición de escritura siempre debe estar detrás de la posición de lectura. Incluso si está escribiendo al mismo ritmo que la lectura, todavía estará bien. Pruebe rot13 con esto en lugar de grep, y luego nuevamente. md5sum el antes y el después y verás que es lo mismo.
camh
66
Agradable. Esta puede ser una valiosa adición a las más útiles de Joey Hess . Puedes usardd , pero es engorroso.
Gilles 'SO- deja de ser malvado'
'grep patrón bigfile | sobrescribir bigfile '. Lo hice funcionar sin errores, pero lo que no entiendo es: ¿no es necesario reemplazar lo que está en el patrón con algún otro texto? entonces, ¿no debería ser algo como: 'grep pattern bigfile | sobrescribir / reemplazar texto / archivo grande '
Alexander Mills
20

Puede usar sedpara editar archivos en su lugar (pero esto crea un archivo 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
dogbane
fuente
interesante, ¿tendrá que ser este archivo temporal del mismo tamaño que el original?
Nim
3
Sí, entonces eso probablemente no sea bueno.
pjc50
17
Esto no es lo que pide el OP ya que crea un segundo archivo.
Arcege
1
Esta solución fallará en un sistema de archivos de solo lectura, donde "solo lectura" significa que $HOME se podrá escribir, pero /tmpserá de solo lectura (por defecto). Por ejemplo, si tiene Ubuntu y ha arrancado en la Consola de recuperación, este suele ser el caso. Además, el operador de documento aquí <<<tampoco funcionará allí, ya que requiere /tmpser r / w porque también escribirá un archivo temporal allí. (cf. esta pregunta incluye una stracesalida 'd)
syntaxerror
sí, esto tampoco funcionará para mí, todos los comandos sed que he intentado reemplazarán el archivo actual con un nuevo archivo (a pesar de la bandera - en el lugar).
Alexander Mills
19

Asumiré que su comando de filtro es lo que llamaré un filtro de reducción de prefijo , que tiene la propiedad de que el byte N en la salida nunca se escribe antes de haber leído al menos N bytes de entrada. greptiene esta propiedad (siempre que solo filtre y no haga otras cosas como agregar números de línea para coincidencias). Con dicho filtro, puede sobrescribir la entrada a medida que avanza. Por supuesto, debe asegurarse de no cometer ningún error, ya que la parte sobrescrita al comienzo del archivo se perderá para siempre.

La mayoría de las herramientas de Unix solo dan la opción de agregar un archivo o truncarlo, sin posibilidad de sobrescribirlo. La única excepción en la caja de herramientas estándar es dd, que se puede decir que no trunque su archivo de salida. Entonces, el plan es filtrar el comando dd conv=notrunc. Esto no cambia el tamaño del archivo, por lo que también tomamos la longitud del nuevo contenido y truncamos el archivo a esa longitud (nuevamente con dd). Tenga en cuenta que esta tarea es inherentemente no robusta: si se produce un error, usted es el único.

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

Puedes escribir Perl con un equivalente áspero. Aquí hay una implementación rápida que no intenta ser eficiente. Por supuesto, es posible que también desee realizar su filtrado inicial directamente en ese idioma.

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- deja de ser malvado'
fuente
16

Con cualquier caparazón 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 redirección de lectura y escritura estándar de 40 años year .

Abrimos bigfileen modo lectura + escritura y (lo que más importa aquí) sin truncamiento stdoutmientras bigfileestá abierto (por separado) en cat's stdin. Después de que grephaya terminado, y si ha eliminado algunas líneas, stdoutahora apunta a algún lugar dentro bigfile, debemos deshacernos de lo que está más allá de este punto. De ahí el perlcomando que trunca el archivo ( truncate STDOUT) en la posición actual (tal como lo devuelve tell STDOUT).

(el cates para GNU grepque de otro modo se queja si stdin y stdout apuntan al mismo archivo).


¹ Bueno, si bien <>estuvo en el shell Bourne desde el principio a fines de los años setenta, inicialmente no estaba documentado y no se implementó correctamente . No estaba en la implementación original de ash1989 y, si bien es un shoperador de redireccionamiento POSIX (desde principios de los 90, ya que POSIX shse basa en lo ksh88que siempre lo tuvo), no se agregó a FreeBSD, shpor ejemplo, hasta 2000, por lo que es portátil durante 15 años. viejo es probablemente más exacto. También tenga en cuenta que el descriptor de archivo predeterminado cuando no se especifica está <>en todos los shells, excepto que ksh93cambió de 0 a 1 en ksh93t + en 2010 (rompiendo la compatibilidad con versiones anteriores y el cumplimiento de POSIX)

Stéphane Chazelas
fuente
2
¿Puedes explicar el perl -e 'truncate STDOUT, tell STDOUT'? Funciona para mí sin incluir eso. ¿Alguna forma de lograr lo mismo sin usar Perl?
Aaron Blenkush
1
@AaronBlenkush, ver edición.
Stéphane Chazelas
1
Absolutamente brillante, gracias. Estuve allí entonces, pero no recuerdo esto ... Una referencia para el estándar de "36 años" sería divertido, ya que no se menciona en en.wikipedia.org/wiki/Bourne_shell . ¿Y para qué se usaba? Veo una referencia a una corrección de errores en SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). que es una pista.
nealmcb
2
@nealmcb, ver edición.
Stéphane Chazelas
@ StéphaneChazelas ¿Cómo se compara su solución con esta respuesta ? Aparentemente hace lo mismo pero parece más simple.
Akhan
9

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 donde se debe el crédito: no estoy seguro de haberlo ideado sin tener en cuenta la mención de Stéphane Chazelas del <>operador de actualización.

Abrir un archivo para actualizarlo en un shell Bourne es de utilidad limitada. El shell no le da forma de buscar en un archivo, y no tiene forma de establecer su nueva longitud (si es más corta que la anterior). Pero eso se soluciona fácilmente, así que me sorprende que no esté entre las utilidades estándar /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. Quizás algo ha cambiado desde que escribió su respuesta).

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

El comando anterior (primer ejemplo)

  • abre el descriptor de archivo 4 Tpara actualización. Al igual que con open (2), al abrir el archivo de esta manera, el desplazamiento actual se sitúa en 0.
  • grep luego procesa Tnormalmente, y el shell redirige su salida a Ttravés del descriptor 4.
  • ftruncate llama a ftruncate (2) en el descriptor 4, configurando la longitud al valor del desplazamiento actual (exactamente donde grep lo dejó).

El subshell luego sale, cerrando el descriptor 4. Aquí está 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) no es portátil cuando se usa de esta manera. Para una generalidad absoluta, lea el último byte escrito, vuelva a abrir el archivo 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 el exec para abrir un nuevo descriptor y el <>operador, los cuales son arcanos. No puedo pensar en una utilidad estándar que manipule un inodo por descriptor de archivo. (La sintaxis podría ser ftruncate >&4, pero no estoy seguro de que sea una mejora). Es considerablemente más corta que la respuesta exploratoria competente de Camh. Es solo un poco más claro que Stéphane's, en mi opinión, a menos que te guste Perl más que a mí. Espero que alguien lo encuentre útil.

Una forma diferente de hacer lo mismo sería una versión ejecutable de lseek (2) que informa el desplazamiento actual; la salida podría usarse para / usr / bin / truncate , que algunos Linuxi proporcionan.

James K. Lowden
fuente
5

ed es probablemente la opción correcta para editar un archivo en el lugar:

ed my_big_file << END_OF_ED_COMMANDS
g/foo:/d
w
q 
END_OF_ED_COMMANDS
Glenn Jackman
fuente
Me gusta la idea, pero a menos que diferentes edversiones se comporten de manera diferente ... esto es de 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, si estás insinuando que guardar los cambios no afectará el archivo nombrado, eres incorrecto. Interpreto esa cita para decir que sus cambios no se reflejan hasta que los guarde. edReconozco que no es una solución gool para editar archivos de 35 GB, ya que el archivo se lee en un búfer.
Glenn Jackman
2
Estaba pensando que significaba que el archivo completo se cargaría en el búfer ... pero tal vez solo las secciones que necesita se cargan en el búfer ... Tengo curiosidad por ed por un tiempo ... pensé que podría hacer la edición in situ ... solo tendré que probar un archivo grande ... Si funciona es una solución razonable, pero mientras escribo, estoy empezando a pensar que esto puede ser lo que inspiró sed ( liberado de trabajar con grandes fragmentos de datos ... Me he dado cuenta de que 'ed' en realidad puede aceptar la entrada transmitida desde un script (con el prefijo !), por lo que puede tener algunos trucos más interesantes bajo la manga.
Peter.O
Estoy bastante seguro de que la operación de escritura en edtrunca el archivo y lo reescribe. Por lo tanto, esto no alterará los datos en el disco en el lugar como lo desea el OP. Además, no puede funcionar si el archivo es demasiado grande para cargarlo en la memoria.
Nick Matteo
5

Puede usar un descriptor de archivo de lectura / escritura bash para abrir su archivo (para sobrescribirlo in situ), luego sedy truncate... pero, por supuesto, nunca permita que sus cambios sean mayores que la cantidad de datos leídos hasta ahora .

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 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

Aquí está la salida de prueba

# 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
fuente
3

Mapearía el archivo en la memoria, haría todo en el lugar usando punteros char * a memoria desnuda, luego desasignaría el archivo y lo truncaría.

bmcnett
fuente
3
+1, pero solo porque la disponibilidad generalizada de CPU y SO de 64 bits hace posible hacerlo con un archivo de 35 GB ahora. Aquellos que todavía están en sistemas de 32 bits (la gran mayoría incluso de la audiencia de este sitio, sospecho) no podrán usar esta solución.
Warren Young
2

No exactamente in situ, pero esto podría ser útil en circunstancias similares.
Si el espacio en el disco es un problema, comprima primero el archivo (dado que es texto, esto dará una gran reducción) y luego use sed (o grep, o lo que sea) de la manera habitual en medio de una tubería de descompresión / compresió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
Ed Randall
fuente
2
Pero seguramente gzip está escribiendo la versión comprimida en el disco antes de reemplazarla con la versión comprimida, por lo que necesita al menos ese espacio adicional, a diferencia de las otras opciones. Pero es más seguro, si tienes el espacio (que yo no ...)
nealmcb
Esta es una solución inteligente que puede optimizarse aún más para realizar solo una compresión en lugar de dos:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen el
0

Para el beneficio de cualquiera que busque en Google esta pregunta, la respuesta correcta es dejar de buscar características de shell oscuras que corran el riesgo de corromper su archivo para obtener un aumento de rendimiento insignificante, y en su lugar use alguna variación de este patrón:

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

Solo en la situación extremadamente infrecuente de que esto por alguna razón no sea factible, debe considerar seriamente cualquiera de las otras respuestas en esta página (aunque ciertamente son interesantes de leer). Reconozco que el enigma del OP de no tener espacio en disco para crear un segundo archivo es exactamente una situación así. Aunque incluso entonces, hay otras opciones disponibles, por ejemplo, según lo provisto por @Ed Randall y @Basile Starynkevitch.

Todd Owen
fuente
1
Puedo entender mal pero no tiene nada que ver con lo que el OP preguntó originalmente. también conocido como edición en línea de bigfile sin tener suficiente espacio en disco para el archivo temporal.
Kiwy
@Kiwy Es una respuesta dirigida a otros espectadores de esta pregunta (de las cuales ha habido casi 15,000 hasta ahora). La pregunta "¿Hay alguna manera de modificar un archivo en el lugar?" tiene una relevancia más amplia que el caso de uso específico del OP.
Todd Owen el
-3

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

usuario54620
fuente
3
Esto no funciona si el archivo es grande y los greppeddatos exceden la longitud de lo que permite la línea de comandos. luego corrompe los datos
Anthon