Імена динамічних змінних у Bash


159

Мене бентежить сценарій баш.

У мене є такий код:

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

Я хочу мати змогу створити ім'я змінної, що містить перший аргумент команди і має значення, наприклад, останній рядок ls.

Отже, щоб проілюструвати те, що я хочу:

$ ls | tail -1
stack-overflow.txt

$ grep_search() open_box
stack-overflow.txt

Отже, як я повинен визначити / оголосити $magic_way_to_define_magic_variable_$1і як я повинен викликати це в сценарії?

Я спробував eval, ${...}, \$${...}, але я до сих пір плутаю.


3
Не варто. Використовуйте асоціативний масив, щоб зіставити ім'я команди до даних.
чепнер

3
VAR = A; VAL = 333; читати "$ VAR" <<< "$ VAL"; відлуння "A = $ A"
Григорій К

Відповіді:


150

Використовуйте асоціативний масив із назви команд як ключі.

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

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

Якщо ви не можете використовувати асоціативні масиви (наприклад, ви повинні підтримувати bash3), ви можете використовувати declareдля створення імен динамічних змінних:

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

і використовувати непряме розширення параметрів для доступу до значення.

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

Див. BashFAQ: Непрямий - Оцінка непрямих / опорних змінних .


5
@DeaDEnD -aоголошує індексований масив, а не асоціативний масив. Якщо аргументом до grep_searchчисла є число, воно буде розглядатися як параметр з числовим значенням (який за замовчуванням дорівнює 0, якщо параметр не встановлений).
чепнер

1
Хм. Я використовую bash 4.2.45(2)і заявляю, що це не вказано як варіант declare: usage: declare [-afFirtx] [-p] [name[=value] ...]. Однак, здається, працює правильно.
схиляється

declare -hв 4.2.45 (2) для мене показує declare: usage: declare [-aAfFgilrtux] [-p] [name[=value] ...]. Ви можете двічі перевірити, чи справді ви працюєте 4.x, а не 3.2.
чепнер

5
Чому б не просто declare $varname="foo"?
Бен Девіс

1
${!varname}набагато простіший і широко сумісний
Бред Хайн

227

Я шукав кращого способу зробити це останнім часом. Асоціативний масив звучав як надмірний для мене. Дивись що я знайшов:

suffix=bzz
declare prefix_$suffix=mystr

...і потім...

varname=prefix_$suffix
echo ${!varname}

Якщо ви хочете оголосити глобальний всередині функції, можете використовувати "оголосити -g" в bash> = 4.2. У попередньому файлі bash можна використовувати "readonly" замість "объявить", доки ви не хочете пізніше змінювати значення. Може бути добре для конфігурації чи що у вас є.
Сем Уоткінс

7
найкраще використовувати інкапсульований змінний формат: prefix_${middle}_postfix(тобто ваше форматування не працюватиме varname=$prefix_suffix)
msciwoj

1
Я застряг у bash 3 і не міг використовувати асоціативні масиви; як таке це було рятівником життя. $ {! ...} не просто перейти на Google. Я припускаю, що це просто розширює ім'я var.
Ніл Макгілл

10
@NeilMcGill: Див. "Man bash" gnu.org/software/bash/manual/html_node/… : Основна форма розширення параметра - $ {parameter}. <...> Якщо першим символом параметра є знак оклику (!), Вводиться рівень змінної непрямості. Bash використовує значення змінної, утвореної з решти параметра, як ім'я змінної; Потім ця змінна розширюється, і це значення використовується в решті підстановки, а не в значенні самого параметра.
Yorik.sar

1
@syntaxerror: ви можете призначити значення скільки завгодно, скориставшись командою "заявити" вище.
Yorik.sar

48

Крім асоціативних масивів, існує кілька способів досягнення динамічних змінних у Bash. Зауважте, що всі ці методи становлять ризики, про які йде мова в кінці цієї відповіді.

У наступних прикладах я припускаю, i=37що ви хочете псевдонім змінної, названої var_37початковим значенням lolilol.

Метод 1. Використання змінної “pointer”

Ви можете просто зберегти ім'я змінної в непрямій змінній, не на відміну від вказівника C. Потім Bash має синтаксис для читання aliase змінної: ${!name}розширюється до значення змінної, ім'я якої - значення змінної name. Ви можете мислити це як двоступеневе розширення: ${!name}розширюється до $var_37, яке розширюється до lolilol.

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

На жаль, не існує відповідного синтаксису для модифікації aliase змінної. Натомість ви можете домогтися завдання одним із наступних прийомів.

1а. Призначення сeval

