Динамические имена переменных в Bash

Я запутался в bash script.

У меня есть следующий код:

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 и как мне его называть в script?

Я пробовал eval, ${...}, \$${...}, но я все еще запутался.

Ответы

Ответ 1

Используйте ассоциативный массив с именами команд в качестве ключей.

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

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

Если вы не можете использовать ассоциативные массивы (например, вы должны поддерживать bash 3), вы можете использовать declare для создания имен динамических переменных:

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

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

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

См. BashFAQ: косвенное обращение - оценка косвенных/ссылочных переменных.

Ответ 2

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

suffix=bzz
declare prefix_$suffix=mystr

... и затем...

varname=prefix_$suffix
echo ${!varname}

Ответ 3

Пример ниже возвращает значение $name_of_var

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

Ответ 4

Помимо ассоциативных массивов, в Bash есть несколько способов достижения динамических переменных. Обратите внимание, что все эти методы представляют риски, которые обсуждаются в конце этого ответа.

В следующих примерах я предполагаю, что i=37 и что вы хотите присвоить псевдоним переменной с именем var_37, начальное значение которой - lolilol.

Способ 1. Использование переменной указателя

Вы можете просто сохранить имя переменной в косвенной переменной, в отличие от указателя Си. Bash затем имеет синтаксис для чтения псевдонима переменной: ${!name} расширяется до значения переменной, имя которой равно значению переменной name. Вы можете думать об этом как о двухэтапном расширении: ${!name} расширяется до $var_37, который расширяется до lolilol.

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

К сожалению, нет никакого аналога синтаксиса для изменения псевдонима переменной. Вместо этого вы можете выполнить задание с помощью одного из следующих приемов.

1a. Назначение с 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'

Downsides:

  • не проверяет правильность имени переменной.
  • eval это зло.
  • eval это зло.
  • eval это зло.

1б. Назначение с read

Встроенная функция read позволяет вам присваивать значения переменной, которой вы даете имя, и этот факт можно использовать в сочетании с здесь-строками:

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

Часть IFS и опция -r следят за тем, чтобы значение было присвоено как есть, а опция -d '' позволяет назначать многострочные значения. Из-за этой последней опции команда возвращается с ненулевым кодом выхода.

Обратите внимание, что, поскольку мы используем строку here, к значению добавляется символ новой строки.

МИНУСЫ:

  • немного неясен;
  • возвращается с ненулевым кодом выхода;
  • добавляет новую строку к значению.

1c. Назначение с printf

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

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

Downsides:

  • Менее портативный (но хорошо).

Способ 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!

Источник:

Ответ 5

Это должно работать:

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"

Ответ 6

Это тоже будет работать

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

Ответ 7

Вау, большая часть синтаксиса ужасна! Вот одно решение с более простым синтаксисом, если вам нужно косвенно ссылаться на массивы:

#!/bin/bash

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

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

Для более простых случаев использования я рекомендую синтаксис, описанный в Advanced Bash-Scripting Guide.

Ответ 8

Для индексированных массивов вы можете ссылаться на них следующим образом:

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 нужно включить -a на declare.

Ответ 9

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

файл 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:

Выполните аргументы как команду оболочки.


Вы также можете использовать косвенное расширение Bash ${!var}, как уже упоминалось, однако оно не поддерживает получение индексов массива.


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

Нам неизвестны какие-либо хитрости, которые могут дублировать эту функциональность в оболочках POSIX или Bourne без eval, что может быть сложно сделать безопасно. Итак, рассмотрите это использование на свой страх и риск.

Тем не менее, вы должны пересмотреть использование косвенного обращения согласно следующим примечаниям.

Обычно в сценариях bash вам вообще не нужны косвенные ссылки. Обычно люди ищут это решение, когда они не понимают или не знают о массивах Bash или еще не полностью рассмотрели другие функции Bash, такие как функции.

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

Ответ 10

Согласно BashFAQ/006, вы можете использовать синтаксис read with here для назначения косвенных переменных:

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

Использование:

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

Ответ 11

Используйте 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

Но расширяем список argv как

for v in "[email protected]"; do declare "${v%=*}=${v#*=}"; done

Дополнительные советы

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

Ответ 12

для формата varname=$prefix_suffix, просто используйте:

varname=${prefix}_suffix