Bash'te dinamik değişken adları


159

Bir bash senaryosu hakkında kafam karıştı.

Takip koduna sahibim:

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

Komutun ilk argümanını içeren ve örneğin son satırının değerini taşıyan bir değişken adı oluşturmak istiyorum ls.

Yani ne istediğimi göstermek için:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Öyleyse, nasıl tanımlamalı / beyan $magic_way_to_define_magic_variable_$1etmeli ve senaryo içinde nasıl çağırmalıyım?

Ben denedim eval, ${...}, \$${...}, ama yine karıştı.


3
Yapma. Komut adını verilerle eşlemek için ilişkilendirilebilir bir dizi kullanın.
chepner

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

Yanıtlar:


150

Anahtar olarak komut adlarıyla ilişkilendirilebilir bir dizi kullanın.

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

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

İlişkilendirilebilir dizileri kullanamıyorsanız (ör. bash3'ü desteklemelisiniz ), declaredinamik değişken adları oluşturmak için kullanabilirsiniz :

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

ve değere erişmek için dolaylı parametre genişletmeyi kullanın.

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

Bkz. BashFAQ: Dolaylı - Dolaylı / referans değişkenlerin değerlendirilmesi .


5
@DeaDEnD -a, ilişkilendirilebilir bir dizi değil, dizinlenmiş bir dizi bildirir. Argümanı grep_searchbir sayı değilse, sayısal değere sahip bir parametre olarak değerlendirilir (parametre ayarlanmamışsa varsayılan olarak 0 olur).
chepner

1
Hmm. Bash kullanıyorum 4.2.45(2)ve declare bunu bir seçenek olarak listelemiyor declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Ancak doğru çalışıyor gibi görünüyor.
fent

declare -h4.2.45 (2) benim için gösterir declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Aslında 3.2 değil, 4.x çalıştırıp çalıştırmadığınızı bir kez daha kontrol edebilirsiniz.
chepner

5
Neden sadece declare $varname="foo"?
Ben Davis,

1
${!varname}çok daha basit ve geniş uyumlu
Brad Hein

227

Son zamanlarda bunu yapmanın daha iyi bir yolunu arıyordum. İlişkisel dizi benim için aşırıya kaçmış gibi geldi. Bak ne buldum:

suffix=bzz
declare prefix_$suffix=mystr

...ve sonra...

varname=prefix_$suffix
echo ${!varname}

Bir fonksiyonun içinde bir global bildirmek isterseniz, bash> = 4.2'de "declare -g" kullanabilirsiniz. Daha önceki bash'da, değeri daha sonra değiştirmek istemediğiniz sürece "declare" yerine "salt okunur" kullanabilirsiniz. Yapılandırma için iyi olabilir veya neye sahip olabilirsiniz.
Sam Watkins

7
kapsüllenmiş değişken biçimini kullanmak için en iyisi: prefix_${middle}_postfix(ör. biçimlendirmeniz işe yaramaz varname=$prefix_suffix)
msciwoj

1
Ben bash 3 ile sıkışmış ve ilişkilendirilebilir diziler kullanamadı; bu yüzden bu bir hayat kurtarıcıydı. $ {! ...} bu konuda Google'a gitmek kolay değil. Ben sadece bir var adı genişletir varsayalım.
Neil McGill

10
@NeilMcGill: Bkz. "Man bash" gnu.org/software/bash/manual/html_node/… : Parametre genişletmenin temel biçimi $ {parametre} 'dir. <...> Parametrenin ilk karakteri bir ünlem işareti (!) İse, değişken dolaylılık düzeyi eklenir. Bash, değişkenin adı olarak parametrenin geri kalanından oluşan değişkenin değerini kullanır; bu değişken daha sonra genişletilir ve bu değer, değiştirmenin geri kalanında, parametrenin kendisinden ziyade kullanılır.
Yorik.sar

1
@syntaxerror: yukarıdaki "declare" komutuyla değerleri istediğiniz kadar atayabilirsiniz.
Yorik.sar

48

İlişkilendirilebilir dizilerin ötesinde, Bash'te dinamik değişkenlere ulaşmanın birkaç yolu vardır. Tüm bu tekniklerin, bu cevabın sonunda tartışılan riskler sunduğunu unutmayın.

Aşağıdaki örneklerde , ilk değeri olan i=37değişkenin diğer adını taklit etmek istediğinizi varsayacağım .var_37lolilol

Yöntem 1. “İşaretçi” değişkeni kullanma

Değişkenin adını basitçe bir C işaretçisinin aksine bir dolaylı değişkente saklayabilirsiniz. Bash daha sonra, diğer değişkenin okunması için bir sözdizimine sahiptir : adı değişkenin ${!name}değeri olan değişkenin değerine genişler name. Bunu iki aşamalı bir genişleme olarak düşünebilirsiniz: ${!name}genişler $var_37, genişler lolilol.

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

Maalesef, diğer adı değiştirilen değişkeni değiştirmek için bir karşılık sözdizimi yoktur . Bunun yerine, aşağıdaki numaralardan biriyle ödev alabilirsiniz.

1 A. İle atamaeval

evalkötüdür, ama aynı zamanda hedefimize ulaşmanın en basit ve taşınabilir yoludur. İki kez değerlendirileceği için ödevin sağ tarafından dikkatlice kaçmanız gerekir . Bunu yapmanın kolay ve sistematik bir yolu, sağ tarafı önceden (veya kullanmak printf %q) değerlendirmektir.

Ayrıca , sol tarafın geçerli bir değişken adı evil_code #mı yoksa dizine sahip bir ad mı olduğunu kontrol etmelisiniz (ya öyleyse ?). Buna karşılık, aşağıdaki diğer tüm yöntemler otomatik olarak uygular.

# 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”

Downsides:

  • değişken adının geçerliliğini kontrol etmez.
  • eval şeytan.
  • eval şeytan.
  • eval şeytan.

1b. İle atamaread

readBuiltin sen adını, burada-dizeleri ile birlikte yararlanılabilir olani hangi bir değişkene değerler atamanızı sağlar:

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

IFSBölüm ve seçenek -rolduğu gibi seçenek ise değer atanır emin olun -d ''çok hatlı değerleri atamak için izin verir. Bu son seçenek nedeniyle, komut sıfırdan farklı bir çıkış koduyla döner.

Burada bir dize kullandığımızdan, değere bir yeni satır karakteri eklendiğini unutmayın.

Downsides:

  • biraz karanlık;
  • sıfır olmayan bir çıkış koduyla döner;
  • değere bir satırsonu ekler.

1c. İle atamaprintf

Bash 3.1'den beri (2005 yayınlandı), printfyerleşik ayrıca sonucunu adı verilen bir değişkene atayabilir. Önceki çözümlerin aksine, sadece işe yarıyor, şeylerden kaçmak, bölünmeyi önlemek vb. İçin ekstra çaba gerektirmiyor.

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

Downsides:

  • Daha az taşınabilir (ama iyi).

Yöntem 2. “Referans” değişkeni kullanma

Bash 4.3'ten (2014 yılında piyasaya sürüldüğünden) beri, declareyerleşik -nC ++ referansları gibi başka bir değişkene “ad referansı” olan bir değişken oluşturma seçeneğine sahiptir . Yöntem 1'deki gibi, başvuru da takma değişkenin adını saklar, ancak referansa her erişildiğinde (okuma veya atama için), Bash dolaylı olarak otomatik olarak çözer.

Buna ek olarak, Bash kendiniz referans kendisi, yargıç değerini almak için çok özel ve çok kafa karıştırıcı sözdizimi vardır: ${!ref}.

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

Bu, aşağıda açıklanan tuzaklardan kaçınmaz, ancak en azından sözdizimini basitleştirir.

Downsides:

  • Taşınabilir değil.

Riskler

Tüm bu takma teknikleri çeşitli riskler taşır. Birincisi, indirimi her çözdüğünüzde (okuma veya atama için) rastgele kod yürütmektir . Gerçekten de, skaler değişken adı yerine, gibi var_37, bir dizi altyazısını da takma adlandırabilirsiniz arr[42]. Ancak Bash, her seferinde gerektiğinde köşeli parantezlerin içeriğini değerlendirir, bu nedenle takma adınarr[$(do_evil)] beklenmedik etkileri olur ... Sonuç olarak, bu teknikleri yalnızca takma adın provenansını kontrol ettiğinizde kullanın .

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)

