test , ou seja, [ , precisa de espaços em branco em torno dos operadores, basicamente porque senão foo== seria considerado como contendo um operador, mesmo que seja uma cadeia válida. A construção [[ .. ]] funciona da mesma forma. (Veja esta questão e BashFAQ 031 para a diferença [ vs [[ .
Portanto, [ ${aa}=="" ] será sempre verdadeiro, pois [ string ] é igual a [ -n string ] , ou seja, testa se a string não está vazia. E aqui a string contém pelo menos == .
Em seguida, [ ${aa} == "" ] será um erro se aa não estiver definido ou vazio, pois uma variável vazia expandida sem aspas desaparece e [ == foo ] não é um teste válido. Se aa tiver um valor não vazio, é falso. ( [[ ${aa} == "" ]] funcionaria mesmo sem as aspas, pois [[ .. ]] é especial.)
É claro que [ "${aa}" == "" ] testaria que aa não está definido ou está vazio, o mesmo que [ -z "${aa}" ] .
Para testar se está definido e vazio, poderíamos usar [ "${aa+x}" ] && [ -z "$aa" ] , ou uma combinação bacana deles, roubada de um resposta de @ Stéphane Chazelas :
if [ "${aa+x$aa}" = "x" ] ; then
echo "aa is empty but set"
fi
(isso funciona, pois se aa não estiver definido, a expansão + expandirá para a sequência vazia e, se estiver definida, será expandida para x$aa , que é apenas x se aa estiver vazio. )
$ foo() { [ "${1+x$1}" = "x" ] && echo "set but empty" || echo "unset or non-empty"; }
$ foo; foo ""; foo bar
unset or non-empty
set but empty
unset or non-empty