Nomes de variáveis ​​dinâmicas no Bash


159

Estou confuso sobre um script bash.

Eu tenho o seguinte código:

function grep_search() {
    magic_way_to_define_magic_variable_$1=`ls | tail -1`
    echo $magic_variable_$1
}

Eu quero ser capaz de criar um nome de variável contendo o primeiro argumento do comando e com o valor de, por exemplo, a última linha de ls.

Então, para ilustrar o que eu quero:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Então, como devo definir / declarar $magic_way_to_define_magic_variable_$1e como devo chamá-lo dentro do script?

Eu tentei eval, ${...}, \$${...}, mas eu ainda estou confuso.


3
Não. Use uma matriz associativa para mapear o nome do comando para os dados.
Chepner # 14/13

3
VAR = A; VAL = 333; leia "$ VAR" <<< "$ VAL"; eco "A = $ A"
Grigory K

Respostas:


150

Use uma matriz associativa, com nomes de comandos como chaves.

# Requires bash 4, though
declare -A magic_variable=()

function grep_search() {
    magic_variable[$1]=$( ls | tail -1 )
    echo ${magic_variable[$1]}
}

Se você não pode usar matrizes associativas (por exemplo, você deve suportar bash3), você pode usar declarepara criar nomes de variáveis ​​dinâmicas:

declare "magic_variable_$1=$(ls | tail -1)"

e use a expansão indireta de parâmetros para acessar o valor.

var="magic_variable_$1"
echo "${!var}"

Consulte BashFAQ: Indirection - Avaliando variáveis ​​indiretas / de referência .


5
@DeaDEnD -adeclara uma matriz indexada, não uma matriz associativa. A menos que o argumento to grep_searchseja um número, ele será tratado como um parâmetro com um valor numérico (o padrão é 0 se o parâmetro não estiver definido).
Chepner #

1
Hmm. Estou usando o bash 4.2.45(2)e declaro não listá-lo como uma opção declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Parece estar funcionando corretamente no entanto.
fent

declare -hem 4.2.45 (2) para mim mostra declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Você pode verificar se está realmente executando o 4.xe não o 3.2.
chepner

5
Por que não apenas declare $varname="foo"?
Ben Davis

1
${!varname}é muito mais simples e amplamente compatível
Brad Hein

227

Estive procurando uma maneira melhor de fazê-lo recentemente. A matriz associativa parecia um exagero para mim. Olha o que eu achei:

suffix=bzz
declare prefix_$suffix=mystr

...e depois...

varname=prefix_$suffix
echo ${!varname}

Se quiser declarar um global dentro de uma função, pode usar "declare -g" em bash> = 4.2. No bash anterior, você pode usar "somente leitura" em vez de "declarar", desde que não deseje alterar o valor posteriormente. Pode ser bom para a configuração ou o que você tem.
23413 Sam

7
Melhor usar encapsulado formato variável: prefix_${middle}_postfix(isto é, a sua formatação não não iria trabalhar para. varname=$prefix_suffix)
msciwoj

1
Eu estava preso no bash 3 e não podia usar matrizes associativas; como tal, isso salvou a vida. $ {! ...} não é fácil pesquisar no google nesse. Presumo que apenas expanda um nome var.
Neil McGill

10
@NeilMcGill: Veja "man bash" gnu.org/software/bash/manual/html_node/… : a forma básica de expansão de parâmetro é $ {parameter}. <...> Se o primeiro caractere do parâmetro for um ponto de exclamação (!), É introduzido um nível de indireção variável. Bash usa o valor da variável formada a partir do restante do parâmetro como o nome da variável; essa variável é expandida e esse valor é usado no restante da substituição, em vez do valor do próprio parâmetro.
Yorik.sar

1
@ syntaxerror: você pode atribuir valores o quanto quiser com o comando "declarar" acima.
Yorik.sar

48

Além das matrizes associativas, existem várias maneiras de obter variáveis ​​dinâmicas no Bash. Observe que todas essas técnicas apresentam riscos, que são discutidos no final desta resposta.

Nos exemplos a seguir, assumirei isso i=37e que você deseja criar um alias para a variável var_37cujo valor inicial é lolilol.

Método 1. Usando uma variável "ponteiro"

