Impressão de matriz associativa BASH

17

Existe uma maneira de imprimir uma matriz inteira ([chave] = valor) sem repetir todos os elementos?

Suponha que eu criei uma matriz com alguns elementos:

declare -A array
array=([a1]=1 [a2]=2 ... [b1]=bbb ... [f500]=abcdef)

Posso imprimir de volta toda a matriz com

for i in "${!array[@]}"
do
echo "${i}=${array[$i]}"
done

No entanto, parece que o bash já sabe como obter todos os elementos da matriz de uma só vez - chaves ${!array[@]}e valores ${array[@]}.

Existe uma maneira de fazer o bash imprimir essas informações sem o loop?

Edit:
typeset -p arrayfaz isso!
No entanto, não consigo remover o prefixo e o sufixo em uma única substituição:

a="$(typeset -p array)"
b="${a##*(}"
c="${b%% )*}"

Existe uma maneira mais limpa de obter / imprimir apenas a parte key = value da saída?

Dani_l
fonte

Respostas:

15

Eu acho que você está perguntando duas coisas diferentes lá.

Existe uma maneira de fazer o bash imprimir essas informações sem o loop?

Sim, mas eles não são tão bons quanto usar o loop.

Existe uma maneira mais limpa de obter / imprimir apenas a parte key = value da saída?

Sim, o forlaço. Tem as vantagens de não exigir programas externos, é simples e facilita o controle exato do formato de saída sem surpresas.


Qualquer solução que tente manipular a saída de declare -p( typeset -p) deve lidar com a) a possibilidade de as próprias variáveis ​​conterem parênteses ou colchetes, b) a citação que declare -pdeve ser adicionada para tornar sua entrada válida de saída para o shell.

Por exemplo, sua expansão b="${a##*(}"consome alguns valores, se alguma chave / valor contiver um parêntese de abertura. Isso ocorre porque você usou ##, o que remove o prefixo mais longo . O mesmo para c="${b%% )*}". Embora você possa, obviamente, corresponder ao padrão impresso de maneira declaremais exata, você ainda teria dificuldade se não quisesse todas as citações.

Isso não parece muito bom, a menos que você precise.

$ declare -A array=([abc]="'foobar'" [def]='"foo bar"')
$ declare -p array
declare -A array='([def]="\"foo bar\"" [abc]="'\''foobar'\''" )'

Com o forloop, é mais fácil escolher o formato de saída como você gosta:

# without quoting
$ for x in "${!array[@]}"; do printf "[%s]=%s\n" "$x" "${array[$x]}" ; done
[def]="foo bar"
[abc]='foobar'

# with quoting
$ for x in "${!array[@]}"; do printf "[%q]=%q\n" "$x" "${array[$x]}" ; done
[def]=\"foo\ bar\"
[abc]=\'foobar\'

A partir daí, também é simples alterar o formato de saída (remova os colchetes ao redor da chave, coloque todos os pares de chave / valor em uma única linha ...). Se você precisar citar algo diferente do próprio shell, ainda precisará fazer isso sozinho, mas pelo menos terá os dados brutos para trabalhar. (Se você tiver novas linhas nas chaves ou nos valores, provavelmente precisará de algumas aspas.)

Com um Bash atual (4.4, eu acho), você também pode usar em printf "[%s]=%s" "${x@Q}" "${array[$x]@Q}"vez de printf "%q=%q". Ele produz um formato citado um pouco melhor, mas é obviamente um pouco mais trabalhoso para se lembrar de escrever. (E cita a caixa de canto @como chave de matriz, que %qnão cita.)

Se o loop for parecer muito cansativo para escrever, salve uma função em algum lugar (sem citar aqui):

printarr() { declare -n __p="$1"; for k in "${!__p[@]}"; do printf "%s=%s\n" "$k" "${__p[$k]}" ; done ;  }  

E então basta usar isso:

$ declare -A a=([a]=123 [b]="foo bar" [c]="(blah)")
$ printarr a
a=123
b=foo bar
c=(blah)

Também funciona com matrizes indexadas:

