Existe-t-il un moyen de modifier un fichier sur place?


54

J'ai un fichier assez volumineux (35 Go), et j'aimerais filtrer ce fichier in situ (c'est-à-dire que je n'ai pas assez d'espace disque pour un autre fichier), en particulier je veux grep et ignorer certains modèles - faire cela sans utiliser un autre fichier?

Disons que je veux filtrer toutes les lignes contenant foo:par exemple ...


3
@Tshepang: Je pense qu'il veut écrire dans le même fichier.
Faheem Mitha

5
"in situ" est une expression latine signifiant "en place". Littéralement, "en position".
Faheem Mitha

3
Dans ce cas, la question devrait être plus claire: quelque chose comme : existe-t-il un moyen de modifier un fichier sur place ?
Tshepang

5
@Tshepang, "in situ" est une expression assez courante utilisée en anglais pour décrire exactement cela - je pensais que le titre était assez explicite ... @Gilles, je pensais tout autant, plus facile d'attendre plus d'espace disque! ;)
Nim

2
@Nim: Eh bien, je pense que sur place est plus courant qu'in situ .
Tshepang

Réponses:


41

Au niveau des appels système, cela devrait être possible. Un programme peut ouvrir votre fichier cible en écriture sans le tronquer et commencer à écrire ce qu'il lit à partir de stdin. Lors de la lecture de EOF, le fichier de sortie peut être tronqué.

Puisque vous filtrez les lignes depuis l'entrée, la position d'écriture du fichier de sortie doit toujours être inférieure à la position de lecture. Cela signifie que vous ne devriez pas corrompre votre entrée avec la nouvelle sortie.

Cependant, trouver un programme qui fait cela est le problème. dd(1)a l'option conv=notruncqui ne tronque pas le fichier de sortie à l'ouverture, mais ne tronque pas non plus à la fin, laissant le contenu du fichier d'origine après le contenu de grep (avec une commande similaire grep pattern bigfile | dd of=bigfile conv=notrunc)

Comme il est très simple du point de vue des appels système, j’ai écrit un petit programme et l’ai testé sur un petit système de fichiers en boucle (1 Mo). Il a fait ce que vous vouliez, mais vous voulez vraiment le tester d'abord avec d'autres fichiers. Il y aura toujours des risques à écraser un fichier.

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

Vous l'utiliseriez comme:

grep pattern bigfile | overwrite bigfile

Je poste surtout ceci pour que les autres puissent commenter avant de l'essayer. Peut-être que quelqu'un d'autre connait un programme qui fait quelque chose de similaire qui est plus testé.


Je voulais voir si je pouvais partir sans écrire quelque chose pour ça! :) Je suppose que cela fera l'affaire! Merci!
Nim

2
+1 pour C; Cela semble fonctionner, mais je vois un problème potentiel: le fichier est en cours de lecture du côté gauche alors que le droit écrit dans le même fichier et, à moins que vous ne coordonniez les deux processus, vous auriez potentiellement des problèmes de réécriture sur le même fichier. des blocs. Il serait peut-être préférable que l’intégrité du fichier utilise une taille de bloc plus petite, car la plupart des outils principaux utiliseront probablement 8192. Cela pourrait ralentir suffisamment le programme pour éviter les conflits (mais ne peut pas garantir). Peut-être lire des portions plus grandes dans la mémoire (pas toutes) et écrire dans des blocs plus petits. Pourrait aussi ajouter un nanosleep (2) / usleep (3).
Arcege

4
@Arcege: L'écriture n'est pas faite en blocs. Si votre processus de lecture a lu 2 octets et que votre processus d'écriture écrit 1 octet, seul le premier octet est modifié et le processus de lecture peut continuer à lire à l'octet 3 avec le contenu original inchangé à ce stade. Etant donné grepque ne produira pas plus de données qu'il n'en lit, la position d'écriture doit toujours se situer derrière la position de lecture. Même si vous écrivez au même rythme que la lecture, tout ira bien. Essayez rot13 avec ceci au lieu de grep, puis de nouveau. md5sum l’avant et l’après et vous verrez que c’est la même chose.
camh le

6
Agréable. Cela peut être un ajout précieux aux moreutils de Joey Hess . Vous pouvez utiliserdd , mais c'est lourd.
Gilles 'SO- arrête d'être méchant'

