Como salvar a localização de destino / dev / stdout em um script bash?

12

Eu tenho um certo script bash, que deseja preservar o /dev/stdoutlocal original antes de substituir o descritor do 1º arquivo por outro local.

Então, naturalmente, eu escrevi algo como

old_stdout=$(readlink -f /dev/stdout)

E não deu certo. Muito rapidamente entendi qual era o problema:

test@ubuntu:~$ echo $(readlink -f /dev/stdout)
/proc/5175/fd/pipe:[31764]
test@ubuntu:~$ readlink -f /dev/stdout
/dev/pts/18

Obvioulsly, $()é executado em um subshell, que é canalizado para o shell pai.

Portanto, a pergunta é: existe uma maneira confiável (com escopo de portabilidade entre distribuições Linux) para salvar o /dev/stdoutlocal como uma string em um script bash?

alexey.e.egorov
fonte
Isso soa um pouco como um problema XY . Qual é o problema subjacente?
Kusalananda
O problema subjacente é um certo script de instalação que é executado em dois modos - silencioso, no qual ele registra toda a saída em arquivo e detalhado, em que não apenas registra em arquivo, mas também imprime tudo no terminal. Mas nos dois modos, o script deseja interagir com o usuário, ou seja, imprimir no terminal e ler a resposta do usuário. Por isso, pensei que a economia /dev/stdoutresolveria o problema com a impressão de mensagens no modo silencioso. A alternativa é redirecionar todas as outras ações que produzem saída, e há várias delas. Aproximadamente 100 vezes mais que as mensagens de interação do usuário.
Alexey.e.egorov
A maneira padrão de interagir com o usuário é imprimir para stderr. É por exemplo, por que os prompts são enviados stderrpor padrão.
Kusalananda
Infelizmente, ele stderrtambém deve ser redirecionado e salvo, pois o script chama vários programas externos e todas as mensagens de erro possíveis devem ser coletadas e registradas.
Alexey.e.egorov

Respostas:

14

Para salvar um descritor de arquivo, você o duplica em outro arquivo fd. Salvar um caminho para o arquivo correspondente não é suficiente; você precisará salvar o modo de abertura, os sinalizadores de abertura, a posição atual no arquivo e assim por diante. E, é claro, para tubos ou soquetes anônimos, que não funcionariam, pois não têm caminho. O que você deseja salvar é a descrição do arquivo aberto a que o fd se refere, e a duplicação de um fd está realmente retornando um novo fd à mesma descrição de arquivo aberto .

Para duplicar um descritor de arquivo em outro, com shell semelhante ao Bourne, a sintaxe é:

exec 3>&1

Acima, o fd 1 é duplicado no fd 3.

Qualquer coisa que o fd 3 já estivesse aberto antes seria encerrada, mas observe que os fds 3 a 9 (geralmente mais, até 99 com yash) são reservados para esse fim (e não têm significado especial contrário a 0, 1 ou 2), o A shell sabe que não deve usá-los para seus próprios negócios internos. A única razão pela qual o fd 3 teria sido aberto anteriormente é porque você fez isso no script 1 ou foi vazado pelo chamador.

Em seguida, você pode alterar o stdout para outra coisa:

exec > /dev/null

E mais tarde, para restaurar o stdout:

exec >&3 3>&-

( 3>&-sendo fechar o descritor de arquivo que não precisamos mais).

Agora, o problema é que, exceto no ksh, todos os comandos que você executar depois exec 3>&1herdarão o fd 3. Esse é um vazamento de fd. Geralmente não é grande coisa, mas isso pode causar problemas.

kshdefine o sinalizador close-on-exec nesses fds (para fds acima de 2), mas nenhum outro shells e outros shells não têm como definir esse sinalizador manualmente.

A solução para outro shell é fechar o fd 3 para cada comando, como:

exec 3>&-

exec > file.log

ls 3>&-
uname 3>&-

exec >&3 3>&-

Pesado. Aqui, a melhor maneira seria não usar exec, mas redirecionar os grupos de comandos:

{
  ls
  uname
} > file.log

Lá, é o shell que cuida de salvar o stdout e restaurá-lo posteriormente (e o faz internamente duplicando-o em um fd (acima de 9, acima de 99 para yash) com o conjunto de sinalizadores close-on-exec ).

Nota 1

