Como atribuir valores que contêm espaço a variáveis ​​no bash usando eval

19

Eu quero atribuir valores dinamicamente a variáveis ​​usando eval. O seguinte exemplo fictício funciona:

var_name="fruit"
var_value="orange"
eval $(echo $var_name=$var_value)
echo $fruit
orange

No entanto, quando o valor da variável contém espaços, evalretorna um erro, mesmo que $var_valueseja colocado entre aspas duplas:

var_name="fruit"
var_value="blue orange"
eval $(echo $var_name="$var_value")
bash: orange : command not found

Alguma maneira de contornar isso?

Sébastien Clément
fonte

Respostas:

13

Não use eval, use declare

$ declare "$var_name=$var_value"
$ echo "fruit: >$fruit<"
fruit: >blue orange<
Glenn Jackman
fonte
11

Não use evalpara isso; use declare.

var_name="fruit"
var_value="blue orange"
declare "$var_name=$var_value"

Observe que a divisão de palavras não é um problema, porque tudo o que se segue a =é tratado como valor declare, não apenas a primeira palavra.

Na bash4.3, as referências nomeadas tornam isso um pouco mais simples.

$ declare -n var_name=fruit
$ var_name="blue orange"
$ echo $fruit
blue orange

Você pode fazer o evaltrabalho, mas ainda não deveria :) Usar evalé um mau hábito para entrar.

$ eval "$(printf "%q=%q" "$var_name" "$var_value")"
chepner
fonte
2
Usar eval esse caminho está errado. Você está expandindo $var_valueantes de passá-lo para o evalque significa que ele será interpretado como código do shell! (tente por exemplo com var_value="';:(){ :|:&};:'")
Stéphane Chazelas
1
Bom ponto; existem algumas strings que você não pode atribuir com segurança usando eval(que é uma das razões pelas quais eu disse que você não deveria usar eval).
chepner 11/09/14
@chepner - Eu não acredito que isso seja verdade. talvez seja, mas pelo menos não este. as substituições de parâmetros permitem expansão condicional e, portanto, você pode expandir apenas valores seguros na maioria dos casos. Ainda assim, seu principal problema $var_valueé o inverso de citação - assumindo um valor seguro para $var_name (que pode ser uma suposição tão perigosa, na verdade) , você deve colocar as aspas duplas do lado direito entre aspas simples - não vice-versa.
mikeserv
Eu acho que corrigi o formato eval, using printfe bashespecífico %q. Ainda não é uma recomendação a ser usada eval, mas acho que é mais seguro do que era antes. O fato de você precisar fazer tanto esforço para fazê-lo funcionar é a prova de que você deve usar declareou nomear referências.
chepner
Bem, na verdade, na minha opinião, referências nomeadas são o problema. A melhor maneira de usá-lo - na minha experiência - é como ... set -- a bunch of args; eval "process2 $(process1 "$@")"onde process1apenas imprime números citados como "${1}" "${8}" "${138}". Isso é muito simples - e tão fácil quanto '"${'$((i=$i+1))'}" 'na maioria dos casos. referências indexadas tornam seguro, robusto e rápido . Ainda assim - eu votei.
mikeserv
4

Uma boa maneira de trabalhar evalé substituí-lo echopelos testes. echoe evalfunciona da mesma forma (se deixarmos de lado a \xexpansão feita por algumas echoimplementações como bashas de algumas condições).

Ambos os comandos juntam seus argumentos com um espaço no meio. A diferença é que echo exibe o resultado enquanto eval avalia / interpreta como código shell o resultado.

Então, para ver qual código shell

eval $(echo $var_name=$var_value)

avaliar, você pode executar:

$ echo $(echo $var_name=$var_value)
fruit=blue orange

Não é isso que você quer, o que você quer é:

fruit=$var_value

Além disso, usar $(echo ...)aqui não faz sentido.

Para gerar o resultado acima, você executaria:

$ echo "$var_name=\$var_value"
fruit=$var_value

Então, para interpretar, é simplesmente:

eval "$var_name=\$var_value"

Observe que também pode ser usado para definir elementos individuais da matriz:

var_name='myarray[23]'
var_value='something'
eval "$var_name=\$var_value"

Como outros já disseram, se você não se importa que seu código seja bashespecífico, você pode usar declarecomo:

declare "$var_name=$var_value"

No entanto, note que ele tem alguns efeitos colaterais.

Limita o escopo da variável à função em que ela é executada. Portanto, você não pode usá-la, por exemplo, em coisas como:

setvar() {
  var_name=$1 var_value=$2
  declare "$var_name=$var_value"
}
setvar foo bar

Porque isso declararia uma foovariável local para setvarisso seria inútil.

bash-4.2adicionou uma -gopção para declaredeclarar uma variável global , mas isso não é o que queremos, pois o nosso setvardefiniria uma var global em oposição à do chamador se o chamador fosse uma função, como em:

setvar() {
  var_name=$1 var_value=$2
  declare -g "$var_name=$var_value"
}
foo() {
  local myvar
  setvar myvar 'some value'
  echo "1: $myvar"
}
foo
echo "2: $myvar"

