Redirecionamento de E / S e o comando head

9

Hoje eu estava tentando editar rapidamente um .hgignorearquivo do shell bash do Cygwin e adicionei uma linha que era um erro. Não tenho certeza se essa era a melhor maneira de fazê-lo, mas rapidamente pensei em usar head -1 .hgignorepara remover a linha incorreta (anteriormente eu só tinha uma linha no arquivo). Com certeza, quando executado, fornece a primeira linha como a única saída.

Mas quando tentei redirecionar a saída e reescrever o arquivo usando head -1 .hgignore > .hgignore, o arquivo estava vazio. Por que isso acontece? Se eu tentar acrescentar head -1 .hgignore >> .hgignore, ele será anexado corretamente, mas esse obviamente não é o resultado desejado. Por que um redirecionamento truncado não funciona nesse caso?

voithos
fonte

Respostas:

10

Quando o shell obtém uma linha de comando como: command > file.outo próprio shell abre (e talvez crie) o arquivo chamadofile.out . O shell define o descritor de arquivo 0 para o descritor de arquivo obtido desde o início. É assim que o redirecionamento de E / S funciona: todo processo conhece os descritores de arquivo 0, 1 e 2.

A parte mais difícil disso é como abrir file.out. Na maioria das vezes, você deseja file.outabrir para gravação no deslocamento 0 (ou seja, truncado) e é isso que o shell fez por você. Ele truncou .hgignore, abriu-o para gravação, duplicou o filedescriptor para 0 e depois executou head. Ficheiros instantâneos.

No shell bash, você faz um set noclobberpara alterar esse comportamento.

Bruce Ediger
fonte
Aha eu vejo. Eu achava que o shell estava truncando o arquivo antes de executar o comando, mas não sabia o porquê. Obrigada pelo esclarecimento!
voithos
10

Eu acho que Bruce responde o que está acontecendo aqui com o oleoduto.

Um dos meus pequenos utilitários favoritos é o spongecomando do moreutils . Ele resolve exatamente esse problema "absorvendo" toda a entrada disponível antes de abrir o arquivo de saída de destino e gravar os dados. Ele permite que você escreva pipelines exatamente como você esperava:

$ head -1 .hgignore | sponge .hgignore

A solução do pobre homem é canalizar a saída para um arquivo temporário; depois que o pipline for concluído (por exemplo, o próximo comando que você executar), será movido o arquivo temporário de volta ao local original do arquivo.

$ head -1 .hgingore > .hgignore.tmp
$ mv .hgignore{.tmp,}
Caleb
fonte
Olhando para isso alguns anos depois, um pensamento me ocorreu: não poderíamos simplesmente fazer head -1 .hgignore | tee .hgignore? teeestá em coreutils, e como um efeito colateral regalia /, este também escreve para STDOUT
voithos
@voithos Até onde eu sei, teeabre e trunca o arquivo para o qual está gravando quando é instanciado, como todo o resto, para que não resolva o problema principal da condição de corrida na leitura do conteúdo do arquivo antes de você truncá-lo com a gravação.
Caleb
Você mencionou um ponto que eu não sabia, na verdade - a saber, que os comandos canalizados são iniciados imediatamente, em vez de sequencialmente. Isso é preciso? No entanto, eu testei e tee parece fazer a coisa desejada. Eu tenho versão 8.13na minha máquina.
voithos 28/03
11
@voithos Yes comandos em um pipline e todos os canais de entrada / saída envolvidos são iniciados na ordem inversa para que o pipeline esteja pronto para receber dados quando o primeiro começar a fornecê-los. Suspeito que seu teste seja defeituoso porque você provavelmente usou um pedaço de dados muito pequeno e ele armazenou tudo em cache em um buffer de leitura antes de precisar. O teeprograma truncará seus arquivos, não está configurado para duplicá-los.
Caleb
3

No

head -n 1 file > file