İkinci risk, döngüsel bir takma ad oluşturmaktır. Bash değişkenleri, adlarına göre değil, adlarına göre tanımlandığından, yanlışlıkla kendi başına bir takma ad oluşturabilirsiniz (bu, onu kapsayan bir kapsamdaki bir değişkeni takma ad olarak düşünürken). Bu özellikle ortak değişken adları (gibi var) kullanıldığında meydana gelebilir . Sonuç olarak, bu teknikleri yalnızca takma değişkenin adını kontrol ettiğinizde kullanın .

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!

Kaynak:


1
Bu en iyi cevaptır, özellikle ${!varname}teknik için bir ara değişken gerektirdiği için varname.
RichVel

Bu cevabın daha fazla yükseltilmediğini anlamak zor
Marcos

18

Aşağıdaki örnek $ name_of_var değerini döndürür

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

4
echoKomut ikamesi (tırnak işaretlerini özleyen) ile iki s iç içe yerleştirmek gereksizdir. Ayrıca, seçenek -nbelirtilmelidir echo. Ve her zaman evalolduğu gibi güvensizdir. Bash bu çok amaç için daha güvenli, daha net ve daha kısa sözdizimi vardır çünkü Ama tüm bu gereksizdir: ${!var}.
Maëlan

4

Bu çalışmalı:

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