'grep pattern bigfile | écraser bigfile '- cela fonctionne sans erreur, mais ce que je ne comprends pas, c'est que l'exigence de remplacer le contenu du motif par un autre texte n'est-elle pas? il ne devrait donc pas y avoir quelque chose comme: 'grep pattern bigfile | écraser / remplacer-texte / bigfile '
Alexander Mills

20

Vous pouvez utiliser sedpour éditer des fichiers sur place (mais cela crée un fichier temporaire intermédiaire):

Pour supprimer toutes les lignes contenant foo:

sed -i '/foo/d' myfile

Pour garder toutes les lignes contenant foo:

sed -i '/foo/!d' myfile

intéressant, ce fichier temporaire devra-t-il avoir la même taille que l’original?
Nim

3
Oui, donc ce n'est probablement pas bon.
pjc50

18
Ce n'est pas ce que l'OP demande, car il crée un deuxième fichier.
Arcege

1
Cette solution échouera sur un système de fichiers en lecture seule, où "lecture seule" signifie que vous $HOME serez en écriture, mais /tmpen lecture seule (par défaut). Par exemple, si vous avez Ubuntu et que vous avez démarré dans la console de récupération, c'est généralement le cas. En outre, l'opérateur here-document <<<n'y travaillera pas non plus, car il nécessite /tmpd'être r / w car il y écrit également un fichier temporaire. (voir cette question avec une stracesortie)
syntaxerror

oui, cela ne fonctionnera pas pour moi non plus, toutes les commandes sed que j'ai essayées remplaceront le fichier actuel par un nouveau fichier (malgré l'indicateur --in-place).
Alexander Mills

19

Je suppose que votre commande de filtre est ce que j'appellerai un filtre de réduction de préfixe , qui a la propriété que l'octet N dans la sortie n'est jamais écrit avant d'avoir lu au moins N octets d'entrée. grepa cette propriété (tant qu’il ne fait que filtrer et ne pas faire d’autres choses comme ajouter des numéros de ligne pour les correspondances). Avec un tel filtre, vous pouvez écraser l’entrée au fur et à mesure. Bien sûr, vous devez vous assurer de ne commettre aucune erreur, car la partie écrasée au début du fichier sera perdue à jamais.

La plupart des outils Unix permettent seulement d'ajouter ou de tronquer un fichier, sans possibilité de l'écraser. La seule exception dans la boîte à outils standard est celle à ddlaquelle on peut dire de ne pas tronquer son fichier de sortie. Le plan consiste donc à filtrer la commande en dd conv=notrunc. Cela ne change pas la taille du fichier, nous allons donc aussi saisir la longueur du nouveau contenu et tronquer le fichier à cette longueur (à nouveau avec dd). Notez que cette tâche est intrinsèquement non robuste - si une erreur se produit, vous êtes seul.

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

Vous pouvez écrire en gros équivalent Perl. Voici une mise en œuvre rapide qui n'essaie pas d'être efficace. Bien sûr, vous pouvez également effectuer votre filtrage initial directement dans cette langue.

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

Avec n'importe quel shell Bourne-like:

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

Pour une raison quelconque, il semble que les gens ont tendance à oublier cet opérateur de redirection lecture + écriture de 40 ans¹ et standard .

Nous ouvrons bigfileen lecture en mode écriture + et (ce qui est le plus important ici) sans troncature sur stdouttout bigfileest ouvert (séparément) sur cat« s stdin. Après grepa été terminé, et s'il a supprimé certaines lignes et qu'il stdoutpointe maintenant quelque part à l'intérieur bigfile, nous devons nous débarrasser de ce qui est au-delà de ce point. D'où la perlcommande qui tronque le fichier ( truncate STDOUT) à la position actuelle (telle que retournée par tell STDOUT).