$ b=(abba acdc)
$ printarr b
0=abba
1=acdc
ilkkachu
fonte
Observe que a saída da sua printf ...%q...variante não é adequada para reinicialização no shell se a matriz tiver uma @chave, pois% q não a cita e a=([@]=value)for um erro de sintaxe bash.
Stéphane Chazelas
@ StéphaneChazelas, aparentemente. "${x@Q}"cita isso também, uma vez que cita todas as strings (e fica melhor). adicionou uma nota sobre o uso disso.
Ilkkachu
Sim, copiado do mksh. Outro operador de uma forma diferente que não pode ser combinada com a maioria das outras. Novamente, veja zshcom seus sinalizadores de expansão variáveis ​​(que antecedem novamente o bash's por décadas e com o qual você pode escolher o estilo de citação: $ {(q) var}, $ {(qq) var} ...) para um design melhor. O bash tem o mesmo problema que o mksh, pois não cita a string vazia (não é um problema aqui, pois o bash não suporta chaves vazias). Além disso, ao usar estilos de citação diferentes de aspas simples ( ${var@Q}recorre a $'...'alguns valores), é importante que o código seja reinicializado no mesmo local.
Stéphane Chazelas
@ StéphaneChazelas, acho que você quer dizer um valor não definido, não uma string vazia? ( x=; echo "${x@Q}"'', unset x; echo "${x@Q}"não dá nada.) O Bash @Qparece preferir $'\n'uma nova linha literal, o que pode ser bom em algumas situações (mas não sei dizer o que os outros preferem). Claro que ter uma escolha não seria ruim.
Ilkkachu
Oh sim desculpe, eu não tinha percebido isso. Essa é uma diferença de mksh então. A $'...'sintaxe é um problema potencial em coisas como LC_ALL=zh_HK.big5hkscs bash -c 'a=$'\''\n\u3b1'\''; printf "%s\n" "${a@Q}"'quais saídas $'\n<0xa3><0x5c>'e 0x5csozinha é barra invertida; portanto, você terá um problema se essa citação for interpretada em um código de idioma diferente.
Stéphane Chazelas
9
declare -p array
declare -A array='([a2]="2" [a1]="1" [zz]="Hello World" [b1]="bbb" [f50]="abcd" )'

2 garfos

Talvez isto:

printf "%s\n" "${!array[@]}"
a2
a1
f50
zz
b1

printf "%s\n" "${array[@]}"
2
1
abcd
Hello World
bbb

printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t
a2                              2
a1                              1
f50                             abcd
zz                              Hello World
b1                              bbb

3 garfos

ou isto:

paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}")
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Sem garfo

para ser comparado com

for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done
a2=2
a1=1
f50=abcd
zz=Hello World
b1=bbb

Comparação dos tempos de execução

Como a última sintaxe não usa fork, eles podem ser mais rápidos:

time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
      5      11      76
real    0m0.005s
user    0m0.000s
sys     0m0.000s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
      5       6      41
real    0m0.008s
user    0m0.000s
sys     0m0.000s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
      5       6      41
real    0m0.002s
user    0m0.000s
sys     0m0.001s

Mas essa afirmação não permanece verdadeira se a matriz se tornar grande; se a redução de garfos é eficiente para processos pequenos, o uso de ferramentas dedicadas é mais eficiente para processos maiores.

for i in {a..z}{a..z}{a..z};do array[$i]=$RANDOM;done


time printf "%s\n" "${!array[@]}" "${array[@]}" | pr -2t | wc
  17581   35163  292941
real    0m0.150s
user    0m0.124s
sys     0m0.036s

time paste -d= <(printf "%s\n" "${!array[@]}") <(printf "%s\n" "${array[@]}") | wc
  17581   17582  169875
real    0m0.140s
user    0m0.000s
sys     0m0.004s

time for i in "${!array[@]}";do printf "%s=%s\n" "$i" "${array[$i]}";done | wc
  17581   17582  169875
real    0m0.312s
user    0m0.268s
sys     0m0.076s

Observação

Como ambas as soluções ( bifurcadas ) usam alinhamento , nenhuma delas funcionará se qualquer variável contiver uma nova linha . Nesse caso, a única maneira é um forloop.

F. Hauri
fonte
Embora pareçam inteligentes, os dois modos são menos eficientes que o for. O que é uma pena, realmente.
Satō Katsura
@SatoKatsura Concordo, mas se for mais lento, o uso da sintaxe pré mais curto ... Não tenho certeza se a prsintaxe fica mais lenta, mesmo com grandes matrizes!
F. Hauri
2
@MiniMax Porque não produz o resultado correto (mesmos elementos, ordem errada). Você precisaria compactar matrizes ${!array[@]}e ${array[@]}primeiro para que isso funcionasse.
Satō Katsura
1
Esse último trecho pasteé mais longo que o forloop da pergunta escrita em uma linha for i in "${!array[@]}"; do echo "$i=${array[$i]}" ; done, mas requer duas subcascas e um programa externo. Como é que está mais limpo? A solução prtambém interrompe se houver muitos elementos, pois ele tenta paginar a saída. Você precisaria usar algo como o | pr -2t -l"${#array[@]}"que está começando a ficar difícil de lembrar em comparação com o loop simples e, novamente, é mais longo que isso.
Ilkkachu
1
Em bash, cmd1 | cmd2significa 2 garfos, mesmo que cmd1 ou cmd2 ou ambos estejam integrados.
Stéphane Chazelas
2

