Ordem de vetores associativos
Pergunta feita pelo participante @NRZcode, do curso Técnicas do Shell.
@NRZcode perguntou:
Sabemos que um array associativo não ordena seus índices nem seus valores. Sabemos que o array não segue a ordem em que são inseridos seus índices e valores também. Afinal, qual é a lógica por de trás da ordem dos índices de um array associativo?
$ declare -A carros
$
$ carros[vw]=fusca
$ carros[fiat]=147
$ carros[ford]=Ka
$ carros[willis]=jeep
$
$ printf '%s\n' "${!carros[@]}"
willis
vw
fiat
ford
$
$ printf '%s\n' "${carros[@]}"
jeep
fusca
147
Ka
Arrays indexados mantém a ordem de seus índices apesar da ordem que inserimos seus valores
$ alunos=([3]=João [1]=Maria [5]=José)
$
$ printf '%s\n' "${alunos[@]}"
Maria
João
José
Primeiro, a pergunta precisa ser entendida quanto ao que se quer dizer com ordem:
- Ordem dos índices?
- Ordem dos valores?
- Ordem de criação dos elementos?
Porque, quando falamos de vetores indexados numericamente, fica claro que estamos falando da ordem numérica dos índices, mas isso não se aplica a vetores associativos, visto que eles recebem strings como índices.
Seja como for, existem soluções para cada tipo de ordenamento desejado, mas a questão aqui é sobre "a lógica por de trás da ordem dos índices de um array associativo".
A ordem dos índices de vetores associativos no shell será sempre imprevisível, porque a implementação é feita através de uma tabela de dispersão (hash table, em inglês).
Aqui está uma imagem que ilustra bem esse tipo de estrutura:
O objetivo da tabela de dispersão é proporcionar o acesso a dados que precisem ser lidos, escritos, alterados ou removidos em tempos uniformes. Isso é feito distribuindo (dispersando) seus registros numa região encapsulada na memória (na ilustração, o bucket, ou "balde") onde eles serão associados, cada um, a uma chave (uma hash) calculada por um algorítimo (uma função de dispersão).
Portanto, a aparente desordem na expansão de vetores associativos é, na verdade, uma ordem determinada pela sequência das chaves atribuídas a cada índice, o que quase nunca coincide com a ordem de criação dos elementos do vetor.
Antes de falarmos de uma solução para expandir índices de vetores associativos na ordem em que foram criados, que é a demanda mais comum, existe algo muito interessante sobre o uso de hashes no shell: o mesmo mecanismo é utilizado para tornar uniforme o tempo de localização de "comandos externos"!
Observe:
~ $ hash
hash: tabela de hash está vazia
Em uma sessão recém-iniciada do shell, o comando interno hash
, do Bash, nos mostra que não há nenhum caminho para utilitários invocados pela linha de comando. Neste ponto, qualquer comando externo invocado terá que passar pela busca da sua localização no PATH
.
Quando um comando externo é executado, o Bash o inclui em uma tabela hash e, assim, pode "acelerar" uma invocação posterior.
O ponto aqui não é bem tornar mais rápido o acesso ao executável (o que pode até acontecer), mas garantir uma uniformidade nos tempos de localização.
Veja o que acontece após a execução de alguns comandos:
~ $ ls
adm bin dld git mus pic pub tpl www teste.txt
aud Desktop doc lib not prj tmp vid mbox
~ $ grep blau /etc/passwd
blau:x:1000:1000:Blau Araujo,,,:/home/blau:/bin/bash
~ $ date
ter 03 mai 2022 00:03:06 -03
~ $ ls
adm bin dld git mus pic pub tpl www teste.txt
aud Desktop doc lib not prj tmp vid mbox
~ $ hash
número comando
1 /usr/bin/grep
2 /usr/bin/ls
1 /usr/bin/date
A tradução da coluna
número
está imprecisa: trata-se do número de invocações do executável externo (hits, no original).
Uma possível solução é criar um segundo vetor para registar a criação dos índices e algumas funções para lidar com as ações mais comuns, como nesse exemplo:
declare -A carro
declare -a carro_index
carro_add() {
carro[$1]="$2"
carro_index+=($1)
}
carro_expand() {
local i exp
for i in ${carro_index[@]}; do
exp+="${carro[$i]} "
done
printf '%s' "${exp::-1}"
}
carro_remove() {
local i
unset carro[$1]
for i in "${!carro_index[@]}"; do
if [[ ${carro_index[i]} = $1 ]]; then
unset carro_index[i]
return
fi
done
}
Deste modo, poderíamos definir os elementos como abaixo:
carro_add vw fusca
carro_add fiat 147
carro_add ford corcel
A expansão dos elementos, já na ordem de definição, aconteceria assim, por exemplo:
for i in $(carro_expand); do
echo i
done
O que resultaria na expansão dos elementos na ordem de sua definição:
fusca
147
corcel
Para outros tipos de ordenação, nós podemos recorrer aos utilitários sort
e cut
, por exemplo:
# Ordem por hash...
:~$ for i in "${!carro[@]}"; do echo $i ${carro[i]}; done
vw fusca
ford corcel
fiat 147
# Obter índices ordenados...
:~$ for i in "${!carro[@]}"; do echo $i ${carro[$i]}; done | cut -d' ' -f1 | sort
fiat
ford
vw
# Obter valores ordenados...
:~$ for i in "${!carro[@]}"; do echo $i ${carro[$i]}; done | cut -d' ' -f2 | sort
147
corcel
fusca