Linux: Comment utiliser un fichier en entrée et en sortie simultanément?

55

Je viens de lancer le suivant dans Bash:

uniq .bash_history > .bash_history

et mon fichier d’histoire s’est retrouvé complètement vide.

Je suppose que j'ai besoin d'un moyen de lire le fichier en entier avant de l'écrire. Comment est-ce fait?

PS: J'ai évidemment pensé à utiliser un fichier temporaire, mais je cherche une solution plus élégante.

MilliaLover
la source
C'est parce que les fichiers sont ouverts de droite à gauche. Voir aussi stackoverflow.com/questions/146435/…
WheresAlice
Vous devez écrire la sortie dans un nouveau fichier du même répertoire et le renommer par-dessus l'ancien fichier. Toute autre approche risquerait de perdre vos données si elles étaient interrompues à mi-parcours. Certains outils peuvent vous cacher cette étape.
Kasperd
Ou, bashne mettra pas de dupes consécutives dans son historique si vous définissez HISTCONTROL pour inclure les ignoredups; voir la page de manuel.
dave_thompson_085
veuillez envisager de changer la réponse à celle-ci. serverfault.com/a/557566/130392
23inhouse

Réponses:

49

Je recommande d'utiliser spongede moreutils . De la page de manuel:

DESCRIPTION
  sponge  reads  standard  input  and writes it out to the specified file. Unlike
  a shell redirect, sponge soaks up all its input before opening the output file.
  This allows for constructing pipelines that read from and write to the same 
  file.

Pour appliquer cela à votre problème, essayez:

uniq .bash_history | sponge .bash_history
Jldugger
la source
6
C'est comme un chat, mais avec des capacités de succion: D
MilliaLover
77

Je voulais simplement apporter une autre réponse simple, qui n'utilise pas d'éponge (car souvent, elle n'est pas incluse dans les environnements légers).

echo "$(uniq .bash_history)" > .bash_history

devrait avoir le résultat souhaité. Le sous-shell est exécuté avant que .bash_history ne soit ouvert en écriture. Comme expliqué dans la réponse de Phil P, au moment où .bash_history est lu dans la commande d'origine, il a déjà été tronqué par l'opérateur '>'.

Hart Simha
la source
15
Je ne suis normalement pas un fan des réponses qui soulèvent une question ancienne qui a déjà une réponse valide et acceptée - mais celle-ci est élégante, bien écrite et plaide avec force pour sa nécessité (environnements légers); pour moi, cela ajoute vraiment quelque chose à l'ensemble de réponses existant. Bienvenue à SF, Hart (vous êtes ici depuis un mois, mais je pense qu'il s'agit de votre premier poste important). J'espère lire plus de réponses de votre part comme celle-ci!
MadHatter soutient Monica
4
C'est la meilleure solution. J'ai dû utiliser un sous-shell $()au lieu de backticks en raison de problèmes qui échappaient.
CMCDragonkai
3
Je me demande si cette solution s'adapte aux fichiers volumineux, disons ... 20 ou 50 Go.
Amit Naidu
1
Cela devrait vraiment être la réponse acceptée.
maxywb
1
J'ai utilisé cette réponse à faire echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txtaujourd'hui. Je cherchais depuis longtemps une solution élégante. Merci beaucoup, @Hart Simha!
shredalert le
12

Le problème est que votre shell est en train de configurer le pipeline de commandes avant d'exécuter les commandes. Ce n'est pas une question "d'entrée et de sortie", c'est que le contenu du fichier est déjà parti avant même que l'uniq ne s'exécute. Cela va quelque chose comme:

  1. Le shell ouvre le >fichier de sortie en écriture, le tronquant
  2. Le shell est configuré pour que le descripteur de fichier 1 (pour stdout) soit utilisé pour cette sortie
  3. Le shell exécute uniq, peut-être quelque chose comme execlp ("uniq", "uniq", ".bash_history", NULL)
  4. Uniq s'exécute, ouvre .bash_history et n'y trouve rien

Il existe différentes solutions, y compris l'édition en place et l'utilisation de fichiers temporaires dont d'autres ont parlé, mais l'essentiel est de comprendre le problème, ce qui ne va vraiment pas et pourquoi.

Phil P
la source
10

Une autre astuce pour ce faire, sans utiliser sponge, est la commande suivante:

{ rm .bash_history && uniq > .bash_history; } < .bash_history

C'est l'une des astuces décrites dans l'excellent article « Éditer à la place» des fichiers sur backreference.org.

Fondamentalement, il ouvre le fichier en lecture, puis le "supprime". Ce n'est pas vraiment supprimé, cependant: il y a un descripteur de fichier ouvert pointant vers lui, et tant qu'il reste ouvert, le fichier est toujours là. Ensuite, il crée un nouveau fichier portant le même nom et y écrit les lignes uniques.

Inconvénient de cette solution: Si vous uniqéchouez pour une raison quelconque, votre historique disparaîtra.

scy
la source
6

utiliser une éponge de moreutils

uniq .bash_history | sponge .bash_history
Justin
la source
3

Ce sedscript supprime les doublons adjacents. Avec l' -ioption, la modification est effectuée sur place. C'est du sed infofichier:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history
Dennis Williamson
la source
sed utilise toujours le fichier temporaire, ajoute une réponse avec straceillustration (pas que cela compte vraiment) :-)
Kyle Brandt
3
@ Kyle: C'est vrai, mais "loin des yeux, loin du coeur". Personnellement, j'utiliserais le fichier temporaire explicite, car quelque chose comme process input > tmp && mv tmp inputest beaucoup plus simple et lisible que d'utiliser la sedruse simplement pour éviter un fichier temporaire et il ne remplacera pas mon fichier original s'il échoue (je ne sais pas si cela sed -iéchoue normalement). pense que ce serait bien). De plus, vous pouvez faire beaucoup de choses avec la méthode sortie-dans-fichier temporaire qui ne peut pas être faite sur place sans quelque chose d'encore plus complexe que ce sedscript. Je sais que vous savez tout cela, mais certains spectateurs pourraient en bénéficier.
Dennis Williamson
3