( catc'est pour GNU grepqui se plaint sinon si stdin et stdout pointent sur le même fichier).


¹ Eh bien, bien qu’il <>soit dans le shell Bourne depuis le début à la fin des années 70, il était initialement non documenté et n’a pas été correctement mis en œuvre . Ce n’était pas dans l’implémentation initiale ashde 1989 et, bien qu’il s’agisse d’un shopérateur de redirection POSIX (depuis le début des années 90, POSIX shétant basé sur ksh88qui l’a toujours eu), il n’a pas été ajouté à FreeBSD, shpar exemple, avant 2000, donc de manière transférable . vieux est probablement plus précis. Notez également que le descripteur de fichier par défaut, lorsqu'il n'est pas spécifié, se trouve <>dans tous les shells, sauf qu'il ksh93est passé de 0 à 1 dans ksh93t + en 2010 (rupture de la compatibilité avec les versions antérieures et de la conformité POSIX).


2
Pouvez-vous expliquer le perl -e 'truncate STDOUT, tell STDOUT'? Cela fonctionne pour moi sans inclure cela. Tout moyen de réaliser la même chose sans utiliser Perl?
Aaron Blenkush

1
@AaronBlenkush, voir éditer.
Stéphane Chazelas

1
Absolument génial - merci. J'y étais alors, mais ne vous souvenez pas de cela… Il serait amusant de faire référence au standard «36 ans», car ce n'est pas mentionné sur fr.wikipedia.org/wiki/Bourne_shell . Et à quoi servait-il? Je vois une référence à un correctif dans SunOS 5.6: redirection "<>" fixed and documented (used in /etc/inittab f.i.). ce qui est un indice.
nealmcb

2
@nealmcb, voir edit.
Stéphane Chazelas

@ StéphaneChazelas Comment votre solution se compare-t-elle à cette réponse ? Apparemment, il fait la même chose mais semble plus simple.
Akhan

9

Même s’il s’agit d’une question ancienne, il me semble que c’est une question éternelle et qu’une solution plus générale et plus claire que celle suggérée jusqu’à présent est disponible. Le crédit est dû: Je ne suis pas sûr que je l'aurais trouvé sans tenir compte de la mention de Stéphane Chazelas sur l' <>opérateur de mise à jour.

L’ouverture d’un fichier à mettre à jour dans un shell Bourne est d’une utilité limitée. Le shell ne vous donne aucun moyen de rechercher sur un fichier, ni de définir sa nouvelle longueur (si elle est plus courte que l’ancien). Mais on y résout facilement, si facilement que je suis surpris que ce ne soit pas un utilitaire standard de Windows /usr/bin.

Cela marche:

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

De même que cela (astuce à Stéphane):

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

(J'utilise GNU grep. Peut-être que quelque chose a changé depuis qu'il a écrit sa réponse.)

Sauf que vous n'avez pas / usr / bin / ftruncate . Pour une douzaine de lignes de C, vous pouvez, voir ci-dessous. Cet utilitaire ftruncate tronque un descripteur de fichier arbitraire à une longueur arbitraire, par défaut à la sortie standard et à la position actuelle.

La commande ci-dessus (1er exemple)

  • ouvre le descripteur de fichier 4 sur Tpour la mise à jour. Comme avec open (2), ouvrir le fichier de cette façon positionne le décalage actuel à 0.
  • grep traite ensuite Tnormalement et le shell redirige sa sortie Tvia le descripteur 4.
  • ftruncate appelle ftruncate (2) sur le descripteur 4, en fixant la longueur à la valeur de l'offset actuel (exactement là où grep l'a laissé).

Le sous-shell se ferme alors, fermant le descripteur 4. Voici 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) est non-portable lorsqu'il est utilisé de cette façon. Pour une généralité absolue, lisez le dernier octet écrit, rouvrez le fichier O_WRONLY, recherchez, écrivez l'octet et fermez-le.

Étant donné que la question a 5 ans, je vais dire que cette solution est non évidente. Il profite d’ exec pour ouvrir un nouveau descripteur et l’ <>opérateur, qui sont tous les deux arcanes. Je ne peux pas penser à un utilitaire standard qui manipule un inode par descripteur de fichier. (La syntaxe pourrait être ftruncate >&4, mais je ne suis pas sûr que ce soit une amélioration.) Elle est considérablement plus courte que la réponse compétente et exploratoire de camh. C'est un peu plus clair que celui de Stéphane, OMI, à moins que vous n'aimiez plus Perl que moi. J'espère que quelqu'un le trouvera utile.

Une autre façon de faire la même chose serait une version exécutable de lseek (2) qui rapporte le décalage actuel; la sortie pourrait être utilisée pour / usr / bin / truncate , fourni par certains Linuxi.


5

ed est probablement le bon choix pour éditer un fichier sur place:

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

J'aime l'idée, mais à moins que différentes edversions ne se comportent différemment ..... c'est 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 vous insinuez que l'enregistrement des modifications n'affectera pas le fichier nommé, vous êtes incorrect. J'interprète cette citation en disant que vos modifications ne sont pas prises en compte tant que vous ne les avez pas enregistrées. Je concède que ce edn'est pas une solution gool pour l'édition de fichiers de 35 Go puisque le fichier est lu dans un tampon.
Glenn Jackman

