Linux: Como usar um arquivo como entrada e saída ao mesmo tempo?


55

Acabei de executar o seguinte no bash:

uniq .bash_history > .bash_history

e meu arquivo de histórico acabou completamente vazio.

Acho que preciso de uma maneira de ler o arquivo inteiro antes de escrever nele. Como isso é feito?

PS: Obviamente, pensei em usar um arquivo temporário, mas estou procurando uma solução mais elegante.


É porque os arquivos são abertos da direita para a esquerda. Veja também stackoverflow.com/questions/146435/…
WheresAlice

Você precisa gravar a saída em um novo arquivo no mesmo diretório e renomeá-lo na parte superior do arquivo antigo. Qualquer outra abordagem corre o risco de perder seus dados se eles forem interrompidos na metade. Algumas ferramentas podem ocultar esta etapa de você.
kasperd

Ou, bashnão colocará dupes consecutivos em seu histórico se você configurar HISTCONTROL para incluir ignoredups; veja a página de manual.
Dave_thompson_085

Respostas:


49

Eu recomendo usar spongefrom moreutils . Na página de manual:

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.

Para aplicar isso ao seu problema, tente:

uniq .bash_history | sponge .bash_history

6
É como gato, mas com sugando recursos: D
MilliaLover

77

Eu só queria contribuir com outra resposta que seja simples e não use esponja (porque geralmente não é incluída em ambientes leves).

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

deve ter o resultado desejado. O subshell é executado antes que o .bash_history seja aberto para gravação. Conforme explicado na resposta de Phil P, no momento em que .bash_history é lido no comando original, ele já foi truncado pelo operador '>'.


15
Normalmente, não sou fã de respostas que tragam uma pergunta antiga que já tem uma resposta válida e aceita - mas isso é elegante, bem escrito e faz um forte argumento para sua necessidade (ambientes leves); para mim, realmente adiciona algo ao conjunto de respostas existente. Bem-vindo ao SF, Hart (você está aqui há um mês, mas acho que esta é sua primeira postagem substantiva). Espero ler mais respostas suas como esta!
21413 MadHatter

4
essa é a melhor solução. Eu tive que usar um subshell em $()vez de backticks devido a alguns problemas de escape.
precisa saber é o seguinte

3
Gostaria de saber se esta solução é dimensionada para arquivos grandes, digamos ... 20 ou 50 GB.
Amit Naidu 29/11

11
Essa realmente deve ser a resposta aceita.
maxywb

11
Eu usei esta resposta para fazer echo "$(fmt -p '# ' -w 50 readme.txt)" > readme.txthoje. Estava procurando há muito tempo uma solução elegante. Muito obrigado, @Hart Simha!
shredalert 6/06

12

O problema é que seu shell está configurando o pipeline de comandos antes de executar os comandos. Não se trata de "entrada e saída", é que o conteúdo do arquivo já desapareceu antes da execução do uniq. É algo como:

  1. O shell abre o >arquivo de saída para gravação, truncando-o
  2. O shell é configurado para que o descritor de arquivo 1 (para stdout) seja usado para essa saída
  3. O shell executa uniq, talvez algo como execlp ("uniq", "uniq", ".bash_history", NULL)
  4. O uniq roda, abre .bash_history e não encontra nada lá

Existem várias soluções, incluindo a edição no local e o uso temporário de arquivos que outros mencionam, mas a chave é entender o problema, o que realmente está errado e o porquê.


9

Outro truque para fazer isso, sem usar sponge, é o seguinte comando:

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

Esse é um dos truques descritos no excelente artigo "Edição no local" de arquivos em backreference.org.

Basicamente, ele abre o arquivo para leitura e depois o "remove". Porém, ele não é realmente removido: existe um descritor de arquivo aberto apontando para ele e, enquanto ele permanecer aberto, o arquivo ainda está por aí. Em seguida, ele cria um novo arquivo com o mesmo nome e grava as linhas exclusivas nele.

Desvantagem desta solução: se uniqfalhar por algum motivo, seu histórico desaparecerá.



3

Este sedscript remove duplicatas adjacentes. Com a -iopção, ele faz a modificação no local. É do sed infoarquivo:

sed -i 'h;:b;$b;N;/^\(.*\)\n\1$/ {g;bb};$b;P;D' .bash_history

sed ainda usa o arquivo temporário, acrescentou uma resposta com a straceilustração (não que realmente importa) :-)
Kyle Brandt

3
@ Kyle: Verdade, mas "fora da vista, fora da mente". Pessoalmente, eu usaria o arquivo temporário explícito, já que algo como process input > tmp && mv tmp inputé muito mais simples e mais legível do que usar sedtruques para evitar um arquivo temporário e ele não substituirá o original se falhar (não sei se sed -ifalha normalmente - eu usaria acho que sim). Além disso, há muitas coisas que você pode fazer com o método de saída para arquivo temporário que não pode ser feito no local sem algo ainda mais envolvido que esse sedscript. Eu sei que você sabe tudo isso, mas pode beneficiar alguns espectadores.
Dennis Williamson

3

Como um boato interessante, o sed também usa um arquivo temporário (isso serve apenas para você):

$ 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

Descrição:
o arquivo temporário ./sedPmPv9zse torna fd 4 e os fooarquivos se tornam fd 3. As operações de leitura estão no fd 3 e as gravações no fd 4 (o arquivo temporário). O arquivo foo é substituído pelo arquivo temporário na chamada de renomeação.



0

Um arquivo temporário é praticamente o mesmo, a menos que o comando em questão suporte a edição no local ( uniqnão - alguns sedfazem ( sed -i)).


0

Você pode usar o Vim no modo Ex:

ex -sc '%!uniq' -cx .bash_history
  1. % selecione todas as linhas

  2. ! comando de execução

  3. x salvar e fechar


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.