Bash中的动态变量名

我对bash脚本感到困惑。

我有以下代码:

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${...}\$${...},但我仍然困惑。

296896 次浏览

使用关联数组,命令名作为键。

# 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: 间接-评估间接/参考变量

我最近一直在寻找更好的方法。联想数组对我来说太夸张了。看看我发现了什么

suffix=bzz
declare prefix_$suffix=mystr

...然后……

varname=prefix_$suffix
echo ${!varname}

文档:

' $ '字符用于参数展开、命令替换或算术展开。...

参数展开的基本形式是${parameter}。参数的值被替换。...

如果parameter的第一个字符是一个感叹号(!),并且parameter不是一个命名ef,那么它引入了一个间接级别。Bash使用展开parameter其余部分形成的值作为新参数;然后展开该参数,并将该值用于展开的其余部分,而不是原始参数的展开。这就是所谓的间接扩张。可进行波浪号展开、参数展开、命令替换和算术展开。...

对于varname=$prefix_suffix格式,只需使用:

varname=${prefix}_suffix

这应该可以工作:

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"

哇,大部分语法都很糟糕!如果你需要间接引用数组,这里有一个简单语法的解决方案:

#!/bin/bash


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


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

对于更简单的用例,我推荐参见高级Bash-Scripting Guide

对于索引数组,你可以像这样引用它们:

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

关联数组可以类似地引用,但需要在declare而不是-a上使用-A开关。

下面的示例返回$name_of_var的值

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

我希望能够创建一个包含该命令的第一个参数的变量名

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:

作为shell命令执行参数。


你也可以使用Bash ${!var}间接展开,正如前面提到的,但是它不支持检索数组下标。


如需进一步阅读或示例,请检查BashFAQ/006关于间接

我们不知道有任何技巧可以在POSIX或Bourne shell中复制没有eval的功能,这可能很难安全地做到。所以,考虑这是一个使用你自己的风险黑客

但是,您应该根据以下注意事项重新考虑使用间接方法。

通常,在bash脚本中,根本不需要间接引用。通常,当人们不理解或不了解Bash数组或没有充分考虑其他Bash特性(如函数)时,他们会将此视为解决方案。

在参数中放置变量名或任何其他bash语法通常是不正确的,并且在不合适的情况下解决了有更好解决方案的问题。它违反了代码和数据之间的分离,因此会导致错误和安全问题。间接性会使你的代码不那么透明,更难理解。

根据BashFAQ / 006,你可以使用read这里是字符串语法来分配间接变量:

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

用法:

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

这个也可以

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

除了关联数组之外,在Bash中还有几种实现动态变量的方法。请注意,所有这些技术都存在风险,这些风险将在本回答的最后讨论。

在下面的例子中,我将假设i=37和你想要别名名为var_37的变量,其初始值为lolilol

方法1。使用“指针”变量

您可以简单地将变量名存储在间接变量中,这与C指针没有什么不同。然后Bash有一个别名变量阅读的语法:${!name}展开为变量名为name的变量的值。你可以把它想象成一个两阶段的展开:${!name}展开到$var_37,而$var_37展开到lolilol

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

不幸的是,别名变量修改没有对应的语法。相反,你可以用以下技巧之一来完成任务。

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 b。使用read赋值

内置的read允许你给一个变量赋值,这个变量的名字是你指定的,这个事实可以和here-strings结合使用:

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

IFS部分和选项-r确保值按原样分配,而选项-d ''允许分配多行值。由于最后一个选项,该命令返回一个非零退出码。

请注意,由于我们使用的是here字符串,一个换行符被附加到值上。

缺点:

  • 有点模糊的;
  • 返回非零退出码;
  • 将换行符追加到值。

1 c。使用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!

来源:

使用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 "$@"; 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 test $# -gt 0; do
case "$1" in ?*=?*) declare "${1%=*}=${1#*=}";; *) break;; esac
shift
done

另一个不依赖于你的shell/bash版本的方法是使用envsubst。例如:

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

尽管这是一个老问题,但在获取动态变量名时,我仍然遇到了一些困难,同时避免使用eval(邪恶)命令。