Sed utilise également un fichier temporaire (ceci le fait pour vous):

$ strace sed -i 's/foo/bar/g' foo    
open("foo", O_RDONLY|O_LARGEFILE)       = 3
...
open("./sedPmPv9z", O_RDWR|O_CREAT|O_EXCL|O_LARGEFILE, 0600) = 4
...
read(3, "foo\n"..., 4096)               = 4
write(4, "bar\n"..., 4)                 = 4
read(3, ""..., 4096)                    = 0
close(3)                                = 0
close(4)                                = 0
rename("./sedPmPv9z", "foo")            = 0
close(1)                                = 0
close(2)                                = 0

Description: le fichier
temporaire ./sedPmPv9zdevient le foofichier 4 et les fichiers le fichier 3. Les opérations de lecture se font sur le disque 3 et les écritures sur le disque 4 (le fichier temporaire). Le fichier foo est alors remplacé par le fichier temporaire de l'appel de changement de nom.

Kyle Brandt
la source
1

Une autre solution:

uniq file 1<> líneas.txt
Antonio Lebrón
la source
0

Un fichier temporaire est à peu près tout, sauf si la commande en question prend en charge l’édition sur place ( uniqne le fait pas - certains sedfont ( sed -i)).

Douglas Leeder
la source
0

Vous pouvez utiliser Vim en mode Ex:

ex -sc '%!uniq' -cx .bash_history
  1. % sélectionner toutes les lignes

  2. ! commande d'exécution

  3. x sauver et fermer

Steven Penny
la source
-1

Vous pouvez également utiliser tee, en utilisant la sortie uniq comme entrée:

uniq .bash_history | tee .bash_history
Saul Vargas
la source
Non, vous ne pouvez pas le faire askubuntu.com/a/752451
Steven Penny