Você pode simplesmente armazenar o nome da variável em uma variável indireta, não muito diferente de um ponteiro C. O Bash possui uma sintaxe para ler a variável com alias: ${!name}expande para o valor da variável cujo nome é o valor da variável name. Você pode pensar nisso como uma expansão em dois estágios: ${!name}expande para $var_37, e expande para lolilol.

name="var_$i"
echo "$name"         # outputs “var_37”
echo "${!name}"      # outputs “lolilol”
echo "${!name%lol}"  # outputs “loli”
# etc.

Infelizmente, não há sintaxe de contraparte para modificar a variável com alias. Em vez disso, você pode conseguir a atribuição com um dos seguintes truques.

1a. Atribuindo comeval

evalé mau, mas também é a maneira mais simples e portátil de alcançar nosso objetivo. Você deve escapar cuidadosamente do lado direito da tarefa, pois ela será avaliada duas vezes . Uma maneira fácil e sistemática de fazer isso é avaliar o lado direito de antemão (ou usá-lo printf %q).

E você deve verificar manualmente se o lado esquerdo é um nome de variável válido ou um nome com índice (e se fosse evil_code #?). Por outro lado, todos os outros métodos abaixo o aplicam automaticamente.

# check that name is a valid variable name:
# note: this code does not support variable_name[index]
shopt -s globasciiranges
[[ "$name" == [a-zA-Z_]*([a-zA-Z_0-9]) ]] || exit

value='babibab'
eval "$name"='$value'  # carefully escape the right-hand side!
echo "$var_37"  # outputs “babibab”

Desvantagens:

  • não verifica a validade do nome da variável.
  • eval é mau.
  • eval é mau.
  • eval é mau.

1b. Atribuindo comread

O readbuiltin permite atribuir valores a uma variável da qual você fornece o nome, fato que pode ser explorado em conjunto com as strings here:

IFS= read -r -d '' "$name" <<< 'babibab'
echo "$var_37"  # outputs “babibab\n”

A IFSpeça e a opção -rgarantem que o valor seja atribuído como está, enquanto a opção -d ''permite atribuir valores com várias linhas. Devido a esta última opção, o comando retorna com um código de saída diferente de zero.

Observe que, como estamos usando uma string here, um caractere de nova linha é anexado ao valor.

Desvantagens:

  • um tanto obscuro;
  • retorna com um código de saída diferente de zero;
  • anexa uma nova linha ao valor.

1c. Atribuindo comprintf

Desde o Bash 3.1 (lançado em 2005), o printfbuilt-in também pode atribuir seu resultado a uma variável cujo nome é dado. Ao contrário das soluções anteriores, ele simplesmente funciona, não sendo necessário nenhum esforço extra para escapar das coisas, evitar divisões e assim por diante.

printf -v "$name" '%s' 'babibab'
echo "$var_37"  # outputs “babibab”

Desvantagens:

  • Menos portátil (mas, bem).

Método 2. Usando uma variável "reference"

Desde o Bash 4.3 (lançado em 2014), o declarebuilt-in tem uma opção -npara criar uma variável que é uma “referência de nome” para outra variável, assim como as referências em C ++. Assim como no método 1, a referência armazena o nome da variável com alias, mas cada vez que a referência é acessada (para leitura ou atribuição), o Bash resolve automaticamente o indireto.

Além disso, Bash tem um especial e sintaxe muito confuso para obter o valor da própria referência, juiz por si mesmo: ${!ref}.

declare -n ref="var_$i"
echo "${!ref}"  # outputs “var_37”
echo "$ref"     # outputs “lolilol”
ref='babibab'
echo "$var_37"  # outputs “babibab”

Isso não evita as armadilhas explicadas abaixo, mas pelo menos torna a sintaxe direta.

Desvantagens:

  • Não é portátil.

Riscos

Todas essas técnicas de aliasing apresentam vários riscos. A primeira é a execução de código arbitrário toda vez que você resolve o indireto (para leitura ou atribuição) . De fato, em vez de um nome de variável escalar, como var_37, você também pode usar um apelido de matriz, como arr[42]. Mas o Bash avalia o conteúdo dos colchetes toda vez que necessário, para que o alias arr[$(do_evil)]tenha efeitos inesperados ... Como conseqüência, use essas técnicas apenas quando você controlar a proveniência do alias .

function guillemots() {
  declare -n var="$1"
  var="«${var}»"
}

arr=( aaa bbb ccc )
guillemots 'arr[1]'  # modifies the second cell of the array, as expected
guillemots 'arr[$(date>>date.out)1]'  # writes twice into date.out
            # (once when expanding var, once when assigning to it)

O segundo risco é criar um alias cíclico. Como as variáveis ​​Bash são identificadas pelo nome e não pelo escopo, você pode inadvertidamente criar um alias para si mesmo (enquanto pensa que aliasaria uma variável de um escopo em anexo). Isso pode acontecer em particular ao usar nomes de variáveis ​​comuns (como var). Como conseqüência, use essas técnicas apenas quando você controlar o nome da variável com alias .

function guillemots() {
  # var is intended to be local to the function,
  # aliasing a variable which comes from outside
  declare -n var="$1"
  var="«${var}»"
}

var='lolilol'
guillemots var  # Bash warnings: “var: circular name reference”
echo "$var"     # outputs anything!

Fonte:


1
Essa é a melhor resposta, principalmente porque a ${!varname}técnica requer uma var intermediária para varname.
RichVel 20/11/19

Difícil entender que esta resposta não tenha sido votada mais alta
Marcos

18

O exemplo abaixo retorna o valor de $ name_of_var

var=name_of_var
echo $(eval echo "\$$var")

4
Aninhar dois echos com uma substituição de comando (que perde aspas) é desnecessário. Além disso, a opção -ndeve ser dada echo. E, como sempre, evalé inseguro. Mas tudo isso é desnecessário desde Bash tem uma sintaxe mais segura, mais claro e mais curto para este fim: ${!var}.
Maëlan 30/03/19

4

Isso deve funcionar:

function grep_search() {
    declare magic_variable_$1="$(ls | tail -1)"
    echo "$(tmpvar=magic_variable_$1 && echo ${!tmpvar})"
}
grep_search var  # calling grep_search with argument "var"

4

Isso vai funcionar também

my_country_code="green"
x="country"

eval z='$'my_"$x"_code
echo $z                 ## o/p: green

No seu caso

eval final_val='$'magic_way_to_define_magic_variable_"$1"
echo $final_val

3

De acordo com BashFAQ / 006 , você pode usar readcom a sintaxe aqui para atribuir variáveis ​​indiretas:

function grep_search() {
  read "$1" <<<$(ls | tail -1);
}

Uso:

$ grep_search open_box
$ echo $open_box
stack-overflow.txt

3

Usar declare

Não há necessidade de usar prefixos como em outras respostas, nem matrizes. Use apenas declare, aspas duplas , e expansão de parâmetros .

Costumo usar o seguinte truque para analisar listas de one to nargumentos que contenham argumentos formatados como key=value otherkey=othervalue etc=etc, como:

# brace expansion just to exemplify
for variable in {one=foo,two=bar,ninja=tip}
do
  declare "${variable%=*}=${variable#*=}"
done
echo $one $two $ninja 
# foo bar tip

Mas expandindo a lista argv como

for v in "$@"; do declare "${v%=*}=${v#*=}"; done

Dicas extras

# parse argv's leading key=value parameters
for v in "$@"; do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
done
# consume argv's leading key=value parameters
while (( $# )); do
  case "$v" in ?*=?*) declare "${v%=*}=${v#*=}";; *) break;; esac
  shift
done

1
Parece uma solução muito limpa. Não babadores malignos e bobs e você usar as ferramentas que estão relacionados com variáveis, não obscurecer funções aparentemente não relacionados ou mesmo perigosos, tais como printfoueval
kvantour

2

Uau, a maior parte da sintaxe é horrível! Aqui está uma solução com alguma sintaxe mais simples, se você precisar fazer referência indireta a matrizes:

#!/bin/bash

foo_1=("fff" "ddd") ;
foo_2=("ggg" "ccc") ;

for i in 1 2 ;
do
    eval mine=( \${foo_$i[@]} ) ;
    echo ${mine[@]} ;
done ;

Para casos de uso mais simples, recomendo a sintaxe descrita no Advanced Bash-Scripting Guide .


2
O ABS é alguém conhecido por mostrar más práticas em seus exemplos. Por favor, considere se apoiar no wiki do bash-hackers ou no Wooledge - que tem a entrada diretamente no tópico BashFAQ # 6 -.
Charles Duffy

2
Isso funciona apenas se as entradas foo_1e foo_2estiverem livres de espaço em branco e símbolos especiais. Exemplos de entradas problemáticas: 'a b'criará duas entradas dentro mine. ''não criará uma entrada dentro mine. '*'será expandido para o conteúdo do diretório de trabalho. Você pode evitar esses problemas citando:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi 1/19/19

@Socowi Esse é um problema geral com o loop através de qualquer array no BASH. Isso também pode ser resolvido alterando temporariamente o IFS (e, é claro, alterando-o novamente). É bom ver a citação elaborada.
ingyhere 17/05/19

@ingyhere Eu imploro para diferir. É não um problema geral. Existe uma solução padrão: sempre cite [@]construções. "${array[@]}"sempre será expandido para a lista correta de entradas sem problemas como divisão de palavras ou expansão de *. Além disso, o problema de divisão de palavras só pode ser contornado IFSse você souber algum caractere não nulo que nunca apareça dentro da matriz. Além disso, o tratamento literal de *não pode ser alcançado pela configuração IFS. Você define IFS='*'e divide as estrelas ou define IFS=somethingOthere *expande.
Socowi 18/05/19

@Socowi Você está assumindo que a expansão do shell é indesejável, e esse nem sempre é o caso. Os desenvolvedores reclamam de bugs quando as expressões do shell não se expandem depois de citar tudo. Uma boa solução é conhecer os dados e criar scripts adequadamente, mesmo usando |ou LFcomo IFS. Novamente, o problema geral nos loops é que a tokenização ocorre por padrão, de modo que a citação é a solução especial para permitir seqüências estendidas que contêm tokens. (É expansão globbing / parâmetro ou seqüências de caracteres estendidas entre aspas, mas não ambas.) Se forem necessárias oito aspas para ler uma var, shell é o idioma errado.
ingyhere 21/07

1

Para matrizes indexadas, é possível referenciá-las da seguinte maneira:

foo=(a b c)
bar=(d e f)

for arr_var in 'foo' 'bar'; do
    declare -a 'arr=("${'"$arr_var"'[@]}")'
    # do something with $arr
    echo "\$$arr_var contains:"
    for char in "${arr[@]}"; do
        echo "$char"
    done
done

Matrizes associativas podem ser referenciadas da mesma forma, mas precisam ser -Aativadas em declarevez de -a.


1

Um método extra que não depende da versão do shell / bash que você possui está usando envsubst. Por exemplo:

newvar=$(echo '$magic_variable_'"${dynamic_part}" | envsubst)

0

Quero poder criar um nome de variável que contenha o primeiro argumento do comando

script.sh Arquivo:

#!/usr/bin/env bash
function grep_search() {
  eval $1=$(ls | tail -1)
}

Teste:

$ source script.sh
$ grep_search open_box
$ echo $open_box
script.sh

Conforme help eval:

Execute argumentos como um comando shell.


Você também pode usar ${!var}a expansão indireta do Bash , como já mencionado, no entanto, ele não suporta a recuperação de índices de matriz.


Para mais leituras ou exemplos, consulte BashFAQ / 006 sobre Indirection .

Não temos conhecimento de nenhum truque que possa duplicar essa funcionalidade nos shells POSIX ou Bourne sem eval, o que pode ser difícil de executar com segurança. Portanto, considere isso um uso por sua conta e risco .

No entanto, você deve considerar novamente o uso de indireção, conforme as notas a seguir.

Normalmente, no script bash, você não precisará de referências indiretas. Geralmente, as pessoas procuram uma solução para isso quando não entendem ou sabem sobre matrizes Bash ou não consideram completamente outros recursos do Bash, como funções.

Colocar nomes de variáveis ​​ou qualquer outra sintaxe do bash dentro dos parâmetros é frequentemente feita incorretamente e em situações inadequadas para resolver problemas com melhores soluções. Isso viola a separação entre código e dados e, como tal, coloca você em uma posição escorregadia em relação a bugs e problemas de segurança. Indirecionar pode tornar seu código menos transparente e mais difícil de seguir.


Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.