Bu da işe yarayacak

my_country_code="green"
x="country"

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

Senin durumunda

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

3

Gereğince BashFAQ / 006 kullanabileceğiniz readile burada dize sözdizimi dolaylı değişkenleri atamak için:

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

Kullanımı:

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

3

kullanım declare

Diğer cevaplar gibi öneklerin kullanılmasına gerek yoktur, ikisi de diziler. Sadece kullanın declare, çift tırnak ve parametre genişleme .

Sıklıkla, aşağıdaki one to ngibi biçimlendirilmiş bağımsız değişken listelerini ayrıştırmak için aşağıdaki hile kullanılır key=value otherkey=othervalue etc=etc:

# 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

Ama argv listesini genişletmek gibi

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

Ekstra ipuçları

# 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
Bu çok temiz bir çözüm gibi görünüyor. Hiçbir kötülük Önlükler tefek öğeler ve değişkenler ile ilgili araçları kullanmak gibi görünüşte ilgisiz, hatta tehlikeli işlevleri kapamayın printfveyaeval
kvantour

2

Vay, sözdiziminin çoğu korkunç! Dizilere dolaylı olarak başvurmanız gerekiyorsa, daha basit bir sözdizimine sahip bir çözüm:

#!/bin/bash

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

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

Daha basit kullanım durumları için Gelişmiş Bash-Scripting Kılavuzu'nda açıklanan sözdizimini öneririm .


2
ABS, örneklerinde kötü uygulamalar sergilediği için kötü şöhretli biri. Lütfen bunun yerine doğrudan bash-hacker wiki'ye ya da doğrudan konuya giren BashFAQ # 6'ya sahip Wooledge wiki'ye yaslanmayı düşünün .
Charles Duffy

2
Bu girişler durumunda çalışır foo_1ve foo_2boşluk ve özel sembollerin serbesttir. Sorunlu girişlere örnekler: 'a b'içinde iki giriş oluşturur mine. ''içeride bir girdi oluşturmaz mine. '*'çalışma dizininin içeriğine genişleyecektir. Bu sorunları alıntı yaparak önleyebilirsiniz:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Bu, BASH içindeki herhangi bir dizi arasında geçiş yapmakla ilgili genel bir sorundur. Bu, IFS'yi geçici olarak değiştirerek (ve sonra elbette geri değiştirerek) da çözülebilir. Alıntı yapmanın işe yaradığını görmek güzel.
ingyhere