Se você está procurando um shell com melhor suporte a array associativo, tente zsh.

Em zsh(onde as matrizes de associação foram adicionados em 1998, em comparação com 1993 para ksh93 e 2009 para bater), $varou ${(v)var}se expande para o (não vazios) valores de hash, ${(k)var}para as chaves (não vazios) (na mesma ordem), e ${(kv)var}para chaves e valores.

Para preservar os valores vazios, como para matrizes, é necessário citar e usar o @sinalizador.

Então, para imprimir as chaves e os valores, é apenas uma questão de

printf '%s => %s\n' "${(@kv)var}"

Apesar de explicar um hash possivelmente vazio, você deve fazer:

(($#var)) &&  printf '%s => %s\n' "${(@kv)var}"

Observe também que o zsh usa uma sintaxe de definição de matriz muito mais sensível e útil do que ksh93a (copiada por bash):

typeset -A var
var=(k1 v1 k2 v2 '' empty '*' star)

O que facilita muito copiar ou mesclar matrizes associativas:

var2=("${(@kv)var1}")
var3+=("${(@kv)var2}")
var4=("${@kv)var4}" "${(@kv)var5}")

(você não pode copiar facilmente um hash sem um loop com bashe observe que bashatualmente não suporta chaves vazias ou chaves / valores com bytes NUL).

Consulte também os zshrecursos de zip da matriz que você normalmente precisará para trabalhar com matrizes associativas:

keys=($(<keys.txt)) values=($(<values.txt))
hash=(${keys:^values})
Stéphane Chazelas
fonte
1

Como o typeset faz o que você quer, por que não apenas editar sua saída?

typeset -p array | sed s/^.*\(// | tr -d ")\'\""  | tr "[" "\n" | sed s/]=/' = '/

a2 = 2  
a1 = 1  
b1 = bbb 

Onde

array='([a2]="2" [a1]="1" [b1]="bbb" )'

Detalhada, mas é muito fácil ver como a formatação funciona: basta executar o pipeline com progressivamente mais dos comandos sed e tr . Modifique-os para se adequar aos gostos bonitos da impressão.

Nadreck
fonte
Esse tipo de pipeline provavelmente falhará no momento em que algumas das chaves ou valores da matriz contiverem qualquer um dos caracteres que você está substituindo, como parênteses, colchetes ou aspas. E um pipeline de seds e tr's não é muito mais simples do que um forloop com printf.
Ilkkachu 22/05/19
Além disso, você sabe que trtraduzir caractere por caractere não corresponde a cadeias de caracteres? tr "]=" " ="muda "]" para um espaço e um =para um =, independentemente da posição. Então você provavelmente poderia combinar todos os três trpara um.
Ilkkachu
É verdade sobre alguns dos caracteres não alfanuméricos que consomem isso. No entanto tudo o que tem de lidar deles recebe uma ordem de magnitude mais complexa e por isso menos legível a menos que haja uma realmente boa razão para tê-los em seu feed de dados e que é afirmado na pergunta que eu assumir que eles estão filtrados antes de chegarmos aqui. Deve sempre ter sua ressalva explícita. Acho esses pipelines mais simples, por exemplo, para fins de depuração, do que um globo de impressão que funciona perfeitamente ou explode na sua cara. Aqui você faz uma alteração simples por elemento, teste e adiciona mais 1.
Nadreck 23/05
Foi mal! Tenho meus _tr_s e _sed_s totalmente misturados! Corrigido na edição mais recente.
Nadreck 23/05
1

Mais uma opção é listar todas as variáveis ​​e grep para a que você deseja.

set | grep -e '^aa='

Eu uso isso para depuração. Duvido que seja muito eficiente, pois lista todas as variáveis.

Se você estivesse fazendo isso com frequência, poderia torná-lo uma função assim:

aap() { set | grep -e "^$1="; }

Infelizmente, quando verificamos o desempenho usando o tempo:

$ time aap aa aa=([0]="abc") . real 0m0.014s user 0m0.003s sys 0m0.006s

Portanto, se você estivesse fazendo isso com muita frequência, desejaria a versão NO FORKS do F.Hauri, porque é muito mais rápida.

xer0x
fonte