declare -n解决了这个问题,它创建了一个动态值的引用,这在CI/CD进程中特别有用,其中CI/CD服务所需的秘密名称直到运行时才知道。方法如下:

# Bash v4.3+
# -----------------------------------------------------------
# Secerts in CI/CD service, injected as environment variables
# AWS_ACCESS_KEY_ID_DEV, AWS_SECRET_ACCESS_KEY_DEV
# AWS_ACCESS_KEY_ID_STG, AWS_SECRET_ACCESS_KEY_STG
# -----------------------------------------------------------
# Environment variables injected by CI/CD service
# BRANCH_NAME="DEV"
# -----------------------------------------------------------
declare -n _AWS_ACCESS_KEY_ID_REF=AWS_ACCESS_KEY_ID_${BRANCH_NAME}
declare -n _AWS_SECRET_ACCESS_KEY_REF=AWS_SECRET_ACCESS_KEY_${BRANCH_NAME}


export AWS_ACCESS_KEY_ID=${_AWS_ACCESS_KEY_ID_REF}
export AWS_SECRET_ACCESS_KEY=${_AWS_SECRET_ACCESS_KEY_REF}


echo $AWS_ACCESS_KEY_ID $AWS_SECRET_ACCESS_KEY
aws s3 ls

将两个评分较高的答案结合成一个完整的例子,希望有用且不言自明:

#!/bin/bash


intro="You know what,"
pet1="cat"
pet2="chicken"
pet3="cow"
pet4="dog"
pet5="pig"


# Setting and reading dynamic variables
for i in {1..5}; do
pet="pet$i"
declare "sentence$i=$intro I have a pet ${!pet} at home"
done


# Just reading dynamic variables
for i in {1..5}; do
sentence="sentence$i"
echo "${!sentence}"
done


echo
echo "Again, but reading regular variables:"
echo $sentence1
echo $sentence2
echo $sentence3
echo $sentence4
echo $sentence5

输出:

你知道吗,我家里有一只宠物猫 你知道吗,我家里有一只宠物鸡 你知道吗,我家里有一头宠物牛 你知道吗,我家里有一只宠物狗 你知道吗,我家里有一只宠物猪

同样,但读取的是规则变量:
你知道吗,我家里有一只宠物猫 你知道吗,我家里有一只宠物鸡 你知道吗,我家里有一头宠物牛 你知道吗,我家里有一只宠物狗 你知道吗,我家里有一只宠物猪

对于zsh(更新的mac os版本),您应该使用

real_var="holaaaa"
aux_var="real_var"
echo ${(P)aux_var}
holaaaa

而不是"!

接吻的方法:

a=1
c="bam"
let "$c$a"=4
echo $bam1

结果4

POSIX兼容的答案

对于这个解决方案,你需要拥有/tmp文件夹的r/w权限。
我们创建一个临时文件保存变量,并利用set内置的-a标志:

$ man set
...
创建或修改的每个变量或函数都被赋予export属性,并标记为可导出到后续命令的环境。

因此,如果我们创建一个包含动态变量的文件,我们可以使用set在脚本中赋予它们生命。

实现

#!/bin/sh
# Give the temp file a unique name so you don't mess with any other files in there
ENV_FILE="/tmp/$(date +%s)"


MY_KEY=foo
MY_VALUE=bar


echo "$MY_KEY=$MY_VALUE" >> "$ENV_FILE"


# Now that our env file is created and populated, we can use "set"
set -a; . "$ENV_FILE"; set +a
rm "$ENV_FILE"
echo "$foo"


# Output is "bar" (without quotes)

解释以上步骤:

# Enables the -a behavior
set -a


# Sources the env file
. "$ENV_FILE"


# Disables the -a behavior
set +a

虽然我认为declare -n仍然是最好的方法,但还有另一种没有人提到的方法,在CI/CD中非常有用

function dynamic(){
export a_$1="bla"
}


dynamic 2
echo $a_2

此函数不支持空格,因此dynamic "2 3"将返回错误。