Sim, isso é um problema de cotação: [ ! $3 ] expande para [ ! south brisbane qld ] (quatro argumentos entre [ e ] ). E quando ele vê quatro argumentos, com o primeiro um ! , [ espera ver algo como [ ! arg1 op arg2 ] onde op é um operador binário. (isso, novamente, é uma das coisas diferentes entre [ .. ] e [[ .. ]] ; veja este Q e também este Q )
brisbane não é um operador válido, portanto, ele reclama e retorna 2, o que é falso, portanto, as instruções dentro do if não são executadas. Para saber a diferença entre um erro e um teste de falha regular, você precisa testar explicitamente o valor de retorno em relação a 2.
Por outro lado, se $3 estiver vazio, o teste se tornará [ ! ] , um teste de um argumento que verifica se o único argumento não é vazio (é, é a cadeia de um caractere ! ) . Nesse caso, ele funciona como planejado, embora talvez não pelo motivo esperado.
Você deseja que [ ! "$3" ] ou [ -z "$3" ] mantenha a string como um argumento para [ .
É claro que você também pode inverter o sentido do teste e fazer o trabalho real dentro do if , para que um erro no teste evite a execução dos comandos principais. Mas isso tornaria a estrutura do código um pouco mais clara.