evalє злом, але це також найпростіший і портативний спосіб досягнення нашої мети. Ви повинні обережно уникнути правої частини завдання, оскільки вона буде оцінюватися двічі . Простий та систематичний спосіб зробити це попередньо оцінити праву частину (або використовувати printf %q).

І ви повинні вручну перевірити, чи ліва частина є дійсним ім'ям змінної або ім'ям з індексом (що, якщо це було evil_code #?). На відміну від цього, всі інші методи, наведені нижче, застосовують його автоматично.

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

Недоліки:

  • не перевіряє дійсність імені змінної.
  • eval є зло.
  • eval є зло.
  • eval є зло.

1б. Призначення сread

readВбудоване дозволяє привласнити значення змінної з яких ви даєте ім'я, факт , який може бути використаний в поєднанні з тут-рядками:

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

IFSЧастина і опції -rпереконайтеся , що значення присвоюється як є, в той час як опція -d ''дозволяє задавати значення декількох рядків. Через цю останню опцію команда повертається з ненульовим кодом виходу.

Зауважте, що оскільки ми використовуємо тут-рядок, до цього значення додається символ нового рядка.

Недоліки:

  • дещо незрозумілий;
  • повертається з ненульовим кодом виходу;
  • додає новий рядок до значення.

1с. Призначення сprintf

Оскільки Bash 3.1 (випущений 2005 р.), printfВбудований також може призначити його результат змінній, ім'я якої надано. На відміну від попередніх рішень, воно просто працює, додаткові зусилля не потрібні, щоб уникнути речей, щоб запобігти розщепленню тощо.

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

Недоліки:

  • Менш портативний (але, добре).

Метод 2. Використання змінної "еталон"

Оскільки Bash 4.3 (випущений 2014 р.), declareВбудований має можливість -nстворити змінну, яка є "посиланням на ім'я" на іншу змінну, подібно до посилань на C ++. Так само, як у Способі 1, посилання зберігає ім'я зведеної змінної, але щоразу, коли доступ до посилання (читання чи присвоєння), Bash автоматично вирішує непрямий характер.

Крім того, Bash має спеціальне і дуже заплутаний синтаксис для отримання значення самого посилання, судді самі: ${!ref}.

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

Це не дозволяє уникнути підводних каменів, пояснених нижче, але принаймні це робить синтаксис прямим.

Недоліки:

  • Не портативний.

Ризики

Усі ці методи випромінювання становлять кілька ризиків. Перший - це виконувати довільний код щоразу, коли ви вирішуєте непрямість (читання чи присвоєння) . Дійсно, замість імені скалярної змінної, як-от var_37, ви також можете мати псевдонім матриці, наприклад arr[42]. Але Bash оцінює вміст квадратних дужок щоразу, коли це потрібно, тому згладжування arr[$(do_evil)]матиме несподівані ефекти… Як наслідок, використовуйте ці методи лише тоді, коли ви керуєте походженням псевдоніму .

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)

Другий ризик - це створення циклічного псевдоніма. Оскільки змінні Bash ідентифікуються за своїм ім’ям, а не за їх сферою, ви можете ненавмисно створити собі псевдонім (думаючи, що це буде псевдонім змінної в області, що додається). Це може статися, зокрема, при використанні загальних імен змінних (наприклад var). Як наслідок, використовуйте ці методи лише тоді, коли ви керуєте ім'ям псевдозмінної змінної .

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!

Джерело:


1
Це найкраща відповідь, тим більше, що для ${!varname}техніки потрібен проміжний варіант для varname.
RichVel

Важко зрозуміти, що ця відповідь не була озвучена вище
Маркос

18

Приклад нижче повертає значення $ name_of_var

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

4
Вкладати два echos із заміною команди (яка не вистачає лапок) непотрібна. Плюс, -nслід надати варіант echo. І, як завжди, evalнебезпечно. Але все це не потрібно , оскільки Bash має більш безпечний, більш чіткий і більш короткий синтаксис для цієї мети: ${!var}.
Maëlan

4

Це має працювати:

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

Це теж буде працювати

my_country_code="green"
x="country"

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

У вашому випадку

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

3

Згідно BashFAQ / 006 , ви можете використовувати readз тут рядки синтаксис для призначення непрямих змінних:

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

Використання:

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

3

Використовуйте declare

Не потрібно використовувати префікси, як в інших відповідях, ні масиви. Використовуйте тільки declare, подвійні лапки , а також розширення параметра .

Я часто використовую наступний трюк для розбору списків аргументів, що містять one to nаргументи, відформатовані як 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