2
Je pensais que cela signifiait que le fichier complet serait chargé dans le tampon .. mais peut-être que seule la ou les section (s) dont il a besoin sont chargées dans le tampon .. Je suis curieux à propos de ed depuis un moment ... je le pensais vous pouvez faire du montage in situ ... Je vais juste devoir essayer un gros fichier ... Si cela fonctionne, c'est une solution raisonnable, mais au moment où j'écris, je commence à penser que c'est peut-être ce qui a inspiré sed ( libéré de mon travail avec de gros morceaux de données ... J'ai remarqué que 'ed' peut en fait accepter les entrées en streaming depuis un script (préfixé avec !), de sorte qu'il peut avoir quelques trucs plus intéressants dans sa manche.
Peter.O

Je suis à peu près sûr que l'opération d'écriture dans edtronque le fichier et le réécrit. Cela ne modifiera donc pas les données stockées sur le disque, comme le souhaite le PO. En outre, cela ne peut pas fonctionner si le fichier est trop gros pour être chargé en mémoire.
Nick Matteo

5

Vous pouvez utiliser un descripteur de fichier bash en lecture / écriture pour ouvrir votre fichier (pour l'écraser in situ), puis sedet truncate... mais bien sûr, ne laissez jamais vos modifications dépasser le nombre de données lues jusqu'à présent. .

Voici le script (utilise: 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

Voici la sortie de 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

Je mapperais le fichier en mémoire, je mettrais tout en place en utilisant des pointeurs sur la mémoire nue, puis remapperais le fichier et le tronquer.


3
+1, mais uniquement parce que la disponibilité généralisée des processeurs et des systèmes d'exploitation 64 bits permet de le faire avec un fichier de 35 Go maintenant. Ceux qui sont toujours sur des systèmes 32 bits (la grande majorité même de l'audience de ce site, je suppose) ne pourront pas utiliser cette solution.
Warren Young

2

Pas exactement in situ mais - cela pourrait être utile dans des circonstances similaires.
Si l'espace disque pose problème, commencez par compresser le fichier (car il s'agit de texte, cela donnera une réduction considérable), puis utilisez sed (ou grep, ou autre chose) de la manière habituelle au milieu d'un pipeline de décompression / compression.

# 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
Mais gzip écrit sûrement la version compressée sur le disque avant de la remplacer par la version compressée. Vous avez donc besoin d'au moins autant d'espace supplémentaire, contrairement aux autres options. Mais il est plus sûr, si vous avez de la place (ce que je n'ai pas ....)
nealmcb le

C'est une solution intelligente qui peut être optimisée pour ne réaliser qu'une compression au lieu de deux:sed -e '/foo/d' MyFile | gzip -c >MyEditedFile.gz && gzip -dc MyEditedFile.gz >MyFile
Todd Owen

0

Pour aider ceux qui recherchent cette question dans Google, la bonne solution consiste à cesser de chercher des fonctionnalités de shell obscures qui risquent de corrompre votre fichier pour un gain de performances négligeable, et d'utiliser plutôt une variante de ce modèle:

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

Ce n'est que dans la situation extrêmement rare où, pour une raison quelconque, que cela n'est pas réalisable, que vous considériez sérieusement l'une des autres réponses de cette page (bien qu'elles soient certainement intéressantes à lire). Je concéderai que le casse-tête de l'OP qui consiste à ne pas disposer d'espace disque pour créer un deuxième fichier est exactement une telle situation. Même dans ce cas, il existe d’autres options, telles que fournies par @Ed Randall et @Basile Starynkevitch.


1
Je peux mal comprendre mais cela n’a rien à voir avec ce que le PO a demandé à l’origine. aka édition en ligne de bigfile sans avoir suffisamment d'espace disque pour le fichier temporaire.
Kiwy

@ Kiwy C'est une réponse qui s'adresse aux autres téléspectateurs de cette question (près de 15 000 l'ont été jusqu'à présent). La question "Y a-t-il un moyen de modifier un fichier sur place?" a une pertinence plus large que le cas d'utilisation spécifique du PO.
Todd Owen

-3

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


3
Cela ne fonctionne pas si le fichier est volumineux et que les greppeddonnées dépassent la longueur autorisée par la ligne de commande. les données sont ensuite corrompues
Anthon
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.