Bash 中的动态变量名

Posted

技术标签:

【中文标题】Bash 中的动态变量名【英文标题】:Dynamic variable names in Bash 【发布时间】:2013-05-09 07:39:22 【问题描述】:

我对 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$...\$$...,但我还是一头雾水。

【问题讨论】:

不要。使用关联数组将命令名称映射到数据。 VAR=A;值=333;阅读 "$VAR" 什么时候有用? @Timo 例如带有参数索引,如"$!ARGUMENT_INDEX:-default" 【参考方案1】:

我最近一直在寻找更好的方法。关联数组对我来说听起来有点矫枉过正。看看我发现了什么:

suffix=bzz
declare prefix_$suffix=mystr

...然后...

varname=prefix_$suffix
echo $!varname

【讨论】:

最好使用封装变量格式:prefix_$middle_postfix(即您的格式不适用于varname=$prefix_suffix 我被 bash 3 卡住了,无法使用关联数组;因此,这是一个救生员。 $!... 用谷歌搜索并不容易。我假设它只是扩展了一个 var 名称。 @NeilMcGill:见“man bash”gnu.org/software/bash/manual/html_node/…:参数扩展的基本形式是$parameter。 <...> 如果参数的第一个字符是感叹号(!),则引入了变量间接级别。 Bash 使用由其余参数形成的变量的值作为变量的名称;然后扩展此变量,并在其余的替换中使用该值,而不是参数本身的值。 @syntaxerror:您可以使用上面的“declare”命令任意分配值。 @Yorik.sar 您能否在答案本身中包含手册的摘录?这样更有意义,因为无论如何你都是作者。 TIA。【参考方案2】:

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

# 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:Indirection - Evaluating indirect/reference variables。

【讨论】:

@DeaDEnD -a 声明一个索引数组,而不是关联数组。除非grep_search 的参数是数字,否则它将被视为具有数值的参数(如果未设置参数,则默认为0)。 嗯。我正在使用 bash 4.2.45(2) 并声明没有将其列为选项 declare: usage: declare [-afFirtx] [-p] [name[=value] ...]。然而,它似乎工作正常。 为什么不只是declare $varname="foo" 有谁知道一种纯 POSIX 的方法,可以与 sh/dash 一起使用? $!varname 更简单且兼容广泛【参考方案3】:

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

在以下示例中,我将假设 i=37 并且您想要为名为 var_37 的变量设置别名,其初始值为 lolilol

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

您可以简单地将变量的名称存储在间接变量中,这与 C 指针不同。然后 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”

缺点:

不检查变量名的有效性。 eval 是邪恶的。 eval 是邪恶的。 eval 是邪恶的。

1b。使用read 分配

read 内置函数让您可以将值分配给您为其命名的变量,这一事实可以与 here-strings 结合使用:

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

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

请注意,由于我们使用的是 here-string,值会附加一个换行符。

缺点:

有点晦涩; 以非零退出代码返回; 将换行符附加到值。

1c。分配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!

来源:

BashFaq/006: How can I use variable variables (indirect variables, pointers, references) or associative arrays? BashFAQ/048: eval command and security issues

【讨论】:

这是最好的答案,特别是因为$!varname 技术需要varname 的中间变量。 很难理解这个答案没有得到更高的评价【参考方案4】:

下面的示例返回 $name_of_var 的值

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

【讨论】:

没有必要用命令替换(缺少引号)嵌套两个echos。另外,选项-n 应该给echo。而且,和往常一样,eval 是不安全的。但是所有这些都是不必要的,因为 Bash 有一个更安全、更清晰、更短的语法来实现这个目的:$!var【参考方案5】:

使用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 (( $# )); do
  case "$v" in ?*=?*) declare "$v%=*=$v#*=";; *) break;; esac
  shift
done

【讨论】:

这看起来是一个非常干净的解决方案。没有邪恶的围兜和鲍勃,您使用与变量相关的工具,而不是模糊看似无关甚至危险的功能,例如printfeval【参考方案6】:

将此处的两个高度评价的答案组合成一个完整的示例,希望有用且不言自明:

#!/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

输出:

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

再次,但读取常规变量: 你知道吗,我家有一只宠物猫 你知道吗,我家有一只宠物鸡 你知道吗,我家有一头宠物牛 你知道吗,我家有一只宠物狗 你知道吗,我家有一只宠物猪

【讨论】:

【参考方案7】:

这个也可以

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

【讨论】:

【参考方案8】:

这应该可行:

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"

【讨论】:

【参考方案9】:

根据BashFAQ/006,您可以使用read 和here string syntax 来分配间接变量:

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

用法:

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

【讨论】:

【参考方案10】:

一种不依赖于您拥有的 shell/bash 版本的额外方法是使用envsubst。例如:

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

【讨论】:

【参考方案11】:

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

#!/bin/bash

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

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

对于更简单的用例,我推荐syntax described in the Advanced Bash-Scripting Guide

【讨论】:

ABS 因在其示例中展示不良做法而臭名昭著。请考虑改用bash-hackers wiki 或Wooledge wiki——它有直接的主题条目BashFAQ #6——。 只有在foo_1foo_2 中的条目没有空格和特殊符号时才有效。有问题的条目示例:'a b' 将在 mine 内创建两个条目。 '' 不会在 mine 内创建条目。 '*' 将扩展为工作目录的内容。您可以通过引用来防止这些问题:eval 'mine=( "$foo_'"$i"'[@]" )' @Socowi 这是在 BASH 中循环遍历任何数组的普遍问题。这也可以通过临时更改 IFS(然后当然再改回来)来解决。很高兴看到报价成功。 @ingyhere 我不敢苟同。这不是一个普遍的问题。有一个标准解决方案:始终引用 [@] 构造。 "$array[@]" 将始终扩展到正确的条目列表,而不会出现分词或 * 扩展等问题。此外,如果您知道任何从未出现在数组中的非空字符,则只能使用 IFS 规避分词问题。此外,* 的字面处理无法通过设置 IFS 来实现。要么设置 IFS='*' 并在星星处拆分,要么设置 IFS=somethingOther 并且 * 扩展。 @Socowi 循环中的一般问题是默认情况下会发生标记化,因此引用是允许包含标记的扩展字符串的特殊解决方案。我更新了答案以删除使读者感到困惑的引用数组值。该答案的重点是创建更简单的语法,而不是针对需要引号来详细说明扩展变量的用例的特定答案。特定用例的作业引用可以留给其他开发人员想象。【参考方案12】:

尽管这是一个老问题,但我仍然很难获取动态变量名称,同时避免使用 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

【讨论】:

【参考方案13】:

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

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

而不是“!”

【讨论】:

P 是什么意思? man zshall, section PARAMETER EXPANSION, subsection Parameter Expansion Flags 中有解释:P:这会强制参数name 的值被解释为另一个参数名称,其值将在适当的地方使用。 [...]【参考方案14】:

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

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

【讨论】:

【参考方案15】:

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

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 about Indirection。

我们不知道有任何技巧可以在没有eval 的情况下在 POSIX 或 Bourne shell 中复制该功能,这很难安全地完成。因此,请考虑这是一种使用风险自负的黑客攻击

但是,您应该按照以下说明重新考虑使用间接。

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

将变量名称或任何其他 bash 语法放入参数中经常会不正确,并且在不适当的情况下以解决具有更好解决方案的问题。它违反了代码和数据之间的分离,因此使您陷入错误和安全问题的滑坡。间接性会使您的代码不那么透明且难以遵循。

【讨论】:

【参考方案16】:

KISS 方法:

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

结果为 4

【讨论】:

"echo bam1" 将输出 "bam1",而不是 "4"【参考方案17】:

符合 POSIX 的答案

对于此解决方案,您需要对 /tmp 文件夹具有读/写权限。 我们创建一个临时文件来保存我们的变量并利用 set 内置的 -a 标志:

$ 人集 ...-a 每个创建或修改的变量或函数都被赋予导出属性并标记为导出到后续命令的环境。

因此,如果我们创建一个包含动态变量的文件,我们可以使用 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

【讨论】:

【参考方案18】:

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

varname=$prefix_suffix

【讨论】:

以上是关于Bash 中的动态变量名的主要内容,如果未能解决你的问题,请参考以下文章

bash中的变量

plpgsql 中的动态变量名(字符串到变量名)

如何访问树枝中的动态变量名?

认识与学习bash

SQL Server 查询中的动态表名和变量名

如何在单个 bash 变量中使用多个主机名?