Bash: o problema do falso negativo

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.

O que não estamos vendo?

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.

Os operadores de encadeamento condicional "não funcionam" como um bloco if/else!

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!

Erros não são necessariamente falhas!

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.

Mas o valor foi incrementado, por que ele ainda é avaliado como 0?

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 de var é atualizado durante a avaliação da expressão;

  • Pós-incremento (var++): o valor de var é 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!

Corrigindo o código

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
:~$

Conclusão

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. 😉

Músico, programador, designer e apaixonado pelos ideais do Software Livre.

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post comment