Como capturar o código de saída / manipular erros corretamente ao usar a substituição de processo?

13

Eu tenho um script que analisa nomes de arquivos em uma matriz usando o seguinte método obtido de uma sessão de perguntas e respostas no SO :

unset ARGS
ARGID="1"
while IFS= read -r -d $'\0' FILE; do
    ARGS[ARGID++]="$FILE"
done < <(find "$@" -type f -name '*.txt' -print0)

Isso funciona muito bem e lida com todos os tipos de variações de nome de arquivo perfeitamente. Às vezes, no entanto, passarei um arquivo inexistente para o script, por exemplo:

$ findscript.sh existingfolder nonexistingfolder
find: `nonexistingfile': No such file or directory
...

Em circunstâncias normais, o script capturaria o código de saída com algo parecido RET=$?e o usaria para decidir como proceder. Isso não parece funcionar com a substituição do processo acima.

Qual é o procedimento correto em casos como este? Como posso capturar o código de retorno? Existem outras maneiras mais adequadas de determinar se algo deu errado no processo substituído?

Glutanimado
fonte

Respostas:

5

Você pode facilmente obter o retorno de qualquer processo subconjunto, ecoando seu retorno sobre o stdout. O mesmo se aplica à substituição de processos:

while IFS= read -r -d $'\0' FILE || 
    ! return=$FILE
do    ARGS[ARGID++]="$FILE"
done < <(find . -type f -print0; printf "$?")

Se eu executar isso, a última linha - (ou \0seção delimitada, conforme o caso) será findo status de retorno do. readretornará 1 quando receber um EOF - portanto, o único horário $returndefinido $FILEé o último bit de informação lido.

Eu uso printfpara não adicionar um \newline extra - isso é importante porque mesmo um readrealizado regularmente - no qual você não delimita em \0NULs - retornará outro que 0 em casos em que os dados que acabou de ler não terminam em um \newline. Portanto, se sua última linha não terminar com uma linha de \new, o último valor na sua variável de leitura será o seu retorno.

Executando o comando acima e depois:

echo "$return"

RESULTADO

0

E se eu alterar a parte de substituição do processo ...

...
done < <(! find . -type f -print0; printf "$?")
echo "$return"

RESULTADO

1

Uma demonstração mais simples:

printf \\n%s list of lines printed to pipe |
while read v || ! echo "$v"
do :; done

RESULTADO

pipe

E, de fato, desde que o retorno que você deseja seja a última coisa que você escreve para stdout na substituição do processo - ou qualquer processo subconjunto do qual você leia dessa maneira -, $FILEsempre será o status de retorno que você deseja quando através. E, portanto, a || ! return=...peça não é estritamente necessária - é usada para demonstrar apenas o conceito.

mikeserv
fonte
5

Os processos de substituição de processos são assíncronos: o shell os lança e, em seguida, não fornece nenhuma maneira de detectar quando eles morrem. Portanto, você não poderá obter o status de saída.

Você pode gravar o status de saída em um arquivo, mas isso geralmente é desajeitado porque você não pode saber quando o arquivo foi gravado. Aqui, o arquivo é gravado logo após o final do loop, portanto, é razoável esperar por ele.

 < <(find …; echo $? >find.status.tmp; mv find.status.tmp find.status)
while ! [ -e find.status ]; do sleep 1; done
find_status=$(cat find.status; rm find.status)

Outra abordagem é usar um pipe nomeado e um processo em segundo plano (que você pode usar wait).

mkfifo find_pipe
find  >find_pipe &
find_pid=$!
 <find_pipe
wait $find_pid
find_status=$?

Se nenhuma das abordagens for adequada, acho que você precisará seguir para uma linguagem mais capaz, como Perl, Python ou Ruby.

Gilles 'SO- parar de ser mau'
fonte
Obrigado por esta resposta. Os métodos que você descreveu funcionam bem, mas devo admitir que são um pouco mais complicados do que eu previra. No meu caso, resolvi fazer um loop anterior ao mostrado na pergunta que repete todos os argumentos e imprime um erro se um deles não for um arquivo ou pasta. Embora isso não lide com outros tipos de erros que podem ocorrer no processo substituído, é bom o suficiente para este caso específico. Se eu precisar de um método de tratamento de erros mais sofisticado em situações como essa, certamente voltarei à sua resposta.
Glutanimado
2

Use um co-processo . Usando o coprocbuiltin, você pode iniciar um subprocesso, ler sua saída e verificar seu status de saída:

coproc LS { ls existingdir; }
LS_PID_=$LS_PID
while IFS= read i; do echo "$i"; done <&"$LS"
wait "$LS_PID_"; echo $?

Se o diretório não existir, waitserá encerrado com um código de status diferente de zero.

Atualmente, é necessário copiar o PID para outra variável, pois $LS_PIDserá desabilitado antes de waitser chamado. Consulte Bash unsets * _PID variable antes que eu possa esperar no coproc para obter detalhes.

Feuermurmel
fonte
1
Estou curioso para saber quando alguém usaria <& "$ LS" vs read -u $ LS? - graças
Brian Chrisman
1
@BrianChrisman Neste caso, provavelmente nunca. read -udeve funcionar tão bem. O exemplo deveria ser genérico e mostrar como a saída do coprocesso poderia ser canalizada para outro comando.
Feuermurmel
1

Uma abordagem é:

status=0
token="WzNZY3CjqF3qkasn"    # some random string
while read line; do
    if [[ "$line" =~ $token:([[:digit:]]+) ]]; then
        status="${BASH_REMATCH[1]}"
    else
        echo "$line"
    fi
done < <(command; echo "$token:$?")
echo "Return code: $status"

A idéia é ecoar o status de saída junto com o token aleatório após a conclusão do comando e, em seguida, usar expressões regulares do bash para procurar e extrair o status de saída. O token é usado para criar uma sequência exclusiva para procurar na saída.

Provavelmente não é a melhor maneira de fazer isso no sentido geral de programação, mas pode ser a maneira menos dolorosa de lidar com isso no bash.

orev
fonte