Il est bien connu qu'une commande comme celle-ci:
cat filename | some_sed_command >filename
efface le fichier nomfichier, car la redirection de sortie, exécutée avant la commande, provoque la troncature de nomfichier.
On pourrait résoudre le problème de la manière suivante:
cat file | some_sed_command | tee file >/dev/null
mais je ne suis pas sûr que cela fonctionnerait dans tous les cas: que se passera-t-il si le fichier (et le résultat de la commande sed) est très volumineux? Comment le système d'exploitation peut-il éviter d'écraser du contenu qui n'est toujours pas lu? Je vois qu’il existe également une commande éponge qui devrait fonctionner dans tous les cas: est-ce "plus sûr" que le tee?
On pourrait résoudre le problème de la manière suivante:
cat file | some_sed_command | tee file >/dev/null
Non .
Les chances que file
soit tronqué, mais il n'y a aucune garantie que cat file | some_sed_command | tee file >/dev/null
ne tronque pas file
.
Tout dépend de la commande traitée en premier, par opposition à ce à quoi on peut s'attendre, les commandes d'un canal ne sont pas traitées de gauche à droite . Il n'y a aucune garantie quant à la commande qui sera sélectionnée en premier. Vous pouvez donc tout aussi bien penser qu'elle est sélectionnée au hasard et ne jamais compter sur le fait que Shell ne sélectionne pas la commande fautive.
Étant donné que les chances que la commande incriminée soit sélectionnée en premier entre trois commandes sont plus faibles que les chances pour que la commande incriminée soit sélectionnée en premier entre deux commandes, il est moins probable que file
soit tronqué, mais ça va encore arriver .
script.sh
:
#!/bin/bash
for ((i=0; i<100; i++)); do
cat >file <<-EOF
foo
bar
EOF
cat file |
sed 's/bar/baz/' |
tee file >/dev/null
[ -s file ] &&
echo 'Not truncated' ||
echo 'Truncated'
done |
sort |
uniq -c
rm file
% bash script.sh
93 Not truncated
7 Truncated
% bash script.sh
98 Not truncated
2 Truncated
% bash script.sh
100 Not truncated
Donc never utilise quelque chose comme cat file | some_sed_command | tee file >/dev/null
. Utilisez sponge
comme suggéré par Oli.
Au lieu de cela, pour les environnements plus exigeants et/ou les fichiers relativement petits, vous pouvez utiliser une chaîne here et une substitution de commande pour lire le fichier avant l'exécution d'une commande:
$ cat file
foo
bar
$ for ((i=0; i<100; i++)); do <<<"$(<file)" sed 's/bar/baz/' >file; done
$ cat file
foo
baz
Pour sed
spécifiquement, vous pouvez utiliser son argument in-situ -i
. Il enregistre simplement dans le fichier qu'il a ouvert, par exemple:
sed -i 's/ /-/g' filename
Si vous voulez faire quelque chose de plus costaud, en supposant que vous fassiez plus que sed
, vous pouvez tout tamponner avec sponge
(du paquet moreutils
) qui va "absorber" "tous les stdin avant d 'écrire dans le fichier. C'est comme tee
mais avec moins de fonctionnalités. Pour une utilisation de base, il s’agit plutôt d’un remplacement immédiat:
cat file | some_sed_command | sponge file >/dev/null
Est-ce plus sûr? Absolument. Il a probablement des limites, donc si vous faites quelque chose de colossal (et que vous ne pouvez pas éditer sur place avec sed), vous voudrez peut-être faire vos modifications dans un second fichier, puis mv
ce fichier à l'original. nom de fichier. Cela devrait être atomique (ainsi, tout ce qui dépend de ces fichiers ne sera pas cassé s’ils ont besoin d’un accès constant).
Oh, mais sponge
n'est pas la seule option; vous ne devez pas obtenir moreutils
pour que cela fonctionne correctement. Tout mécanisme fonctionnera tant qu'il répond aux deux exigences suivantes:
Vous voyez, le problème bien connu auquel l'OP fait référence est que le shell créera tous les fichiers nécessaires au fonctionnement des canaux avant même de commencer à exécuter les commandes dans le pipeline, c'est donc le shell qui tronque réellement le fichier de sortie (qui est malheureusement aussi le fichier d'entrée) avant même que l'une des commandes ait eu la chance de commencer à s'exécuter.
La commande tee
ne fonctionne pas, même si elle satisfait à la première condition, car elle ne satisfait pas à la seconde: elle crée toujours le fichier de sortie dès le démarrage, ce qui est aussi grave que la création d'un tuyau dans le fichier de sortie. (En réalité, il est pire, car son utilisation introduit un délai aléatoire non déterministe avant que le fichier de sortie ne soit tronqué. Vous pourriez donc penser que cela fonctionne, alors qu'en fait, il ne fonctionne pas.)
Donc, tout ce dont nous avons besoin pour résoudre ce problème est une commande qui tampon toutes ses entrées avant de produire une sortie, et capable d'accepter le nom de fichier en sortie en tant que paramètre, de sorte que nous n'ayons pas à canaliser sa sortie vers le fichier de sortie. Une de ces commandes est shuf
. Donc, ce qui suit accomplira la même chose que sponge
:
shuf --output=file --random-source=/dev/zero
La partie --random-source=/dev/zero
joue des tours shuf
pour faire ce qu'elle a à faire, sans rien mélanger, afin de mettre en mémoire tampon votre entrée sans la modifier.
Vous pouvez utiliser Vim en mode Ex:
ex -sc '%!some_sed_command' -cx filename
%
sélectionner toutes les lignes
!
Exécuter la commande
x
Sauvegarder et quitter