O script abaixo é uma ilustração mínima (embora artificial) do problema.
## demo.sh
exitfn () {
printf -- 'exitfn PID: %d\n' "$$" >&2
exit 1
}
printf -- 'script PID: %d\n' "$$" >&2
exitfn | :
printf -- 'SHOULD NEVER SEE THIS (0)\n' >&2
exitfn
printf -- 'SHOULD NEVER SEE THIS (1)\n' >&2
Neste script de exemplo, exitfn
representa uma função cujo trabalho envolve encerrar o processo atual 1 .
Infelizmente, conforme implementado, exitfn
não cumpre essa missão de forma confiável.
Se alguém executar este script, a saída será assim:
% bash ./demo.sh
script PID: 26731
exitfn PID: 26731
SHOULD NEVER SEE THIS (0)
exitfn PID: 26731
(Claro, o valor mostrado para PID será diferente com cada chamada.)
O ponto-chave aqui é que, na primeira invocação da exitfn
função, o exit 1
comando em seu corpo falha ao encerrar a execução do script de inclusão (como evidenciado pela execução do primeiro printf
comando imediatamente a seguir). Em contraste, na segunda invocação de exitfn
, esse exit 1
comando encerra a execução do script (como evidenciado pelo fato de que o segundo printf
comando não é executado).
A única diferença entre as duas invocações de exitfn
é que a primeira ocorre como o primeiro componente de um pipeline de dois componentes, enquanto a segunda é uma chamada "independente".
Estou intrigado com isso. Eu esperava que exit
isso tivesse o efeito de matar o processo atual (ou seja, aquele com PID fornecido por $$
). Obviamente, isso nem sempre é verdade.
Seja como for, existe uma maneira de escrever exitfn
para que ele saia do script ao redor, mesmo quando invocado em um pipeline?
Aliás, o script acima também é um script zsh válido e produz o mesmo resultado:
% zsh ./demo.sh
script PID: 26799
exitfn PID: 26799
SHOULD NEVER SEE THIS (0)
exitfn PID: 26799
Eu estaria interessado na resposta a esta pergunta para zsh também.
Finalmente, devo salientar que implementar exitfn
assim não funciona:
exitfn () {
printf -- 'exitfn PID: %d\n' "$$" >&2
exit 1
kill -9 "$$"
}
...porque, em todas as circunstâncias, o exit 1
comando é sempre a última linha daquela função que é executada. ( Substituir exit 1
por kill -9 $$
não é aceitável: quero controlar o status de saída do script e sua saída para stderr.)
1 Na prática, essa função executaria outras tarefas, como log de diagnóstico ou operações de limpeza, antes de encerrar o processo atual.
“O processo atual” não é a mesma coisa que “o processo com PID dado por
$$
”.exit
sai do subshell atual ou do shell original se não for chamado em um subshell.Certas construções, como o conteúdo de
( … )
(agrupamento com subshell), substituições de comando ($(…)
ou`…`
) e cada lado de um pipe (ou apenas o lado esquerdo em alguns shells), são executados em um subshell. Um subshell se comporta como se fosse um processo separado criado comfork()
, e geralmente é implementado dessa maneira (alguns shells não usam subprocessos em algumas circunstâncias, como otimização de desempenho). O subshell tem sua própria cópia de variáveis, seus próprios redirecionamentos, etc. A chamadaexit
sai do subshell, assim como aexit()
função na biblioteca C padrão sai do processo. Para saber mais sobre subshells, consulte O que é um subshell (no contexto da documentação do make)? , Qual é a diferença exata entre um "subshell" e um "processo filho"?e $() é um subshell? .$$
é sempre o ID do processo do shell original. Ele não muda em um subshell. Alguns shells têm uma variável que muda em um subshell, por exemplo$BASHPID
em bash e mksh, ou${.sh.subshell}
em ksh ou$ZSH_SUBSHELL
ou$sysparams[pid]
em zsh¹.Não exatamente. Você precisará trabalhar mais, no ponto em que o script cria o subshell, no nível superior ou em ambos. Consulte o script de shell de saída de um subshell para obter aproximações.
¹ cuidado, porém, ao contrário de outros shells,
zsh
realiza expansões em pipelines no processo pai (da esquerda para a direita), não em cada um dos membros dos pipelines:zsh -c 'echo $ZSH_SUBSHELL | cat'
saídas0
(mesmo que sejaecho
executada em um processo filho) ezsh -c 'n=0; echo $((++n)) | echo $((++n))'
saídas 2.