fileé truncado antes de headser iniciado, mas se você o escrever:

head -n 1 file 1<> file

não fileé como é aberto no modo de leitura e gravação. No entanto, quando headtermina a gravação, ele não trunca o arquivo, portanto a linha acima não funcionaria ( headapenas reescreveria a primeira linha sobre si mesma e deixaria as outras intocadas).

No entanto, após o headretorno e enquanto o fdainda estiver aberto, você pode chamar outro comando que faça o truncate.

Por exemplo:

{ head -n 1 file; perl -e 'truncate STDOUT, tell STDOUT'; } 1<> file

O que importa aqui é que truncateacima,head apenas move o cursor para fd 1 dentro do arquivo logo após a primeira linha. Ele reescreve a primeira linha da qual não precisamos, mas isso não é prejudicial.

Com uma cabeça POSIX, poderíamos fugir sem reescrever a primeira linha:

{ head -n 1 > /dev/null
  perl -e 'truncate STDIN, tell STDIN'
} <> file

Aqui, estamos usando o fato de headmover a posição do cursor em seu stdin. Embora headnormalmente lesse sua entrada por grandes partes para melhorar o desempenho, o POSIX exigiria (sempre que possível) queseek retornasse logo após a primeira linha, se a ultrapassasse. Observe, no entanto, que nem todas as implementações fazem isso.

Como alternativa, você pode usar o readcomando do shell neste caso:

{ read -r dummy; perl -e 'truncate STDIN, tell STDIN'; } <> file
Stéphane Chazelas
fonte
11
Stephane, você conhece um comando padrão ou coreutils que pode truncar STDINsemelhante ao que você realizou usando perlacima
Iruvar
2
@ 1_CR, não. ddpode truncar em qualquer deslocamento absoluto arbitrário no arquivo. Assim, você pode determinar o deslocamento da segunda linha e truncado de lá com bytedd bs=1 seek="$offset" of=file
Stéphane Chazelas
1

A solução do homem real é

ed .hgignore
$d
wq

ou como one-liner

printf '%s\n' '$d' 'wq' | ed .hgignore

Ou com o GNU sed:

sed -i '$d' .hgignore

(Não, estou brincando. Eu usaria um editor interativo. vi .hgignore GddZZ)

Gilles 'SO- parar de ser mau'
fonte
Eu me perguntei, há alguma vantagem em usar :wqmais ZZ?
voithos
Além disso, :xque é o que os meus dedos fazer automaticamente
Glenn Jackman
e ZQé o mesmo que:q!
glenn jackman 30/06/11
ZZ e: x escrevem apenas se houver algo para escrever ...: w sempre sincroniza o arquivo no disco, independentemente de ser necessário. Eu uso: xa porque uso guias.
Xenoterracide
1

Você pode usar o Vim no modo Ex:

ex -sc '2,d|x' .hgignore
  1. 2, selecione as linhas 2 até o final

  2. d excluir

  3. x salvar e fechar

Steven Penny
fonte
0

Para edição de arquivo no local, você também pode usar o truque de manipulação de arquivo aberto, como mostra Jürgen Hötzel na saída Redirect do sed 's / c / d /' myFile para o myFile .

exec 3<.hgignore
rm .hgignore  # prevent open file from being truncated
head -1 <&3 > .hgignore

ls -l .hgignore  # note that permissions may have changed
dan55
fonte
2
E logo após o rm .hgignoreseu poder falhar, gastando horas de trabalho duro. Ok, não importa .hgignore, mas por que você faria algo tão complicado assim? Assim, meu voto negativo: tecnicamente correto, mas uma péssima idéia.
Gilles 'SO- stop be evil'
@Gilles, talvez não seja uma idéia tão boa, mas é por exemplo o que perl -i(para edição local) faz, e não ficaria surpreso se algumas implementações sed -ifizessem isso também (embora a versão mais recente do GNU sedpareça não).
Stéphane Chazelas