Bash: o problema do falso negativo
Muitas vezes, nós perdemos horas tentando descobrir por que nossos scripts não fazem o que nós achamos que eles deveriam fazer, tudo porque deixamos de observar (ou realmente desconhecemos) os pequenos detalhes de como o shell funciona.
Este artigo pressupõe que você tenha algum conhecimento sobre o shell do GNU/Linux. Se não for o seu caso, talvez seja interessante considerar fazer umas aulas particulares de shell comigo.
O script abaixo é um exemplo disso, observe:
#!/usr/bin/env bash
frutas=(banana laranja abacate)
k=0
while :; do
echo ${frutas[$k]}
[[ $k -lt ${#frutas[@]} ]] && ((k++)) || k=0
read -sn1
done
O que esperamos é que, a cada tecla pressionada, o script exiba um dos elementos do vetor frutas
. Porém, veja o que realmente acontece:
:~$ ./exemplo1.sh
banana
banana
banana
banana
^C
:~$
A linha que determina o índice do elemento a ser exibido é esta aqui:
[[ $k -lt ${#frutas[@]} ]] && ((k++)) || k=0
Nela, nós testamos se o valor expandido em $k
é menor do que (-lt
) a quantidade de elementos em frutas
(${#frutas[@]}
). Com os operadores de encadeamento condicional &&
e ||
, nós dizemos ao shell o que fazer:
-
Se o teste terminar com sucesso (avaliação verdadeira), execute o comando
((k++))
; -
Se o teste terminar com erro (avaliação falsa), execute o comando
k=0
.
Deste modo, nós esperamos que o valor do índice seja incrementado enquanto ele não for maior do que o número de elementos ou seja tornado zero quando seu valor igualar ao número de elementos. Só que, na prática, o valor de k
está resultando em 0
todas as vezes, o que implica sempre na exibição do primeiro elemento do vetor, banana
, de índice 0
. É como se o teste estivesse sempre saindo com erro, causando a execução da atribuição k=0
.
Obviamente, o script está fazendo exatamente o que nós mandamos fazer, então a possibilidade de um bug no Bash está descartada, ou seja: a culpa é nossa! Nessas horas, é preciso voltar ao básico.
Em outras palavras: a decisão de permitir ou não a execução do comando seguinte está condicionada exclusivamente ao estado de saída do último comando executado.
Vamos considerar o valor inicial de k
, que é 0
. Certamente, 0
é menor do que a quantidade de elementos do vetor, que é 3
. Portanto, o nosso teste avalia a expressão assertiva como verdadeira e sai com estado de sucesso.
O encadeamento com o comando seguinte é feito com o operador &&
, que só permite a execução do próximo comando se o anterior sair com sucesso (que é o caso), o que permite a execução do incremento em uma expressão aritmética: ((k++))
.
Por sua vez, o incremento é encadeado com o comando seguinte pelo operador ||
, que só executa o comando seguinte se o anterior terminar com erro -- e aqui está a nossa primeira pista: se k=0
foi executado, o comando anterior, ((k++))
, terminou com erro!
Na verdade, muitos programas e comandos utilizam os estados de saída para enviar valores para o shell, como é o caso dos nossos testes de expressões assertivas ([[ ... ]]
) e... das expressões aritméticas!
Uma expressão aritmética sempre terminará com estado de saída de erro quando a última expressão avaliada em seu interior resultar no valor 0
.
Observe:
:~$ ((0)); echo $?
1
:~$ ((123)); echo $?
0
Para o shell, um erro significa qualquer valor inteiro diferente de zero na saída de um comando. Repare, no exemplo acima, que a expansão da variável especial ?
resultou no valor 1
quando a expressão aritmética avaliou o valor 0
. Por outro lado, quando o valor da expressão foi avaliado como 123
, a variável ?
expandiu o valor 0
, que é como o shell entende que o comando terminou com sucesso.
Sim, você entendeu o que está acontecendo:
-
A atribuição
k=0
foi executada porque a expressão saiu com erro; -
A expressão saiu com erro porque seu valor foi avaliado como
0
.
O que você ainda não deve ter entendido, é que existem dois tipos de incremento:
-
Pré-incremento (
++var
): o valor devar
é atualizado durante a avaliação da expressão; -
Pós-incremento (
var++
): o valor devar
é atualizado após a avaliação da expressão!
Ou seja, como nós fizemos um pós-incremento, no momento da avaliação da expressão, o valor inicial de k
era 0
, o que causou uma saída com estado de erro que, por sua vez, fez com que o operador ||
autorizasse a execução do comando seguinte: k=0
!
Em princípio, bastaria trocar o pós-incremento por um pré-incremento:
#!/usr/bin/env bash
frutas=(banana laranja abacate)
k=0
while :; do
echo ${frutas[$k]}
# Agora estamos usando um pré-incremento...
[[ $k -lt ${#frutas[@]} ]] && ((++k)) || k=0
read -sn1
done
Só que o nosso código ainda não faria o que queremos, veja o que acontece:
:~$ ./exemplo2.sh
banana
laranja
abacate
banana
laranja
^C
:~$
Como a verificação do incremento acontece no loop while após a impressão do elemento de frutas
, por um ciclo, nós tentamos expandir o valor de um elemento que não existe no vetor, o que nos obriga a fazer uma última correção:
#!/usr/bin/env bash
frutas=(banana laranja abacate)
k=0
while :; do
echo ${frutas[$k]}
# Como a saída não será testada, tanto faz se é um
# pré ou pós-incremento!
((k++))
# Nós só precisamos zerar o valor de 'k' se o teste
# sair com erro...
[[ $k -lt ${#frutas[@]} ]] || k=0
read -sn1
done
Agora sim, nós dissemos corretamente ao shell o que fazer:
:~$ ./exemplo3.sh
banana
laranja
abacate
banana
laranja
^C
:~$
O shell nunca faz nada diferente do que nós mandamos ele fazer. A maioria dos problemas na construção de comandos está na não observância aos detalhes de funcionamento do shell, seja por desconhecimento ou distração.
Se você não foi capaz de acompanhar a solução deste problema, talvez seja interessante considerar fazer umas aulas particulares de shell comigo. 😉