Como escrever uma função que saia com segurança (com um status especificado) do processo atual?

1

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 significa uma função cujo trabalho envolve a finalização do processo atual 1 .

Infelizmente, conforme implementado, exitfn não cumpre esta 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 o PID será diferente a cada invocação.)

O ponto chave aqui é que, na primeira invocação da função exitfn , o comando exit 1 em seu corpo falha ao finalizar a execução do script anexo (como evidenciado pela execução do primeiro comando printf imediatamente seguinte). Em contraste, na segunda invocação de exitfn , esse comando exit 1 encerra a execução do script (como evidenciado pelo fato de que o segundo comando printf 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 invocação "independente".

Estou intrigado com isso. Eu esperava que exit tivesse o efeito de matar o processo atual (ou seja, aquele com PID dado por $$ ). Obviamente, isso nem sempre é verdade.

Seja como for, existe uma maneira de escrever exitfn para que saia do script circundante mesmo quando invocado dentro de um pipeline?

Por acaso, 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 também estaria interessado na resposta a esta pergunta para o zsh.

Por fim, devo salientar que implementar exitfn como essa não funciona de forma alguma:

exitfn () {
    printf -- 'exitfn PID: %d\n' "$$" >&2
    exit 1
    kill -9 "$$"
}

... porque, sob todas as circunstâncias, o comando exit 1 é sempre a última linha dessa função executada. ( Substituindo exit 1 por kill -9 $$ não é aceitável: Eu 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 registro de diagnósticos ou operações de limpeza, antes de finalizar o processo atual.

    
por kjo 23.11.2018 / 19:37

1 resposta

3

I had expected that exit would have the effect of killing the current process (i.e. the one with PID given by $$)

“O processo atual” não é a mesma coisa que “o processo com PID dado por $$ ”. exit sai do subshell , ou o 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 algumas camadas) , corra em uma subcamada. Um subshell se comporta como se fosse um processo separado criado com fork() , e geralmente é implementado dessa forma (alguns shells não usam subprocessos em algumas circunstâncias, como uma otimização de desempenho). O subshell possui sua própria cópia de variáveis, seus próprios redirecionamentos, etc. Chamar exit sai do subshell, assim como a função exit() na biblioteca C padrão sai do processo. Para mais informações sobre sub-unidades, 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 original do shell. Não muda em um subshell. Alguns shells têm uma variável que muda em um subshell, por exemplo $BASHPID no bash e mksh, ou ${.sh.subshell} no ksh ou $ZSH_SUBSHELL ou $sysparams[pid] no zsh¹.

is there a way to write exitfn so that it exits the surrounding script even when invoked within a pipeline?

Não exatamente. Você precisará fazer mais trabalho, seja no ponto em que o script cria subshell ou no nível superior, ou ambos. Consulte saia do shell script de um subshell para aproximações.

¹ Cuidado, embora contrário a outros shells, zsh executa 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' outputs 0 (mesmo que echo é executado em um processo filho) e zsh -c 'n=0; echo $((++n)) | echo $((++n))' produz 2.

    
por 23.11.2018 / 23:13