bash: como propagar erros na substituição de processos?

19

Quero que meus scripts de shell falhem sempre que um comando executado com eles falhar.

Normalmente faço isso com:

set -e
set -o pipefail

(normalmente eu adiciono set -utambém)

O fato é que nenhuma das opções acima funciona com substituição de processo. Este código imprime "ok" e sai com o código de retorno = 0, enquanto eu gostaria que falhasse:

#!/bin/bash -e
set -o pipefail
cat <(false) <(echo ok)

Existe algo equivalente a "pipefail", exceto para substituição de processo? Qualquer outra maneira de passar para um comando a saída dos comandos como se fossem arquivos, mas gerando um erro sempre que algum desses programas falha?

A solução do homem pobre seria detectar se esses comandos gravam no stderr (mas alguns comandos gravam no stderr em cenários de sucesso).

Outra solução mais compatível com posix seria usar pipes nomeados, mas eu preciso usar esses comandos que usam substituição de processo como oneliners criados em tempo real a partir de código compilado, e a criação de pipes nomeados complicaria as coisas (comandos extras, erro de interceptação para excluí-los etc.)

juanleon
fonte
Você não precisa interceptar erros para excluir pipes nomeados. De fato, essa não é uma boa maneira. Considere mkfifo pipe; { rm pipe; cat <file; } >pipe. Esse comando será interrompido até que um leitor seja aberto pipeporque é o shell que executa o procedimento open()e assim que houver um leitor no pipelink fs pipefor rm'd e, em seguida, catcopie o infile para o descritor do shell para esse canal. E de qualquer maneira, se você deseja propagar um erro de um processo sub do: <( ! : || kill -2 "$$")
mikeserv
Obrigado pela dica sobre a exclusão de pipes nomeados. Infelizmente, a $$substituição não funciona para mim, pois essa substituição de comando não é feita, pois o comando que usa substituição de processo é feito dentro de um pipeline de comando gerado a partir de um código "não shell" (python). Provavelmente eu deveria criar subprocessos em python e canalizá-los programaticamente.
Juanleon 22/07
Então use kill -2 0.
mikeserv
Infelizmente, "kill -2 0" mataria (sinalizaria) o python. Escrever manipuladores de sinais para executar lógica de negócios dentro de um aplicativo multithread não é algo que eu estou ansioso para :-)
juanleon
Se você não deseja lidar com sinais, por que está tentando recebê-los? De qualquer forma, espero que os pipes nomeados sejam a solução mais simples no final, apenas porque fazê-lo corretamente requer a criação de sua própria estrutura e a instalação de seu próprio despachante personalizado. Supere esse obstáculo e tudo vem junto.
mikeserv

Respostas:

6

Você só poderia solucionar esse problema com isso, por exemplo:

cat <(false || kill $$) <(echo ok)
other_command

O subshell do script é SIGTERMd antes que o segundo comando possa ser executado ( other_command). O echo okcomando é executado "algumas vezes": O problema é que as substituições de processos são assíncronas. Não há garantia de que o kill $$comando seja executado antes ou depois do echo okcomando. É uma questão de agendamento de sistemas operacionais.

Considere um script bash como este:

#!/bin/bash
set -e
set -o pipefail
cat <(echo pre) <(false || kill $$) <(echo post)
echo "you will never see this"

A saída desse script pode ser:

$ ./script
Terminated
$ echo $?
143           # it's 128 + 15 (signal number of SIGTERM)

Ou:

$ ./script
Terminated
$ pre
post

$ echo $?
143

Você pode tentar e depois de algumas tentativas, verá as duas ordens diferentes na saída. No primeiro, o script foi finalizado antes que os outros dois echocomandos pudessem gravar no descritor de arquivo. No segundo, falseo killcomando ou provavelmente foi agendado após os echocomandos.

Ou, para ser mais preciso: a chamada signal()do sistema da killutilidade que envia o SIGTERMsinal para o processo de shells foi agendada (ou foi entregue) mais tarde ou mais cedo que os write()syscalls do eco .

Mas, no entanto, o script é interrompido e o código de saída não é 0. Portanto, ele deve resolver seu problema.

Outra solução é, é claro, usar pipes nomeados para isso. Mas, depende do seu script o quão complexo seria implementar pipes nomeados ou a solução alternativa acima.

Referências:

caos
fonte
2

Para o registro, e mesmo que as respostas e os comentários fossem bons e úteis, eu acabei implementando algo um pouco diferente (eu tinha algumas restrições sobre o recebimento de sinais no processo pai que não mencionei na pergunta)

Basicamente, acabei fazendo algo assim:

command <(subcomand 2>error_file && rm error_file) <(....) ...

Então eu verifico o arquivo de erro. Se existir, sei qual subcomando falhou (e o conteúdo do arquivo error_file pode ser útil). Mais detalhado e hackeado do que eu originalmente queria, mas menos complicado do que criar pipes nomeados em um comando bash de uma linha.

juanleon
fonte
O que funciona melhor para você é geralmente o melhor caminho. Obrigado especialmente por voltar e responder - selfies são os meus favoritos.
mikeserv
2

Este exemplo mostra como usar killjunto com trap.

#! /bin/bash
failure ()
{
  echo 'sub process failed' >&2
  exit 1
}
trap failure SIGUSR1
cat < <( false || kill -SIGUSR1 $$ )

Mas killnão é possível transmitir um código de retorno do seu subprocesso para o processo pai.

ceving
fonte
Eu gosto dessa variação na resposta aceita
sehe
1

De maneira semelhante à que você implementaria $PIPESTATUS/ $pipestatuscom um shell POSIX que não o suporta , é possível obter o status de saída dos comandos passando-os por um canal:

unset -v false_status echo_status
{ code=$(
    exec 3>&1 >&4 4>&-
    cat 3>&- <(false 3>&-; echo >&3 "false_status=$?") \
             <(echo ok 3>&-; echo >&3 "echo_status=$?")
);} 4>&1
cat_status=$?
eval "$code"
printf '%s_code=%d\n' cat   "$cat_status" \
                      false "$false_status" \
                      echo  "$echo_status"

Que dá:

ok
cat_code=0
false_code=1
echo_code=0

Ou você pode usar pipefaile implementar a substituição do processo manualmente, como faria com shells que não o suportam:

set -o pipefail
{
  false <&5 5<&- | {
    echo OK <&5 5<&- | {
      cat /dev/fd/3 /dev/fd/4
    } 4<&0 <&5 5<&-
  } 3<&0
} 5<&0
Stéphane Chazelas
fonte