que produziria:

1:
2: some value

Além disso, observe que while declareé chamado declare(na verdade, basho conceito emprestado do shell Korn typeset), se a variável já estiver definida, declarenão declara uma nova variável e a maneira como a atribuição é feita depende do tipo da variável.

Por exemplo:

varname=foo
varvalue='([PATH=1000]=something)'
declare "$varname=$varvalue"

produzirá um resultado diferente (e potencialmente terá efeitos colaterais desagradáveis) se tiver varnamesido declarado anteriormente como escalar , matriz ou matriz associativa .

Stéphane Chazelas
fonte
2
O que há de errado em ser específico do bash? O OP colocou a tag bash na pergunta, então ele está usando o bash. Fornecer alternativas é bom, mas acho que dizer a alguém para não usar o recurso de um shell porque não é portátil é bobagem.
Patrick
@ Patrick, viu o smiley? Dito isto, o uso de sintaxe portátil significa menos esforço quando você precisa portar seu código para outro sistema onde bashnão está disponível (ou quando você percebe que precisa de um shell melhor / mais rápido). A evalsintaxe funciona em todos os shells semelhantes a Bourne e é POSIX, para que todos os sistemas tenham um shlocal que funcione. (que significa também a minha resposta se aplica a todos os reservatórios, e mais cedo ou mais tarde, como acontece muitas vezes aqui, você verá uma pergunta específica não-festa fechada como uma duplicata de um presente.
Stéphane Chazelas
mas e se $var_namecontiver tokens? ... gosta ;?
mikeserv
@mikeserv, então não é um nome de variável. Se você não pode confiar em seu conteúdo, então você precisa higienizar-lo com ambos evale declare(pense PATH, TMOUT, PS4, SECONDS...).
Stéphane Chazelas
mas no primeiro passo, sempre é uma expansão variável e nunca um nome de variável até o segundo. na minha resposta, eu o desinfecto com uma expansão de parâmetro, mas se você está implicando em fazer a higienização em uma subshell na primeira passagem, isso também pode ser feito portably w / export. Eu não sigo o bit entre parênteses no final.
mikeserv
1

Se você fizer:

eval "$name=\$val"

... e $namecontém um ;- ou vários outros tokens que o shell pode interpretar como delimitando um comando simples - precedido pela sintaxe apropriada do shell, que será executada.

name='echo hi;varname' val='be careful with eval'
eval "$name=\$val" && echo "$varname"

SAÍDA

hi
be careful with eval

Às vezes, pode ser possível separar a avaliação e a execução de tais declarações. Por exemplo, aliaspode ser usado para pré-avaliar um comando. No exemplo a seguir, a definição da variável é salva em uma aliasque só pode ser declarada com êxito se a $nmvariável que está avaliando não contiver bytes que não correspondam a alfanuméricos ASCII ou _.

LC_OLD=$LC_ALL LC_ALL=C
alias "${nm##*[!_A-Z0-9a-z]*}=_$nm=\$val" &&
eval "${nm##[0-9]*}" && unalias "$nm"
LC_ALL=$LC_OLD

evalé usado aqui para manipular a chamada do novo aliasde um nome de var. Mas isso só é chamado se a aliasdefinição anterior for bem-sucedida e, embora eu saiba que muitas implementações diferentes aceitem muitos tipos diferentes de valores para aliasnomes, ainda não encontrei uma que aceite uma completamente vazia .

A definição dentro de aliasé para _$nm, no entanto, e isso é para garantir que nenhum valor significativo do ambiente seja substituído. Não conheço nenhum valor de ambiente notável que comece com a _e geralmente é uma aposta segura para declarações semi-privadas.

De qualquer forma, se a aliasdefinição for bem-sucedida, ela declarará um valor aliasnomeado para $nm. E evalsó chamará isso aliasse também não começar com um número - senão evalrecebe apenas um argumento nulo. Portanto, se as duas condições forem atendidas eval, o alias e a definição da variável salva no alias serão feitos, após o que o novo aliasserá imediatamente removido da tabela de hash.

mikeserv
fonte
;não é permitido em nomes de variáveis. Se você não tiver controle sobre o conteúdo $name, precisará sanitá-lo para export/ declaretambém. Embora exportnão execute código, definir algumas variáveis ​​como PATH, PS4e muitas delas info -f bash -n 'Bash Variables'têm efeitos colaterais igualmente perigosos.
Stéphane Chazelas
@ StéphaneChazelas - é claro que não é permitido, mas, como antes, não é um nome de variável na evalprimeira passagem de um - é uma expansão variável. Como você disse em outro lugar, nesse contexto, é muito permitido. Ainda assim, o argumento $ PATH é muito bom - fiz uma pequena edição e adicionarei mais tarde.
mikeserv
@ StéphaneChazelas - antes tarde do que nunca ...?
mikeserv
Na prática, zsh, pdksh, mksh, yashnão se queixam sobre unset 'a;b'.
Stéphane Chazelas
Você vai querer unset -v -- ...também.
Stéphane Chazelas