Agora, o gerenciamento desses fds 3 a 9 pode ser complicado e problemático se você os usar extensivamente ou em funções, especialmente se o seu script usar algum código de terceiros que por sua vez possa usá-los.

Alguns escudos ( zsh, bash, ksh93, tudo foi adicionado o recurso ( sugerido por Oliver Kiddle dezsh ) em torno do mesmo tempo em 2005 depois de ter sido discutido entre seus desenvolvedores) tem uma sintaxe alternativa para atribuir o primeiro fd livre acima de 10 vez que ajuda neste caso:

myfunction() {
  local fd
  exec {fd}>&1
  # stdout was duplicated onto a new fd above 10, whose actual value
  # is stored in the fd variable
  ...
  # it should even be safe to re-enter the function here
  ...
  exec >&"$fd" {fd}>&-
}
Stéphane Chazelas
fonte
Além disso, seu código está errado no sentido de que o fd 3 já pode ser usado, como acontece quando um script é executado a partir de um rc.localserviço, por exemplo, para que você realmente tenha usado algo parecido exec {FD}>&1ou algo assim. Mas isso é suportado apenas no bash 4, o que é realmente triste. Portanto, isso não é realmente portátil.
Alexey.e.egorov
@ alexey.e.egorov, veja editar.
Stéphane Chazelas
O Bash 3. * não suporta esse recurso, e esta versão é usada no Centos 5, que ainda é suportado e ainda é usado. E encontrar um descritor gratuito e, em seguida, eval "exec $i>&1"é uma coisa que eu gostaria de evitar, devido à sua dificuldade. Posso realmente confiar que fds acima de 9 seriam gratuitos então?
Alexey.e.egorov
@ alexey.e.egorov, não, você está olhando para trás. Os fds 3 a 9 são de uso gratuito (e cabe a você gerenciá-los como quiser) e se destinam a esse fim. fds acima de 9 podem ser usados ​​internamente pelo shell e fechá-los pode ter consequências desagradáveis. A maioria das conchas não permite que você as use. bashpermitirá que você atire no próprio pé.
Stéphane Chazelas
2
@ alexey.e.egorov, se ao iniciar o script tiver alguns fds em (3..9) abertos, isso ocorre porque o chamador esqueceu de fechá-los ou definiu o sinalizador de close-on-exec neles. É o que chamo de vazamento de dados. Agora, talvez o interlocutor pretenda passar esses fds para você, para que você possa ler e / ou gravar dados deles / delas, mas saiba disso. Se você não os conhece, não se importa, então você pode fechá-los livremente (observe que ele apenas fecha o processo fd do seu script, não o do seu interlocutor).
Stéphane Chazelas
3

Como você pode ver, o script bash não é como uma linguagem de programação comum, na qual você pode atribuir descritores de arquivo.

A solução mais simples é usar um sub shell para executar o que você deseja redirecionar, para que o processamento possa ser revertido para o shell superior que possui sua E / S padrão intacta.

Uma solução alternativa seria usar ttypara identificar o dispositivo TTY e controlar a E / S no seu script. Por exemplo:

dev=$(tty)

e então você pode ..

echo message > $dev
Julie Pelletier
fonte
> Uma solução alternativa seria usar tty para identificar o dispositivo TTY e controlar a E / S em seu script. Como se faz isso?
Alexey.e.egorov
1
Acabei de incluir um exemplo na minha resposta.
Julie Pelletier
1

$$ obteria o PID do processo atual; no caso de shell interativo ou script, o PID do shell relevante.

Então você pode usar:

readlink -f /proc/$$/fd/1

Exemplo:

% readlink -f /proc/$$/fd/1
/dev/pts/33

% var=$(readlink -f /proc/$$/fd/1)

% echo $var                       
/dev/pts/33
heemail
fonte
1
Embora seja funcional, depender de uma /procestrutura específica causa problemas de portabilidade, assim como o uso /dev/stdoutmencionado na pergunta.
Julie Pelletier
1
@JuliePelletier Baseando-se em uma /procestrutura específica? Ele iria trabalhar em qualquer Linux que tem procfs..
heemayl
1
Certo, para que possamos generalizar para o Linux como procfsquase sempre está presente, mas geralmente vemos questões de portabilidade e uma boa metodologia de desenvolvimento inclui considerar a portabilidade para outros sistemas. bashpode ser executado em vários sistemas operacionais.
Julie Pelletier