Але розширення списку аргументів, як

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

Додаткові поради

# 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
Це виглядає як дуже чисте рішення. Жодних злих нападників і бобів, і ви використовуєте інструменти, пов’язані зі змінними, не затьмарені, здавалося б, не пов’язані між собою або навіть небезпечні функції, такі як printfабоeval
kvantour

2

Нічого собі, більша частина синтаксису жахлива! Ось одне рішення з більш простим синтаксисом, якщо вам потрібно опосередковано посилатися на масиви:

#!/bin/bash

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

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

Для більш простих випадків використання я рекомендую синтаксис, описаний у Додатковому посібнику сценаріїв Bash .


2
АБС - хтось відомий тим, що демонструє погані практики на своїх прикладах. Будь ласка, подумайте, спираючись на вікі-хакери або на Вікі Wooledge - який має безпосередньо тему BashFAQ # 6 на тему .
Чарльз Даффі

2
Це працює лише в тому випадку, якщо записи в них відсутні foo_1і foo_2не містять пробілів та спеціальних символів. Приклади проблемних записів: 'a b'створять два записи всередині mine. ''не створить запис всередині mine. '*'розшириться до вмісту робочого каталогу. Ви можете запобігти цим проблемам, цитуючи:eval 'mine=( "${foo_'"$i"'[@]}" )'
Socowi

@Socowi Це загальна проблема із прокручуванням будь-якого масиву в BASH. Це також можна вирішити, тимчасово змінивши IFS (а потім, звичайно, змінивши його назад). Добре бачити, що цитування відпрацьоване.
ingyhere

@ingyту я прошу відрізнятися. Це не загальна проблема. Є стандартне рішення: Завжди цитуйте [@]конструкції. "${array[@]}"завжди буде розширюватися до правильного списку записів без проблем, таких як розділення слів або розширення *. Також проблему розділення слів можна обійти лише тоді, IFSколи ви знаєте будь-який ненульовий символ, який ніколи не з’являється всередині масиву. Крім того, буквальне поводження з *не може бути досягнуто шляхом встановлення IFS. Або ви встановлюєте IFS='*'і розділяєте зірки, або встановлюєте IFS=somethingOtherі *розширюєте.
Socowi

@Socowi Ви припускаєте, що розширення оболонки небажане, і це не завжди так. Розробники скаржаться на помилки, коли вирази оболонок не розширюються після цитування всього. Хорошим рішенням є знання даних та створення сценаріїв належним чином, навіть використовуючи |або LFяк IFS. Знову ж таки, загальна проблема циклів полягає в тому, що токенізація відбувається за замовчуванням, так що цитування є спеціальним рішенням, яке дозволяє розширені рядки, що містять лексеми. (Це або глобалізація / розширення параметрів, або цитовані розширені рядки, але не обидва.) Якщо для читання var потрібно 8 лапок, оболонка - це неправильна мова.
ingyhere

1

Для індексованих масивів ви можете посилатися на них так:

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

Асоціативні масиви можуть посилатися аналогічно, але замість цього потрібно -Aввімкнути .declare-a


1

Додатковий метод, який не покладається на те, яку оболонку / bash версію у вас є, використовуючи envsubst. Наприклад:

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

0

Я хочу мати змогу створити ім’я змінної, що містить перший аргумент команди

script.sh файл:

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

Тест:

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

Відповідно до help eval:

Виконайте аргументи у вигляді команди оболонки.


Ви також можете використовувати ${!var}непряме розширення Bash , як уже згадувалося, однак воно не підтримує отримання індексів масиву.


Для подальшого читання або прикладів перевірте BashFAQ / 006 про непряму сторону .

Нам не відомий жоден трюк, який може дублювати цю функціональність в оболонках POSIX або Bourne без цього eval, що може бути важко зробити надійно. Отже, вважайте це використанням на свій страх і ризик .

Однак вам слід задуматися про використання непрямості, як показано в наступних зауваженнях.

Зазвичай в сценаріях з басом вам взагалі не знадобляться непрямі посилання. Як правило, люди розглядають це рішення, коли вони не розуміють і не знають про Bash Arrays або не повністю розглядають інші функції Bash, такі як функції.

Введення імен змінних або будь-якого іншого синтаксису bash всередині параметрів часто виконується неправильно і в невідповідних ситуаціях для вирішення проблем, які мають кращі рішення. Це порушує розділення між кодом і даними, і як таке ставить вас на слизький схил до помилок та проблем безпеки. Непрямий спосіб може зробити ваш код менш прозорим і важче дотримуватися.


Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.