@ingyhere farklı olmaya yalvarıyorum. Bu genel bir sorun değil . Standart bir çözüm vardır: Her zaman [@]yapıları alıntılayın . "${array[@]}"kelime bölme veya genişletme gibi sorunlar olmadan her zaman doğru giriş listesine genişletilir *. Ayrıca, sözcük bölme sorunu yalnızca IFSdizinin içinde hiçbir zaman null olmayan herhangi bir karakter biliyorsanız engellenebilir . Ayrıca, somut bir şekilde *ayarlanarak tedavi gerçekleştirilemez IFS. Ya IFS='*'yıldızları belirlediniz ve böldünüz ya da ayarladınız IFS=somethingOtherve *genişlediniz.
Socowi

@Socowi Kabuk genişlemesinin istenmeyen olduğunu varsayıyorsunuz ve durum her zaman böyle değil. Geliştiriciler, kabuk ifadeleri her şeyi alıntıladıktan sonra genişlemediğinde hatalardan şikayet ediyorlar. İyi bir çözüm, verileri tanımak ve IFS kullanarak |veya LFIFS olarak bile komut dosyaları oluşturmaktır . Yine, döngülerdeki genel sorun , belirteçlerin varsayılan olarak gerçekleşmesidir, böylece tırnak, belirteç içeren genişletilmiş dizelere izin vermek için özel bir çözümdür . (Ya globbing / parametre genişletmesi ya da alıntılanmış genişletilmiş dizeler, ancak ikisi birden değil.) Bir varı okumak için 8 tırnak işareti gerekiyorsa, kabuk yanlış dildir.
ingyhere

1

Dizine alınmış diziler için bunlara şu şekilde başvurabilirsiniz:

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

İlişkilendirilebilir dizilere benzer şekilde başvurulabilir, ancak bunun yerine -Aaçılması gerekir .declare-a


1

Hangi kabuk / bash sürümüne bağlı olmayan ekstra bir yöntem kullanmaktır envsubst. Örneğin:

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

0

Komutun ilk argümanını içeren bir değişken adı oluşturmak istiyorum

script.sh dosya:

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

Ölçek:

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

Başı help eval:

Bağımsız değişkenleri kabuk komutu olarak yürütün.


Bash ${!var}dolaylı genişletmeyi daha önce de belirtildiği gibi kullanabilirsiniz , ancak dizi indekslerinin alınmasını desteklemez.


Daha fazla okuma veya örnek için Dolaylı BashFAQ / 006'ya bakınız .

POSIX veya Bourne mermilerindeki bu işlevselliği, kopyalamak evalzor olan herhangi bir hile yapabilecek hiçbir bilginin farkında değiliz. Yani, bunu kendi risk kesmek için bir kullanım olarak düşünün .

Bununla birlikte, aşağıdaki notlara göre dolaylı kullanmayı tekrar düşünmelisiniz.

Normalde, bash komut dosyalarında dolaylı referanslara ihtiyacınız olmaz. Genel olarak, insanlar Bash Dizilerini anlamadıklarında veya bilmediklerinde veya işlevler gibi diğer Bash özelliklerini tam olarak düşünmediklerinde buna bir çözüm ararlar.

Değişken adlarını veya başka bir bash sözdizimini parametrelerin içine koymak, daha iyi çözümlere sahip sorunları çözmek için sıklıkla yanlış ve uygunsuz durumlarda yapılır. Kod ve veri arasındaki ayrımı ihlal eder ve bu nedenle sizi hatalara ve güvenlik sorunlarına karşı kaygan bir eğime sokar. Dolaylı kod, kodunuzu daha az şeffaf ve takip etmeyi zorlaştırabilir.


Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.