Lendo e gravando um arquivo: comando tee

10

É sabido que um comando como este:

cat filename | some_sed_command >filename

apaga o nome do arquivo, pois o redirecionamento de saída, sendo executado antes do comando, faz com que o nome do arquivo seja truncado.

Pode-se resolver o problema da seguinte maneira:

cat file | some_sed_command | tee file >/dev/null

mas não tenho certeza se isso funcionaria em qualquer caso: o que acontece se o arquivo (e o resultado do comando sed) for muito grande? Como o sistema operacional pode evitar substituir algum conteúdo que ainda não foi lido? Vejo que também existe um comando de esponja que deve funcionar em qualquer caso: é "mais seguro" que o tee?

VeryHardCoder
fonte
Qual é o seu objetivo principal? (em termos simples)
Sergiy Kolodyazhnyy 01/04
@Serg simplesmente entender como as coisas funcionam ... A resposta escrito por kos esclarece o assunto
VeryHardCoder

Respostas:

10

Pode-se resolver o problema da seguinte maneira:

cat file | some_sed_command | tee file >/dev/null

Não .

As chances fileserão truncadas, mas não há garantia cat file | some_sed_command | tee file >/dev/nullque não será truncada file.

Tudo depende de qual comando é processado primeiro, ao contrário do que se pode esperar, os comandos em um canal não são processados ​​da esquerda para a direita . Não há garantia sobre qual comando será escolhido primeiro; portanto, pode-se pensar nele como escolhido aleatoriamente e nunca confiar que o shell não escolha o ofensor.

Como as chances de o comando incorreto ser escolhido primeiro entre três comandos são menores do que as chances de o comando incorreto ser escolhido primeiro entre dois comandos, é menos provável que fileseja truncado, mas ainda vai acontecer .

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

Portanto, nunca use algo como cat file | some_sed_command | tee file >/dev/null. Use spongecomo Oli sugeriu.

Como alternativa, para ambientes mais sofisticados e / ou arquivos relativamente pequenos, pode-se usar uma string here e uma substituição de comando para ler o arquivo antes que qualquer comando seja executado:

$ cat file
foo
bar
$ for ((i=0; i<100; i++)); do <<<"$(<file)" sed 's/bar/baz/' >file; done
$ cat file
foo
baz
kos
fonte
9

Para sedespecificamente, você pode usar seu -iargumento no local. Ele apenas salva de volta no arquivo que abriu, por exemplo:

sed -i 's/ /-/g' filename

Se você quiser fazer algo mais robusto, supondo que você esteja fazendo mais do que sed, sim, você pode armazenar a coisa toda com sponge(do moreutilspacote) que "absorverá" todo o stdin antes de gravar no arquivo. É como, teemas com menos funcionalidade. Para uso básico, porém, é praticamente uma substituição imediata:

cat file | some_sed_command | sponge file >/dev/null

Isso é mais seguro? Definitivamente. Provavelmente, ele tem limites; portanto, se você estiver fazendo algo colossal (e não puder editar no local com o sed), convém fazer as edições em um segundo arquivo e depois mvo arquivo de volta ao nome do arquivo original. Isso deve ser atômico (para que tudo, dependendo desses arquivos, não seja interrompido se eles precisarem de acesso constante).

Oli
fonte
0

Você pode usar o Vim no modo Ex:

ex -sc '%!some_sed_command' -cx filename
  1. % selecione todas as linhas

  2. ! Comando de execução

  3. x Salvar e sair

Steven Penny
fonte
0

Ah, mas spongenão é a única opção; você não precisa obtê moreutils-lo para que isso funcione corretamente. Qualquer mecanismo funcionará desde que atenda aos dois requisitos a seguir:

  1. Ele aceita o nome do arquivo de saída como um parâmetro.
  2. Ele só cria o arquivo de saída quando todas as entradas foram processadas.

Veja bem, o problema bem conhecido ao qual o OP está se referindo é que o shell criará todos os arquivos necessários para que os pipes funcionem antes mesmo de começar a executar os comandos no pipeline, portanto, é o shell que realmente trunca o arquivo de saída (que infelizmente também é o arquivo de entrada) antes que qualquer um dos comandos tivesse a chance de começar a executar.

O teecomando não funciona, embora atenda ao primeiro requisito, porque não atende ao segundo requisito: ele sempre criará o arquivo de saída imediatamente após a inicialização, portanto é tão ruim quanto criar um canal direto para o arquivo de saída. (Na verdade, é pior, porque seu uso introduz um atraso aleatório não determinístico antes que o arquivo de saída seja truncado; portanto, você pode pensar que ele funciona, enquanto na verdade não funciona.)

Portanto, tudo o que precisamos para resolver esse problema é algum comando que armazene em buffer todas as suas entradas antes de produzir qualquer saída e que seja capaz de aceitar o nome do arquivo de saída como parâmetro, para que não tenhamos que canalizar sua saída para o arquivo de saída. Um desses comandos é shuf. Portanto, o seguinte realizará a mesma coisa que spongefaz:

    shuf --output=file --random-source=/dev/zero 

A --random-source=/dev/zeroparte engana-se shufa fazer as coisas sem fazer nenhum embaralhamento, de modo a proteger sua entrada sem alterá-la.

Mike Nakis
fonte