declare -r faz uma variável readonly mas também declara no escopo atual e assim torna-a local para a função atual. Você quer o readonly em vez disso, apenas o primeiro:
readonly_once() {
local __assign
for __assign do
[[ -v ${__assign%%=*} ]] || readonly "$__assign"
done
}
Para ser usado como:
readonly_once VAR1=foo VAR2="$(cmd)" PATH ...
Observe que, desde que contrário a readonly , esse readonly_once não é uma palavra-chave (sim, readonly é também uma palavra-chave, embora bash mantenha esse fato oculto), $(cmd) precisa ser citado para evitar split + glob, não é uma atribuição nesse ponto.
$(cmd) será expandido (e assim cmd run), mesmo que o valor acabe não sendo atribuído a VAR2 se já tiver sido definido.
Essa função só funciona para variáveis escalares, não matrizes nem matrizes associativas.