Dado o seguinte código:
out="$(mktemp)"
rm -f "$out"
clear
printf '%s\n' 0 >"$out"
{
printf '%s\n' '1' >/dev/stdout
printf '%s\n' '2' >/dev/stdout
} >"$out"
cat -e -- "$out"
rm -f "$out"
No Ubuntu isso produz:
2$
No MacOS isso gera:
1$
2$
Ao anexar explicitamente, eles se comportam de forma consistente:
out="$(mktemp)"
rm -f "$out"
clear
printf '%s\n' 0 >"$out"
{
printf '%s\n' '1' >/dev/stdout
printf '%s\n' '2' >>/dev/stdout
} >"$out"
cat -e -- "$out"
rm -f "$out"
No MacOS e no Ubuntu isso gera:
1$
2$
O exemplo mais confuso para mim é este:
out="$(mktemp)"
rm -f "$out"
clear
printf '%s\n' 0 >"$out"
exec 3>>"$out"
{
printf '%s\n' '1' >/dev/stdout
printf '%s\n' '2' >/dev/stdout
} >&3
{
printf '%s\n' '3' >/dev/stdout
printf '%s\n' '4' >/dev/stdout
} >&3
cat -e -- "$out"
rm -f "$out"
exec 3>&-
Que no MacOS gera:
0$
1$
2$
3$
4$
Que no Ubuntu gera:
4$
Eu esperava isso no Ubuntu:
0$
2$
4$
Estou completamente confuso sobre o porquê desse comportamento ocorrer neste exemplo e em todos os outros exemplos que criei para ilustrar essa discrepância.
Minhas perguntas:
- O que é essa discrepância? O que está acontecendo? Essa discrepância é intencional?
- Onde mais essa discrepância se aplica? Quais são suas origens?
- Se essa discrepância é intencional, por que foi justificada? Qual deveria ser o comportamento correto?
- O que pode ser feito para atenuar essas diferenças ao escrever scripts entre sistemas operacionais?
- É
shopt -o noclobber
a resposta apropriada? É essa a verdadeira necessidade denoclobber
?
Então, em
alguém poderia realmente esperar que o redirecionamento
> /dev/stdout
não fizesse nada, já que o que>
faz é redirecionar stdout , e redirecionar stdout para stdout realmente parece ser uma operação nula. Então parece que deveria ser o mesmo que isto:onde ambas as cópias de printf herdam fds que apontam para a mesma descrição de arquivo aberto (OFD), e como tal têm uma posição de gravação compartilhada. O que significa que as gravações da segunda começam de onde a primeira parou e a saída é gravada no arquivo em ordem natural.
Ou seja, na maioria dos sistemas, a abertura
/dev/stdout
atua como uma chamadadup()
ao stdout (fd 1).Mas esse não é o caso no Linux! Em vez disso, no Linux, a abertura
/dev/stdout
(ou qualquer uma das similares) encontra o arquivo subjacente e o abre novamente. O novo descritor de arquivo (fd) aponta para um novo OFD, com a posição de leitura/gravação e o modo de gravação (anexar vs. normal) independente do original. E ele trunca o arquivo se estiver usando>
.Então acima, o shell abre
outputfile
para as chaves, obtém um fd apontando para um OFD com posição de gravação 0, truncando o arquivo. Entãooutputfile
é aberto novamente para o primeiroprintf
, novamente na posição de gravação 0; e entãooutputfile
é aberto novamente para o segundoprintf
, novamente na posição de gravação 0, truncando-o. A saída escrita pelo primeiro printf é perdida e a saída do segundo printf é tudo o que resta.É o mesmo que
Quando você faz
em vez disso, o OFD para o segundo printf é aberto no modo append (não truncando), o que significa que todas as gravações através dele sempre vão para o final do arquivo.
Da mesma forma, aqui:
o shell abre
outfile
na posição de gravação 0 no modo append para o exec; então ele abreoutfile
novamente através de/dev/stdout
, no Linux truncando o arquivo e obtendo um OFD na posição de gravação 0 no modo normal não append, então faz isso de novo, depois de novo, depois de novo. As gravações de todos, exceto o último printf, são perdidas.Em outros sistemas operacionais, isso ocorre novamente sem os
> /dev/stdout
redirecionamentos, e todos os dados são gravados em ordem no arquivo.Se você costuma
1<> /dev/stdout
solicitar uma abertura de leitura e gravação não truncada, você verá parte dos dados sendo substituídos desde o início:no Linux, que resulta em
outfile
conter(No macOS, isso requer que o redirecionamento externo também solicite uma abertura de leitura e gravação (ou seja,
1<> outfile
), o que é bastante sensato, já que você não pode transformar um OFD somente de gravação em um de leitura e gravação sem reabrir.)Essa é a discrepância. Eu esperaria que fosse apenas um acidente histórico do Linux sempre ter feito isso por alguma razão, e agora não pode ser alterado. Não sei ao certo, no entanto.
Qual é o comportamento correto? Bem, se um SO faz X e muitos outros fazem Y, pode-se dizer que Y é pelo menos uma maneira mais aceita. Mas não há autoridade para decidir em geral o que vários SOs podem fazer. AFAIU, POSIX apenas codifica o que o consenso já é, e não menciona
/dev/stdout
e tal, possivelmente por causa de variações como esta... (De todos os Unix-likes, não tenho ideia se algum outro também faz algo diferente wrt./dev/stdout
. Mas eu entendo que pelo menos os BSDs estão alinhados entre si, com o Linux sendo diferente.)Isso depende do que você está tentando alcançar (e você não disse), mas, para começar, não use
/dev/stdout
.Especialmente naquele primeiro exemplo, o redirecionamento para
/dev/stdout
é redundante, como mencionado acima. Se você quiser escrever para o mesmo stdout, apenas descarte o redirecionamento. Se você quiser abrir o arquivo novamente, faça isso em vez disso:Ou, talvez o caso mais provável seja que você tenha um programa que requer um nome de arquivo como saída (e não suporta, por exemplo,
-
stdout) e, portanto, tem que usar/dev/stdout
. Então, use-o apenas para escrever em um pipe ou algo assim. Pipes não podem ser procurados, então não têm uma posição de escrita, e o modo append vs. normal não faz diferença quando tudo vai para o fim do pipe de qualquer maneira.Então isso:
deve funcionar da mesma forma no Linux como no macOS e outros.
Ou com gatos individuais (bobo aqui, mas talvez útil em alguns casos):